dsh-zen-remote 2.3.11 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/CHANGELOG.md +39 -78
  2. package/README.md +94 -84
  3. package/README.zh-CN.md +90 -81
  4. package/bin/zen-remote.mjs +5 -0
  5. package/docs/interface.md +6 -6
  6. package/docs/remote-access.en.md +6 -4
  7. package/docs/remote-access.md +6 -4
  8. package/dsh-push.mjs +96 -10
  9. package/lib/adapters/dsh-better-sidebar/index.js +7 -3
  10. package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
  11. package/lib/admin-routes.js +10 -2
  12. package/lib/admin-routes.js.map +1 -1
  13. package/lib/cli/args.js +251 -0
  14. package/lib/cli/args.js.map +1 -0
  15. package/lib/cli/bootstrap.js +449 -0
  16. package/lib/cli/bootstrap.js.map +1 -0
  17. package/lib/cli/command-setup.js +484 -0
  18. package/lib/cli/command-setup.js.map +1 -0
  19. package/lib/cli/commands-client.js +359 -0
  20. package/lib/cli/commands-client.js.map +1 -0
  21. package/lib/cli/commands-host.js +1108 -0
  22. package/lib/cli/commands-host.js.map +1 -0
  23. package/lib/cli/commands-status.js +279 -0
  24. package/lib/cli/commands-status.js.map +1 -0
  25. package/lib/cli/config-writer.js +334 -0
  26. package/lib/cli/config-writer.js.map +1 -0
  27. package/lib/cli/context.js +10 -0
  28. package/lib/cli/context.js.map +1 -0
  29. package/lib/cli/i18n.js +620 -0
  30. package/lib/cli/i18n.js.map +1 -0
  31. package/lib/cli/ipc-client.js +160 -0
  32. package/lib/cli/ipc-client.js.map +1 -0
  33. package/lib/cli/ipc-server.js +464 -0
  34. package/lib/cli/ipc-server.js.map +1 -0
  35. package/lib/cli/main.js +245 -0
  36. package/lib/cli/main.js.map +1 -0
  37. package/lib/cli/qr-terminal.js +48 -0
  38. package/lib/cli/qr-terminal.js.map +1 -0
  39. package/lib/client-data/qr.js +286 -0
  40. package/lib/client-data/qr.js.map +1 -0
  41. package/lib/client-routes.js +308 -25
  42. package/lib/client-routes.js.map +1 -1
  43. package/lib/client.js +1013 -229
  44. package/lib/config.js +50 -12
  45. package/lib/config.js.map +1 -1
  46. package/lib/debug-report.js +28 -1
  47. package/lib/debug-report.js.map +1 -1
  48. package/lib/fetch-route-intercept.js +334 -26
  49. package/lib/fetch-route-intercept.js.map +1 -1
  50. package/lib/index.js +564 -363
  51. package/lib/index.js.map +1 -1
  52. package/lib/intercept.js +1833 -280
  53. package/lib/intercept.js.map +1 -1
  54. package/lib/lan-gate-server.cjs +36 -10
  55. package/lib/merge-streams.js +103 -11
  56. package/lib/merge-streams.js.map +1 -1
  57. package/lib/plugin-route-intercept.js +93 -63
  58. package/lib/plugin-route-intercept.js.map +1 -1
  59. package/lib/relay-access.js +8 -1
  60. package/lib/relay-access.js.map +1 -1
  61. package/lib/relay-client.js +133 -4
  62. package/lib/relay-client.js.map +1 -1
  63. package/lib/relay-pool.js +275 -0
  64. package/lib/relay-pool.js.map +1 -0
  65. package/lib/relay-server.js +269 -17
  66. package/lib/relay-server.js.map +1 -1
  67. package/lib/restart-fields.js +6 -4
  68. package/lib/restart-fields.js.map +1 -1
  69. package/lib/share-store.js +65 -27
  70. package/lib/share-store.js.map +1 -1
  71. package/lib/state-store.js +400 -0
  72. package/lib/state-store.js.map +1 -0
  73. package/lib/types/adapters/dsh-better-sidebar/index.d.ts +3 -2
  74. package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
  75. package/lib/types/admin-routes.d.ts.map +1 -1
  76. package/lib/types/cli/args.d.ts +78 -0
  77. package/lib/types/cli/args.d.ts.map +1 -0
  78. package/lib/types/cli/bootstrap.d.ts +170 -0
  79. package/lib/types/cli/bootstrap.d.ts.map +1 -0
  80. package/lib/types/cli/command-setup.d.ts +72 -0
  81. package/lib/types/cli/command-setup.d.ts.map +1 -0
  82. package/lib/types/cli/commands-client.d.ts +36 -0
  83. package/lib/types/cli/commands-client.d.ts.map +1 -0
  84. package/lib/types/cli/commands-host.d.ts +82 -0
  85. package/lib/types/cli/commands-host.d.ts.map +1 -0
  86. package/lib/types/cli/commands-status.d.ts +46 -0
  87. package/lib/types/cli/commands-status.d.ts.map +1 -0
  88. package/lib/types/cli/config-writer.d.ts +92 -0
  89. package/lib/types/cli/config-writer.d.ts.map +1 -0
  90. package/lib/types/cli/context.d.ts +39 -0
  91. package/lib/types/cli/context.d.ts.map +1 -0
  92. package/lib/types/cli/i18n.d.ts +32 -0
  93. package/lib/types/cli/i18n.d.ts.map +1 -0
  94. package/lib/types/cli/ipc-client.d.ts +83 -0
  95. package/lib/types/cli/ipc-client.d.ts.map +1 -0
  96. package/lib/types/cli/ipc-server.d.ts +113 -0
  97. package/lib/types/cli/ipc-server.d.ts.map +1 -0
  98. package/lib/types/cli/main.d.ts +57 -0
  99. package/lib/types/cli/main.d.ts.map +1 -0
  100. package/lib/types/cli/qr-terminal.d.ts +17 -0
  101. package/lib/types/cli/qr-terminal.d.ts.map +1 -0
  102. package/lib/types/client/RemoteStatusIcon.d.ts.map +1 -1
  103. package/lib/types/client/current-session-report.d.ts +4 -0
  104. package/lib/types/client/current-session-report.d.ts.map +1 -1
  105. package/lib/types/client/effects/model-group-side.d.ts.map +1 -1
  106. package/lib/types/client/effects/remote-session.d.ts.map +1 -1
  107. package/lib/types/client/locales.d.ts +47 -3
  108. package/lib/types/client/locales.d.ts.map +1 -1
  109. package/lib/types/client/settings/SettingsSection.d.ts.map +1 -1
  110. package/lib/types/client/settings/settings-css.d.ts +1 -1
  111. package/lib/types/client/settings/settings-css.d.ts.map +1 -1
  112. package/lib/types/client/styles/remote-session.css.d.ts +1 -1
  113. package/lib/types/client/styles/remote-session.css.d.ts.map +1 -1
  114. package/lib/types/client/wizard/GatewayWizard.d.ts.map +1 -1
  115. package/lib/types/client/wizard/cards/ProxyCard.d.ts.map +1 -1
  116. package/lib/types/client/wizard/wizard-css.d.ts +1 -1
  117. package/lib/types/client/wizard/wizard-css.d.ts.map +1 -1
  118. package/lib/types/client-data/model-group-side.d.ts +23 -2
  119. package/lib/types/client-data/model-group-side.d.ts.map +1 -1
  120. package/lib/types/client-data/remote-status.d.ts +47 -1
  121. package/lib/types/client-data/remote-status.d.ts.map +1 -1
  122. package/lib/types/client-data/settings-form.d.ts +85 -1
  123. package/lib/types/client-data/settings-form.d.ts.map +1 -1
  124. package/lib/types/client-data/wizard.d.ts +56 -1
  125. package/lib/types/client-data/wizard.d.ts.map +1 -1
  126. package/lib/types/client-routes.d.ts +25 -1
  127. package/lib/types/client-routes.d.ts.map +1 -1
  128. package/lib/types/config.d.ts +40 -14
  129. package/lib/types/config.d.ts.map +1 -1
  130. package/lib/types/debug-report.d.ts.map +1 -1
  131. package/lib/types/fetch-route-intercept.d.ts +66 -8
  132. package/lib/types/fetch-route-intercept.d.ts.map +1 -1
  133. package/lib/types/index.d.ts +9 -2
  134. package/lib/types/index.d.ts.map +1 -1
  135. package/lib/types/intercept.d.ts +72 -10
  136. package/lib/types/intercept.d.ts.map +1 -1
  137. package/lib/types/merge-streams.d.ts +79 -1
  138. package/lib/types/merge-streams.d.ts.map +1 -1
  139. package/lib/types/plugin-route-intercept.d.ts +25 -9
  140. package/lib/types/plugin-route-intercept.d.ts.map +1 -1
  141. package/lib/types/relay-access.d.ts +3 -1
  142. package/lib/types/relay-access.d.ts.map +1 -1
  143. package/lib/types/relay-client.d.ts +23 -2
  144. package/lib/types/relay-client.d.ts.map +1 -1
  145. package/lib/types/relay-pool.d.ts +148 -0
  146. package/lib/types/relay-pool.d.ts.map +1 -0
  147. package/lib/types/relay-server.d.ts +30 -3
  148. package/lib/types/relay-server.d.ts.map +1 -1
  149. package/lib/types/restart-fields.d.ts +5 -5
  150. package/lib/types/restart-fields.d.ts.map +1 -1
  151. package/lib/types/share-store.d.ts.map +1 -1
  152. package/lib/types/state-store.d.ts +179 -0
  153. package/lib/types/state-store.d.ts.map +1 -0
  154. package/lib/types/upload-limits.d.ts +15 -0
  155. package/lib/types/upload-limits.d.ts.map +1 -1
  156. package/lib/types/virtual-id.d.ts +21 -0
  157. package/lib/types/virtual-id.d.ts.map +1 -1
  158. package/lib/types/wizard/probe.d.ts.map +1 -1
  159. package/lib/types/wizard/proxy-snippets.d.ts +3 -0
  160. package/lib/types/wizard/proxy-snippets.d.ts.map +1 -1
  161. package/lib/types/wizard/routes.d.ts +1 -0
  162. package/lib/types/wizard/routes.d.ts.map +1 -1
  163. package/lib/types/wizard/status.d.ts +9 -0
  164. package/lib/types/wizard/status.d.ts.map +1 -1
  165. package/lib/types/wizard/verify.d.ts +3 -0
  166. package/lib/types/wizard/verify.d.ts.map +1 -1
  167. package/lib/upload-limits.js +15 -0
  168. package/lib/upload-limits.js.map +1 -1
  169. package/lib/virtual-id.js +60 -0
  170. package/lib/virtual-id.js.map +1 -1
  171. package/lib/wizard/probe.js +46 -1
  172. package/lib/wizard/probe.js.map +1 -1
  173. package/lib/wizard/proxy-snippets.js +14 -1
  174. package/lib/wizard/proxy-snippets.js.map +1 -1
  175. package/lib/wizard/routes.js +23 -4
  176. package/lib/wizard/routes.js.map +1 -1
  177. package/lib/wizard/status.js +21 -0
  178. package/lib/wizard/status.js.map +1 -1
  179. package/lib/wizard/verify.js +31 -8
  180. package/lib/wizard/verify.js.map +1 -1
  181. package/package.json +8 -4
  182. package/pwa/icons/badge-96.png +0 -0
  183. package/pwa/icons/badge.svg +1 -0
  184. package/pwa/manifest.json +2 -1
  185. package/pwa/sw.js +3 -1
  186. package/scripts/check-debug-report.mjs +41 -1
  187. package/scripts/check-settings-form.mjs +14 -3
  188. package/scripts/check-settings-view.mjs +247 -0
  189. package/scripts/check-wizard-ui.mjs +37 -0
  190. package/src/adapters/dsh-better-sidebar/index.ts +7 -3
  191. package/src/admin-routes.ts +10 -2
  192. package/src/cli/args.ts +289 -0
  193. package/src/cli/bootstrap.ts +569 -0
  194. package/src/cli/command-setup.ts +540 -0
  195. package/src/cli/commands-client.ts +369 -0
  196. package/src/cli/commands-host.ts +1127 -0
  197. package/src/cli/commands-status.ts +296 -0
  198. package/src/cli/config-writer.ts +354 -0
  199. package/src/cli/context.ts +38 -0
  200. package/src/cli/i18n.ts +645 -0
  201. package/src/cli/ipc-client.ts +216 -0
  202. package/src/cli/ipc-server.ts +531 -0
  203. package/src/cli/main.ts +285 -0
  204. package/src/cli/qr-terminal.ts +49 -0
  205. package/src/client/RemoteStatusIcon.tsx +4 -1
  206. package/src/client/current-session-report.ts +28 -4
  207. package/src/client/effects/model-group-side.ts +19 -5
  208. package/src/client/effects/remote-session.ts +26 -6
  209. package/src/client/locales.ts +96 -6
  210. package/src/client/settings/SettingsSection.tsx +287 -92
  211. package/src/client/settings/settings-css.ts +18 -0
  212. package/src/client/styles/remote-session.css.ts +15 -1
  213. package/src/client/wizard/GatewayWizard.tsx +470 -109
  214. package/src/client/wizard/cards/ProxyCard.tsx +2 -1
  215. package/src/client/wizard/wizard-css.ts +23 -0
  216. package/src/client-data/model-group-side.ts +37 -10
  217. package/src/client-data/remote-status.ts +112 -17
  218. package/src/client-data/settings-form.ts +164 -8
  219. package/src/client-data/wizard.ts +112 -2
  220. package/src/client-routes.ts +334 -26
  221. package/src/config.ts +62 -16
  222. package/src/debug-report.ts +27 -1
  223. package/src/fetch-route-intercept.ts +381 -32
  224. package/src/index.ts +566 -367
  225. package/src/intercept.ts +1866 -286
  226. package/src/merge-streams.ts +155 -10
  227. package/src/plugin-route-intercept.ts +120 -71
  228. package/src/relay-access.ts +14 -3
  229. package/src/relay-client.ts +163 -5
  230. package/src/relay-pool.ts +387 -0
  231. package/src/relay-server.ts +312 -16
  232. package/src/restart-fields.ts +6 -4
  233. package/src/share-store.ts +59 -26
  234. package/src/state-store.ts +428 -0
  235. package/src/upload-limits.ts +16 -0
  236. package/src/virtual-id.ts +57 -0
  237. package/src/wizard/probe.ts +54 -2
  238. package/src/wizard/proxy-snippets.ts +17 -2
  239. package/src/wizard/routes.ts +24 -5
  240. package/src/wizard/status.ts +28 -0
  241. package/src/wizard/verify.ts +33 -11
  242. package/AGENTS.md +0 -185
  243. package/docs/adr/0001-plugin-bridge-forwards-plugin-routes.md +0 -15
  244. package/docs/adr/0002-relay-emulates-websocket.md +0 -8
package/lib/client.js CHANGED
@@ -1947,10 +1947,10 @@ exports.RESTART_FIELDS = void 0;
1947
1947
  * effect (T17): `role` chooses which halves load; `port` / `host` /
1948
1948
  * `targetPort` / `rateLimit` / `trustedProxies` become the gateway child's
1949
1949
  * runtime shape; `vapidSubject` / `lang` / `pushEvents` / `pushDebounceMs` /
1950
- * `pushSummary` / `pushTurnEnd` / `pushTool` are the push leg's startup
1951
- * values. Everything else (serverName, idleHours, autoShareNewSessions,
1952
- * serverUrl, deviceToken, and the interface-half knobs) is read live per use
1953
- * and needs no reload.
1950
+ * `pushSummary` / `pushTurnEnd` / `pushTool` / `pushEnabled` /
1951
+ * `pushSessionName` are the push leg's startup values. Everything else
1952
+ * (serverName, idleHours, autoShareNewSessions, serverUrl, deviceToken, and
1953
+ * the interface-half knobs) is read live per use and needs no reload.
1954
1954
  *
1955
1955
  * Single source shared by both halves: the host's restart watcher
1956
1956
  * (src/restart-watcher.ts) fingerprints exactly these fields, and the
@@ -1974,6 +1974,8 @@ exports.RESTART_FIELDS = [
1974
1974
  'pushSummary',
1975
1975
  'pushTurnEnd',
1976
1976
  'pushTool',
1977
+ 'pushEnabled',
1978
+ 'pushSessionName',
1977
1979
  ];
1978
1980
  };
1979
1981
  __modules["client-data/plugin-bridge.js"] = function (require, module, exports) {
@@ -2122,10 +2124,11 @@ __modules["client-data/settings-form.js"] = function (require, module, exports)
2122
2124
  * (devices / pairing) plus the T14 envelope (ok / gateway / config / viaGateway).
2123
2125
  */
2124
2126
  Object.defineProperty(exports, "__esModule", { value: true });
2125
- exports.ZenRemoteSettingsForm = exports.SETTINGS_FIELDS = exports.IDLE_HOURS_MAX = exports.SERVER_NAME_MAX = exports.RESTART_FIELDS = exports.PAIRING_CODE_LENGTH = exports.PAIRING_CODE_ALPHABET = exports.DEVICE_TOKEN_FIELD = exports.CLIENT_CONFIG_ROUTE = exports.CLIENT_DEBUG_REPORT_ROUTE = exports.CLIENT_RECONNECT_ROUTE = exports.CLIENT_STATUS_ROUTE = exports.CLIENT_CLAIM_ROUTE = exports.ADMIN_PLUGIN_BRIDGE_ROUTE = exports.ADMIN_PUSH_TEST_ROUTE = exports.ADMIN_ACTION_ROUTE = exports.ADMIN_PAIR_ROUTE = exports.ADMIN_STATUS_ROUTE = void 0;
2127
+ exports.ZenRemoteSettingsForm = exports.SETTINGS_FIELDS = exports.IDLE_HOURS_MAX = exports.SERVER_NAME_MAX = exports.RESTART_FIELDS = exports.PAIRING_CODE_LENGTH = exports.PAIRING_CODE_ALPHABET = exports.DEVICE_TOKEN_FIELD = exports.CLIENT_CONFIG_ROUTE = exports.MAX_PAIRED_SERVERS = exports.CLIENT_DEBUG_REPORT_ROUTE = exports.CLIENT_UNPAIR_ROUTE = exports.CLIENT_RECONNECT_ROUTE = exports.CLIENT_STATUS_ROUTE = exports.CLIENT_CLAIM_ROUTE = exports.ADMIN_PLUGIN_BRIDGE_ROUTE = exports.ADMIN_PUSH_TEST_ROUTE = exports.ADMIN_ACTION_ROUTE = exports.ADMIN_PAIR_ROUTE = exports.ADMIN_STATUS_ROUTE = void 0;
2126
2128
  exports.deriveClientStatusView = deriveClientStatusView;
2127
2129
  exports.clientStatusLineOf = clientStatusLineOf;
2128
2130
  exports.normalizePairingCode = normalizePairingCode;
2131
+ exports.canSubmitClientPairing = canSubmitClientPairing;
2129
2132
  exports.shouldFollowServerDeviceName = shouldFollowServerDeviceName;
2130
2133
  exports.shouldReloadAfterClientActivation = shouldReloadAfterClientActivation;
2131
2134
  exports.awaitClientStatusReady = awaitClientStatusReady;
@@ -2157,9 +2160,20 @@ exports.CLIENT_CLAIM_ROUTE = '/_dsh/zen-remote/client/claim';
2157
2160
  exports.CLIENT_STATUS_ROUTE = '/_dsh/zen-remote/client/status';
2158
2161
  /** T43: one immediate reconnect — answered 409 unless the client is offline. */
2159
2162
  exports.CLIENT_RECONNECT_ROUTE = '/_dsh/zen-remote/client/reconnect';
2163
+ /** MS-6: POST `{ entryId }` (or `{ all: true }`) removes one / all paired
2164
+ * servers from the pool (host half: src/client-routes.ts, mirrored here
2165
+ * because the browser half cannot import the host module). */
2166
+ exports.CLIENT_UNPAIR_ROUTE = '/_dsh/zen-remote/client/unpair';
2160
2167
  /** CU-4: the redacted diagnostic report (GET, both roles, machine-local only
2161
2168
  * — a gateway-forwarded fetch is refused server-side). */
2162
2169
  exports.CLIENT_DEBUG_REPORT_ROUTE = '/_dsh/zen-remote/client/debug-report';
2170
+ /**
2171
+ * MS-6: the paired-server pool's cap — the browser mirror of
2172
+ * src/state-store.ts's constant of the same name (the host module cannot be
2173
+ * imported here). The settings page and the wizard read it for the
2174
+ * `已配对 N / 8 台` counters and the limit gating.
2175
+ */
2176
+ exports.MAX_PAIRED_SERVERS = 8;
2163
2177
  /** The lightweight client-facing config route (host half, both roles): the
2164
2178
  * settings page's FALLBACK role probe (T17) — it registers wherever a
2165
2179
  * webServer exists, and since T17 its body carries the effective `role`
@@ -2177,6 +2191,40 @@ const WIRE_STATE_ALIASES = {
2177
2191
  online: 'connected',
2178
2192
  offline: 'unreachable',
2179
2193
  };
2194
+ /** The view `state` for one raw wire word: the vocabulary verbatim, the T55
2195
+ * aliases mapped, anything else the conservative `unpaired`. Shared by the
2196
+ * top level and every `servers[]` row. */
2197
+ function clientStateOf(raw) {
2198
+ const word = typeof raw === 'string' ? raw : '';
2199
+ return CLIENT_STATES.includes(word)
2200
+ ? word
2201
+ : WIRE_STATE_ALIASES[word] ?? 'unpaired';
2202
+ }
2203
+ /** The finite-number gate every epoch-ms wire field passes. */
2204
+ function finiteOrUndefined(value) {
2205
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
2206
+ }
2207
+ /** MS-6: one `servers[]` row into its view. Tolerant like the top level —
2208
+ * a junk field degrades, a row without the route's stable `entryId` key
2209
+ * drops whole. */
2210
+ function deriveClientServerView(value) {
2211
+ const record = asRecord(value);
2212
+ const entryId = asStringSet(record.entryId);
2213
+ if (entryId === '')
2214
+ return undefined;
2215
+ return {
2216
+ entryId,
2217
+ serverId: asStringSet(record.serverId),
2218
+ serverUrl: asStringSet(record.serverUrl),
2219
+ serverName: asStringSet(record.serverName),
2220
+ displayName: asStringSet(record.displayName),
2221
+ state: clientStateOf(record.state),
2222
+ nextRetryAt: finiteOrUndefined(record.nextRetryAt),
2223
+ lastError: asStringSet(record.lastError),
2224
+ compat: deriveCompatView(record.compat),
2225
+ pluginBridge: (0, plugin_bridge_ts_1.derivePluginBridgeStatuses)(record.pluginBridge),
2226
+ };
2227
+ }
2180
2228
  /** At most `cap` strings out of an array-shaped value; anything else is none. */
2181
2229
  function stringListOf(value, cap) {
2182
2230
  if (!Array.isArray(value))
@@ -2264,18 +2312,20 @@ function deriveClientStatusView(body) {
2264
2312
  // diagnostics below it showed live relay calls. `offline` rides the same
2265
2313
  // mapping to the probe word for it; anything else unknown still degrades
2266
2314
  // to `unpaired`.
2267
- const rawState = typeof safe.state === 'string' ? safe.state : '';
2268
- const state = CLIENT_STATES.includes(rawState)
2269
- ? rawState
2270
- : WIRE_STATE_ALIASES[rawState] ?? 'unpaired';
2271
- const nextRetryAt = typeof safe.nextRetryAt === 'number' && Number.isFinite(safe.nextRetryAt)
2272
- ? safe.nextRetryAt
2273
- : undefined;
2315
+ const state = clientStateOf(safe.state);
2316
+ // MS-6: the pool list rides as `servers[]` (one row per paired entry); a
2317
+ // legacy single-server body answers the empty list and the pre-MS-6 page
2318
+ // shape otherwise.
2319
+ const servers = (Array.isArray(safe.servers) ? safe.servers : [])
2320
+ .map(deriveClientServerView)
2321
+ .filter((row) => row !== undefined);
2322
+ const maxServersRaw = finiteOrUndefined(safe.maxServers);
2323
+ const maxServers = maxServersRaw !== undefined && maxServersRaw >= 1 ? maxServersRaw : exports.MAX_PAIRED_SERVERS;
2274
2324
  return {
2275
2325
  state,
2276
2326
  serverUrl: typeof safe.serverUrl === 'string' ? safe.serverUrl : '',
2277
2327
  serverName: typeof safe.serverName === 'string' ? safe.serverName : '',
2278
- nextRetryAt,
2328
+ nextRetryAt: finiteOrUndefined(safe.nextRetryAt),
2279
2329
  lastError: typeof safe.lastError === 'string' ? safe.lastError : '',
2280
2330
  deviceName: typeof safe.deviceName === 'string' ? safe.deviceName : '',
2281
2331
  intercept: deriveInterceptView(safe.intercept),
@@ -2285,6 +2335,9 @@ function deriveClientStatusView(body) {
2285
2335
  uploadProbe: safe.serviceProbe !== null && typeof safe.serviceProbe === 'object'
2286
2336
  ? Object.freeze({ ...safe.serviceProbe })
2287
2337
  : undefined,
2338
+ servers,
2339
+ maxServers,
2340
+ limitReached: servers.length >= maxServers,
2288
2341
  };
2289
2342
  }
2290
2343
  function clientStatusLineOf(view, now) {
@@ -2319,6 +2372,39 @@ function normalizePairingCode(input) {
2319
2372
  .replace(/[^ABCDEFGHJKMNPQRSTUVWXYZ23456789]/g, '')
2320
2373
  .slice(0, exports.PAIRING_CODE_LENGTH);
2321
2374
  }
2375
+ /**
2376
+ * The address comparison the overwrite judgment makes (MS-6): trim padding,
2377
+ * drop trailing slashes. Light on purpose — the backend re-normalizes the
2378
+ * address authoritatively on claim; here it only decides whether the typed
2379
+ * address NAMES an already-paired server, and `http://x.local:3088/` must
2380
+ * count as that server while `http://x.local:3089` must not.
2381
+ */
2382
+ function normalizedPairAddress(url) {
2383
+ return url.trim().replace(/\/+$/u, '');
2384
+ }
2385
+ /**
2386
+ * The 配对 button's whole gate, as one pure truth table (MS-6): the claim
2387
+ * needs an address and a COMPLETE 8-character pairing code (the input box
2388
+ * normalizes as the user types; this judge is the same bar the route's code
2389
+ * grammar enforces), a quiet page (`busy`), and a valid device-name draft
2390
+ * (`deviceNameInvalid`, T60 — a red-boxed name would silently pair under the
2391
+ * fallback copy). The pool cap blocks only NEW addresses: a full pool still
2392
+ * accepts re-pairing an existing one in place, which is how a user whose
2393
+ * token was revoked at 8/8 gets back in.
2394
+ */
2395
+ function canSubmitClientPairing(input) {
2396
+ const maxServers = input.maxServers ?? exports.MAX_PAIRED_SERVERS;
2397
+ const address = normalizedPairAddress(input.pairUrl);
2398
+ const isOverwrite = address !== ''
2399
+ && input.servers.some((server) => normalizedPairAddress(server.serverUrl) === address);
2400
+ const blockedByLimit = !isOverwrite && input.servers.length >= maxServers;
2401
+ const allowed = address !== ''
2402
+ && normalizePairingCode(input.pairCode).length === exports.PAIRING_CODE_LENGTH
2403
+ && !blockedByLimit
2404
+ && input.busy !== true
2405
+ && input.deviceNameInvalid !== true;
2406
+ return { allowed, isOverwrite, blockedByLimit };
2407
+ }
2322
2408
  /**
2323
2409
  * Whether the settings page may follow the SERVER's record of this device's
2324
2410
  * name into the row (T59): the body carried a name, it differs from the row
@@ -2580,6 +2666,8 @@ exports.SETTINGS_FIELDS = [
2580
2666
  textField('trustedProxies'),
2581
2667
  intField('rateLimit', 1, Number.MAX_SAFE_INTEGER),
2582
2668
  textField('vapidSubject'),
2669
+ boolField('pushEnabled'),
2670
+ boolField('pushSessionName'),
2583
2671
  boolField('pushSummary'),
2584
2672
  boolField('pushTurnEnd'),
2585
2673
  boolField('pushTool'),
@@ -3251,6 +3339,10 @@ exports.zh = {
3251
3339
  'settings.invalidPort': '需要 1–65535 的整数',
3252
3340
  'settings.invalidPositiveInt': '需要正整数',
3253
3341
  'settings.pushTitle': '推送',
3342
+ 'settings.fieldPushEnabled': '启用会话通知',
3343
+ 'settings.fieldPushEnabledHint': '总开关:关闭后停止所有自动推送,且不注册 push_notify 工具与系统提示词。',
3344
+ 'settings.fieldPushSessionName': '推送来源会话名',
3345
+ 'settings.fieldPushSessionNameHint': '在推送正文第一行显示「来源会话:<会话名>」。',
3254
3346
  'settings.fieldPushSummary': '推送内容摘要',
3255
3347
  'settings.fieldPushSummaryHint': '在推送正文中包含回合的最终文本或待回答的问题。',
3256
3348
  'settings.fieldPushTurnEnd': '回合结束推送',
@@ -3328,7 +3420,7 @@ exports.zh = {
3328
3420
  'settings.refreshFailed': '状态刷新失败,当前显示的是上一次成功的数据',
3329
3421
  'settings.resetPreview': '保存后恢复为下一层的值',
3330
3422
  // --- 插件设置页(T16),子客户端区块 ---
3331
- 'settings.client.connectTitle': '连接服务端',
3423
+ 'settings.client.connectTitle': '连接主服务端',
3332
3424
  'settings.client.serverUrl': '服务端地址',
3333
3425
  'settings.client.serverUrlHint': '服务端网关地址,例如 http://192.168.1.10:3088 或 https://dsh.example.com。',
3334
3426
  // The label above is the shared 设备名称 field in the role card (T57); this
@@ -3348,7 +3440,27 @@ exports.zh = {
3348
3440
  'settings.client.failUnreachable': '无法连接服务端',
3349
3441
  'settings.client.failRoleMismatch': '该配对码不适用于桌面应用端',
3350
3442
  'settings.client.failUnexpected': '服务端回应无法识别,请确认服务端已安装 dsh-zen-remote 2.0.0',
3351
- 'settings.client.failWrite': '配对成功,但本机保存令牌失败:设置存储不可用或只读',
3443
+ 'settings.client.failLimitReached': '已达主服务端数量上限:请先移除旧服务端,或输入已配对服务端的地址覆盖重配',
3444
+ // --- MS-7 多服务端:卡片头计数、8 台上限红字、已配对主服务端列表 ---
3445
+ 'settings.client.poolCount': '已配对 {count} / {max} 台',
3446
+ 'settings.client.poolCapNote': '最多可同时连接 {max} 台主服务端。',
3447
+ 'settings.client.limitReached': '已配满 {count} / {max} 台主服务端(已达上限)。如需添加新服务端,请先移除旧服务端;输入已配对服务端的地址仍可覆盖重配。',
3448
+ 'settings.client.serversTitle': '已配对主服务端',
3449
+ 'settings.client.serversEmpty': '尚未配对任何主服务端(最多可同时连接 {max} 台)',
3450
+ 'settings.client.repair': '重新配对',
3451
+ 'settings.client.serverRemove': '移除',
3452
+ 'settings.client.unpairServerConfirm': '将从本机移除该主服务端的配对(服务端上的数据与设备记录不受影响)。确定移除?',
3453
+ 'settings.client.unpairAllServers': '全部移除',
3454
+ 'settings.client.unpairAllServersConfirm': '将从本机移除全部已配对的主服务端(服务端上的数据与设备记录不受影响)。确定移除?',
3455
+ 'settings.client.serverStateConnected': '已连接',
3456
+ 'settings.client.serverStateRetry': '离线,{seconds} 秒后重试',
3457
+ 'settings.client.serverStateRetrySoon': '离线,即将重试',
3458
+ 'settings.client.serverStateOffline': '离线',
3459
+ 'settings.client.serverStateRevoked': '令牌已吊销',
3460
+ 'settings.client.serverStateIncompatible': '不兼容',
3461
+ 'settings.client.serverStateInvalidUrl': '地址无效',
3462
+ 'settings.client.serverStateUnpaired': '未连接',
3463
+ 'settings.client.serverDiagTitle': '各主服务端诊断',
3352
3464
  'settings.client.statusTitle': '连接状态',
3353
3465
  'settings.client.statusUnpaired': '未配对',
3354
3466
  'settings.client.statusConnected': '已连接到 {serverUrl}',
@@ -3527,6 +3639,7 @@ exports.zh = {
3527
3639
  'wizard.desktop.generate': '生成桌面客户端配对码',
3528
3640
  'wizard.desktop.regenerate': '重新生成配对码',
3529
3641
  'wizard.desktop.copyInfo': '复制地址与配对码',
3642
+ 'wizard.desktop.copiedText': '服务端地址:{address}\n桌面客户端配对码:{code}',
3530
3643
  'wizard.desktop.guide': '在另一台电脑打开本插件详情页,第 1 步选「作为桌面客户端」,第 2 步填入上方服务端地址与 8 位配对码即可完成连接。',
3531
3644
  'wizard.desktop.pairedTitle': '已连接的桌面客户端({count})',
3532
3645
  'wizard.desktop.pairedEmpty': '暂未配对桌面客户端',
@@ -3641,7 +3754,7 @@ exports.zh = {
3641
3754
  'wizard.overview.devicesSummary': '手机/网页 {web} 台 · 桌面客户端 {desktop} 台',
3642
3755
  'wizard.overview.devicesNone': '暂未配对设备',
3643
3756
  'wizard.overview.clientCredLabel': '本地配对凭证',
3644
- 'wizard.overview.clientConnLabel': '服务端连接',
3757
+ 'wizard.overview.clientConnLabel': '连接状态',
3645
3758
  'wizard.overview.clientServerLabel': '远端服务端地址',
3646
3759
  'wizard.overview.startSetup': '开始配置向导',
3647
3760
  'wizard.overview.reconfigure': '修改接入配置',
@@ -3663,6 +3776,26 @@ exports.zh = {
3663
3776
  'wizard.devicesModal.title': '已配对设备管理',
3664
3777
  'wizard.devicesModal.subtitle': '查看与管理已配对的手机/网页设备及桌面客户端,支持改名、切换布局或吊销凭证',
3665
3778
  'wizard.devicesModal.retest': '重新测试连通性',
3779
+ 'wizard.overview.clientSyncLabel': '会话与插件同步',
3780
+ 'wizard.overview.syncOk': '正常',
3781
+ 'wizard.overview.syncBad': '异常',
3782
+ 'wizard.overview.syncPending': '未连接',
3783
+ 'wizard.overview.clientOnlineCount': '在线 {online} / {total} 台',
3784
+ 'wizard.overview.clientOfflineCount': '离线 {count} 台',
3785
+ 'wizard.overview.clientRevokedCount': '已吊销 {count} 台',
3786
+ 'wizard.overview.clientAllOnline': '全部在线',
3787
+ 'wizard.overview.clientServersLabel': '已配对服务端',
3788
+ 'wizard.overview.clientPairedFull': '已配满 {count} / {max} 台(已达上限)',
3789
+ 'wizard.overview.clientPairedNames': '{names}(上限 {max} 台)',
3790
+ 'wizard.overview.limitNote': '已达 {max} 台上限,添加前请先移除旧服务端',
3791
+ 'wizard.overview.manageServers': '管理服务端',
3792
+ 'wizard.overview.addServer': '+ 添加服务端',
3793
+ 'wizard.serversModal.title': '已配对主服务端管理',
3794
+ 'wizard.serversModal.subtitle': '最多同时连接 {max} 台主服务端(当前 {count} / {max} 台)',
3795
+ 'wizard.serversModal.viewStatus': '查看状态',
3796
+ 'wizard.client.poolCapNote': '最多同时连接 {max} 台主服务端(当前已配对 {count} / {max} 台)',
3797
+ 'wizard.client.continueAdd': '+ 继续添加另一台',
3798
+ 'wizard.client.serverPicker': '选择要查看的服务端',
3666
3799
  };
3667
3800
  /** English dictionary, key-identical to the Chinese source of truth. */
3668
3801
  exports.en = {
@@ -3774,6 +3907,10 @@ exports.en = {
3774
3907
  'settings.invalidPort': 'Enter an integer from 1 to 65535',
3775
3908
  'settings.invalidPositiveInt': 'Enter a positive integer',
3776
3909
  'settings.pushTitle': 'Push',
3910
+ 'settings.fieldPushEnabled': 'Enable session notifications',
3911
+ 'settings.fieldPushEnabledHint': 'Master switch: when off, stops all automatic pushes and does not register the push_notify tool or system prompt.',
3912
+ 'settings.fieldPushSessionName': 'Include session name in push',
3913
+ 'settings.fieldPushSessionNameHint': 'Show "Session: <name>" on the first line of the push notification body.',
3777
3914
  'settings.fieldPushSummary': 'Push body summary',
3778
3915
  'settings.fieldPushSummaryHint': 'Include the turn\u2019s final text or pending question in push bodies.',
3779
3916
  'settings.fieldPushTurnEnd': 'Turn-end push',
@@ -3850,7 +3987,7 @@ exports.en = {
3850
3987
  'settings.statusUnknown': 'Could not read the plugin status',
3851
3988
  'settings.refreshFailed': 'The status refresh failed — showing the last successful data',
3852
3989
  'settings.resetPreview': 'Saving reverts to the next layer\u2019s value',
3853
- 'settings.client.connectTitle': 'Connect to the server',
3990
+ 'settings.client.connectTitle': 'Connect to main servers',
3854
3991
  'settings.client.serverUrl': 'Server address',
3855
3992
  'settings.client.serverUrlHint': 'The server\u2019s gateway address, e.g. http://192.168.1.10:3088 or https://dsh.example.com.',
3856
3993
  // The label above is the shared 设备名称 field in the role card (T57); this
@@ -3870,7 +4007,27 @@ exports.en = {
3870
4007
  'settings.client.failUnreachable': 'Could not reach the server',
3871
4008
  'settings.client.failRoleMismatch': 'This pairing code is not valid for desktop clients',
3872
4009
  'settings.client.failUnexpected': 'The server gave an unrecognized answer — make sure it runs dsh-zen-remote 2.0.0',
3873
- 'settings.client.failWrite': 'Pairing succeeded, but saving the token locally failed: the settings store is unavailable or read-only',
4010
+ 'settings.client.failLimitReached': 'The main-server limit is reached: remove an old server first, or enter an already-paired address to re-pair it in place',
4011
+ // --- MS-7 multi-server: card-head count, the 8-server red alert, the paired list ---
4012
+ 'settings.client.poolCount': '{count} / {max} paired',
4013
+ 'settings.client.poolCapNote': 'Up to {max} main servers can be connected at the same time.',
4014
+ 'settings.client.limitReached': 'All {count} / {max} main servers are paired (the limit is reached). To add a new server, remove an old one first; entering an already-paired address still re-pairs it in place.',
4015
+ 'settings.client.serversTitle': 'Paired main servers',
4016
+ 'settings.client.serversEmpty': 'No main servers paired yet (up to {max} at the same time)',
4017
+ 'settings.client.repair': 'Re-pair',
4018
+ 'settings.client.serverRemove': 'Remove',
4019
+ 'settings.client.unpairServerConfirm': 'This machine will forget this main server\u2019s pairing (data and the device record on the server are untouched). Remove it?',
4020
+ 'settings.client.unpairAllServers': 'Remove all',
4021
+ 'settings.client.unpairAllServersConfirm': 'This machine will forget every paired main server (data and device records on the servers are untouched). Remove all?',
4022
+ 'settings.client.serverStateConnected': 'Connected',
4023
+ 'settings.client.serverStateRetry': 'Offline, retrying in {seconds} s',
4024
+ 'settings.client.serverStateRetrySoon': 'Offline, retrying now',
4025
+ 'settings.client.serverStateOffline': 'Offline',
4026
+ 'settings.client.serverStateRevoked': 'Token revoked',
4027
+ 'settings.client.serverStateIncompatible': 'Incompatible',
4028
+ 'settings.client.serverStateInvalidUrl': 'Invalid address',
4029
+ 'settings.client.serverStateUnpaired': 'Not connected',
4030
+ 'settings.client.serverDiagTitle': 'Per-server diagnostics',
3874
4031
  'settings.client.statusTitle': 'Connection',
3875
4032
  'settings.client.statusUnpaired': 'Not paired',
3876
4033
  'settings.client.statusConnected': 'Connected to {serverUrl}',
@@ -4047,6 +4204,7 @@ exports.en = {
4047
4204
  'wizard.desktop.generate': 'Generate Desktop Pairing Code',
4048
4205
  'wizard.desktop.regenerate': 'Regenerate Code',
4049
4206
  'wizard.desktop.copyInfo': 'Copy Address & Code',
4207
+ 'wizard.desktop.copiedText': 'Server address: {address}\nDesktop client pairing code: {code}',
4050
4208
  'wizard.desktop.guide': 'On the other computer, open this plugin page, choose "Desktop Client" in Step 1, and enter the server address and 8-character code above in Step 2.',
4051
4209
  'wizard.desktop.pairedTitle': 'Paired Desktop Clients ({count})',
4052
4210
  'wizard.desktop.pairedEmpty': 'No desktop clients paired yet',
@@ -4161,7 +4319,7 @@ exports.en = {
4161
4319
  'wizard.overview.devicesSummary': 'Phone/Web {web} · Desktop {desktop}',
4162
4320
  'wizard.overview.devicesNone': 'No paired devices yet',
4163
4321
  'wizard.overview.clientCredLabel': 'Local Credential',
4164
- 'wizard.overview.clientConnLabel': 'Server Connection',
4322
+ 'wizard.overview.clientConnLabel': 'Connection',
4165
4323
  'wizard.overview.clientServerLabel': 'Remote Server URL',
4166
4324
  'wizard.overview.startSetup': 'Start Setup Wizard',
4167
4325
  'wizard.overview.reconfigure': 'Reconfigure Wizard',
@@ -4183,6 +4341,26 @@ exports.en = {
4183
4341
  'wizard.devicesModal.title': 'Paired Devices',
4184
4342
  'wizard.devicesModal.subtitle': 'View and manage paired phone/web and desktop client devices — rename, switch layout, or revoke access',
4185
4343
  'wizard.devicesModal.retest': 'Retest Connectivity',
4344
+ 'wizard.overview.clientSyncLabel': 'Session & Plugin Sync',
4345
+ 'wizard.overview.syncOk': 'Normal',
4346
+ 'wizard.overview.syncBad': 'Abnormal',
4347
+ 'wizard.overview.syncPending': 'Not connected',
4348
+ 'wizard.overview.clientOnlineCount': '{online} / {total} online',
4349
+ 'wizard.overview.clientOfflineCount': '{count} offline',
4350
+ 'wizard.overview.clientRevokedCount': '{count} revoked',
4351
+ 'wizard.overview.clientAllOnline': 'All online',
4352
+ 'wizard.overview.clientServersLabel': 'Paired Servers',
4353
+ 'wizard.overview.clientPairedFull': 'All {count} / {max} paired (limit reached)',
4354
+ 'wizard.overview.clientPairedNames': '{names} (up to {max})',
4355
+ 'wizard.overview.limitNote': 'The {max}-server limit is reached — remove an old server before adding another',
4356
+ 'wizard.overview.manageServers': 'Manage Servers',
4357
+ 'wizard.overview.addServer': '+ Add Server',
4358
+ 'wizard.serversModal.title': 'Paired Main Servers',
4359
+ 'wizard.serversModal.subtitle': 'Up to {max} main servers at the same time (currently {count} / {max})',
4360
+ 'wizard.serversModal.viewStatus': 'View Status',
4361
+ 'wizard.client.poolCapNote': 'Up to {max} main servers at the same time ({count} / {max} currently paired)',
4362
+ 'wizard.client.continueAdd': '+ Add Another Server',
4363
+ 'wizard.client.serverPicker': 'Choose the server to view',
4186
4364
  };
4187
4365
  };
4188
4366
  __modules["client-data/shares.js"] = function (require, module, exports) {
@@ -16738,6 +16916,8 @@ __modules["virtual-id.js"] = function (require, module, exports) {
16738
16916
  */
16739
16917
  Object.defineProperty(exports, "__esModule", { value: true });
16740
16918
  exports.VIRTUAL_GROUP_NAME_MARK = exports.VIRTUAL_ID_PREFIX = void 0;
16919
+ exports.encodeServerIdZeroWidth = encodeServerIdZeroWidth;
16920
+ exports.decodeServerIdZeroWidth = decodeServerIdZeroWidth;
16741
16921
  exports.toVirtual = toVirtual;
16742
16922
  exports.fromVirtual = fromVirtual;
16743
16923
  exports.isVirtual = isVirtual;
@@ -16757,9 +16937,69 @@ exports.VIRTUAL_ID_PREFIX = 'zr~';
16757
16937
  * text.
16758
16938
  */
16759
16939
  exports.VIRTUAL_GROUP_NAME_MARK = '\u2060';
16940
+ /** MS-4: the two zero-width code points the serverId rides on inside a
16941
+ * virtual group NAME — ZERO WIDTH SPACE encodes bit 0, ZERO WIDTH
16942
+ * NON-JOINER bit 1. Invisible everywhere, and neither can appear in a
16943
+ * server id's own visible text, so the window between the two marks is
16944
+ * unambiguous. */
16945
+ const ZERO_BIT = '\u200b';
16946
+ const ONE_BIT = '\u200c';
16947
+ const SERVER_ID_BITS = 32;
16760
16948
  /** The server id half: exactly the 8 lowercase hex chars relay-server.ts
16761
16949
  * mints (`randomBytes(4).toString('hex')`). */
16762
16950
  const SERVER_ID_PATTERN = /^[0-9a-f]{8}$/;
16951
+ /**
16952
+ * MS-4: the invisible prefix a REMOTE model-menu group heading carries
16953
+ * before its server name (merge-streams.ts mergeModelCatalogs). The full
16954
+ * form is `${MARK}${32 zero-width bits}${MARK}` — the 32 bits of the
16955
+ * 8-hex serverId (MSB first per nibble), ZERO_BIT for 0 and ONE_BIT for 1
16956
+ * — so the browser half can tell WHICH server a remote model group came
16957
+ * from (effects/model-group-side.ts classifies per-server hiding from
16958
+ * heading text alone). A serverId that is not 8 lowercase hex (the format
16959
+ * every real handshake carries) degrades to the bare legacy mark: the
16960
+ * heading stays a marked remote heading, just without a decodable server.
16961
+ */
16962
+ function encodeServerIdZeroWidth(serverId) {
16963
+ if (typeof serverId !== 'string' || !SERVER_ID_PATTERN.test(serverId))
16964
+ return exports.VIRTUAL_GROUP_NAME_MARK;
16965
+ let bits = '';
16966
+ for (const nibble of serverId) {
16967
+ for (const bit of Number.parseInt(nibble, 16).toString(2).padStart(4, '0')) {
16968
+ bits += bit === '0' ? ZERO_BIT : ONE_BIT;
16969
+ }
16970
+ }
16971
+ return `${exports.VIRTUAL_GROUP_NAME_MARK}${bits}${exports.VIRTUAL_GROUP_NAME_MARK}`;
16972
+ }
16973
+ /**
16974
+ * MS-4: read the server id back out of one group heading, or `undefined`.
16975
+ * Strict like every decoder here: the text must start with the mark, the
16976
+ * window right after it must be exactly 32 zero-width bits closed by the
16977
+ * terminator mark — anything else (the legacy bare-mark heading, visible
16978
+ * text, a short or long bit run, a missing terminator) answers
16979
+ * `undefined`, which the classifier reads as "remote, server unknown".
16980
+ */
16981
+ function decodeServerIdZeroWidth(headingText) {
16982
+ if (typeof headingText !== 'string' || !headingText.startsWith(exports.VIRTUAL_GROUP_NAME_MARK))
16983
+ return undefined;
16984
+ const rest = headingText.slice(exports.VIRTUAL_GROUP_NAME_MARK.length);
16985
+ if (rest.length < SERVER_ID_BITS + 1 || rest[SERVER_ID_BITS] !== exports.VIRTUAL_GROUP_NAME_MARK)
16986
+ return undefined;
16987
+ let hex = '';
16988
+ for (let i = 0; i < SERVER_ID_BITS; i += 4) {
16989
+ let nibble = 0;
16990
+ for (let bit = 0; bit < 4; bit++) {
16991
+ const bitChar = rest[i + bit];
16992
+ if (bitChar === ZERO_BIT)
16993
+ nibble = nibble << 1;
16994
+ else if (bitChar === ONE_BIT)
16995
+ nibble = (nibble << 1) | 1;
16996
+ else
16997
+ return undefined;
16998
+ }
16999
+ hex += nibble.toString(16);
17000
+ }
17001
+ return hex;
17002
+ }
16763
17003
  /** Build the virtual form of one server-side session or workspace id. */
16764
17004
  function toVirtual(serverId, id) {
16765
17005
  return `${exports.VIRTUAL_ID_PREFIX}${serverId}~${id}`;
@@ -17526,9 +17766,38 @@ function pairFailText(fail, t) {
17526
17766
  }
17527
17767
  case 'unreachable': return t('settings.client.failUnreachable');
17528
17768
  case 'role-mismatch': return t('settings.client.failRoleMismatch');
17769
+ case 'limit-reached': return t('settings.client.failLimitReached');
17529
17770
  default: return t('settings.client.failUnexpected');
17530
17771
  }
17531
17772
  }
17773
+ /** Whether one paired server has anything for the per-server diagnostics
17774
+ * group (MS-7): a last error, a compat block, or bridge verdicts. */
17775
+ function serverHasDiag(server) {
17776
+ return server.lastError !== '' || server.compat !== undefined || server.pluginBridge.length > 0;
17777
+ }
17778
+ /** One paired server's state badge (MS-7): copy plus the tone the badge
17779
+ * wears — `live` green for connected, `warn` for an offline row (with the
17780
+ * retry countdown when one is armed), `invalid` red for a verdict only a
17781
+ * re-pair or an upgrade fixes. */
17782
+ function serverStateBadge(server, now, t) {
17783
+ switch (server.state) {
17784
+ case 'connected': return { text: t('settings.client.serverStateConnected'), tone: 'live' };
17785
+ case 'unreachable':
17786
+ case 'unexpected': {
17787
+ if (server.nextRetryAt === undefined)
17788
+ return { text: t('settings.client.serverStateOffline'), tone: 'warn' };
17789
+ const seconds = Math.max(0, Math.ceil((server.nextRetryAt - now) / 1000));
17790
+ return {
17791
+ text: seconds > 0 ? t('settings.client.serverStateRetry', { seconds }) : t('settings.client.serverStateRetrySoon'),
17792
+ tone: 'warn',
17793
+ };
17794
+ }
17795
+ case 'revoked': return { text: t('settings.client.serverStateRevoked'), tone: 'invalid' };
17796
+ case 'incompatible': return { text: t('settings.client.serverStateIncompatible'), tone: 'invalid' };
17797
+ case 'invalid-url': return { text: t('settings.client.serverStateInvalidUrl'), tone: 'invalid' };
17798
+ default: return { text: t('settings.client.serverStateUnpaired'), tone: undefined };
17799
+ }
17800
+ }
17532
17801
  function SettingsSectionPage({ config, shares, t }) {
17533
17802
  const form = (0, react_1.useSyncExternalStore)((0, react_1.useCallback)((cb) => config.subscribe(cb), [config]), () => config.getSnapshot());
17534
17803
  // Latest-wins gates (T15-fix 4): every status request takes a ticket and
@@ -17561,11 +17830,16 @@ function SettingsSectionPage({ config, shares, t }) {
17561
17830
  const [pairCode, setPairCode] = (0, react_1.useState)('');
17562
17831
  const [claimBusy, setClaimBusy] = (0, react_1.useState)(false);
17563
17832
  const [claimFail, setClaimFail] = (0, react_1.useState)(null);
17564
- const [claimWriteFailed, setClaimWriteFailed] = (0, react_1.useState)(false);
17565
17833
  const [unpairBusy, setUnpairBusy] = (0, react_1.useState)(false);
17566
17834
  const [unpairDone, setUnpairDone] = (0, react_1.useState)(false);
17567
17835
  // T43: the 立即重连 button's in-flight marker.
17568
17836
  const [reconnectBusy, setReconnectBusy] = (0, react_1.useState)(false);
17837
+ // MS-7: the pool list's in-flight row action — the entry id, or 'all' for
17838
+ // the remove-all — and whether the last one failed.
17839
+ const [entryBusy, setEntryBusy] = (0, react_1.useState)(null);
17840
+ const [entryActionFailed, setEntryActionFailed] = (0, react_1.useState)(false);
17841
+ // MS-7: the 重新配对 action focuses the code box.
17842
+ const pairCodeRef = (0, react_1.useRef)(null);
17569
17843
  const loadStatus = (0, react_1.useCallback)(() => {
17570
17844
  const ticket = statusGate.next();
17571
17845
  fetch(settings_form_ts_1.ADMIN_STATUS_ROUTE)
@@ -17599,26 +17873,9 @@ function SettingsSectionPage({ config, shares, t }) {
17599
17873
  : { state: 'error', status: error instanceof StatusError ? error.status : undefined });
17600
17874
  });
17601
17875
  }, [statusGate]);
17602
- // T59: the SERVER's record of this device's name wins over the row —
17603
- // unless the user is mid-edit (a staged draft in the field). One direct
17604
- // write lands it in the row's serverName; that write's own volatile
17605
- // update reaches the backend with both sides now EQUAL, so the push path
17606
- // stays silent and the two ends cannot bounce the name back and forth.
17607
- // T59-fix: the decision runs HERE, on each FRESH answer, judging the row
17608
- // and the draft AS THEY ARE at that moment — never off a stale
17609
- // `clientView` racing a row change (an effect keyed on the row value
17610
- // would fire on every save with the PREVIOUS poll's name and write the
17611
- // old one straight back over the user's fresh save).
17612
- const followServerDeviceName = (0, react_1.useCallback)((view) => {
17613
- if (!config.savedRoleIsClient())
17614
- return;
17615
- if (view.state !== 'connected')
17616
- return;
17617
- const rowName = typeof config.rowValue('serverName') === 'string' ? config.rowValue('serverName') : '';
17618
- if (!(0, settings_form_ts_1.shouldFollowServerDeviceName)(view.deviceName, rowName, config.hasDraft('serverName')))
17619
- return;
17620
- void config.writeDeviceName(view.deviceName);
17621
- }, [config]);
17876
+ // MS-7: the device name flows ONE way — a saved row serverName is
17877
+ // broadcast to every paired server by the backend pool; a server-side
17878
+ // record of this device's name never writes back into the row.
17622
17879
  const loadClientStatus = (0, react_1.useCallback)(() => {
17623
17880
  const ticket = clientGate.next();
17624
17881
  fetch(settings_form_ts_1.CLIENT_STATUS_ROUTE)
@@ -17630,9 +17887,7 @@ function SettingsSectionPage({ config, shares, t }) {
17630
17887
  .then((body) => {
17631
17888
  if (!clientGate.isLatest(ticket))
17632
17889
  return;
17633
- const view = (0, settings_form_ts_1.deriveClientStatusView)(body);
17634
- setClientLoad({ state: 'ready', view });
17635
- followServerDeviceName(view);
17890
+ setClientLoad({ state: 'ready', view: (0, settings_form_ts_1.deriveClientStatusView)(body) });
17636
17891
  })
17637
17892
  .catch(() => {
17638
17893
  if (!clientGate.isLatest(ticket))
@@ -17641,17 +17896,36 @@ function SettingsSectionPage({ config, shares, t }) {
17641
17896
  ? { state: 'stale', view: prev.view }
17642
17897
  : { state: 'error' });
17643
17898
  });
17644
- }, [clientGate, followServerDeviceName]);
17899
+ }, [clientGate]);
17645
17900
  // The client group's rendered view, hoisted for the countdown ticker and
17646
17901
  // the retry-follow refresh below (T43).
17647
17902
  const clientView = clientLoad.state === 'ready' || clientLoad.state === 'stale' ? clientLoad.view : undefined;
17903
+ // MS-7: the paired-server pool, in pairing order (empty on a legacy body),
17904
+ // and the cap the counters and the red alert read.
17905
+ const serverRows = clientView?.servers ?? [];
17906
+ const maxServers = clientView?.maxServers ?? settings_form_ts_1.MAX_PAIRED_SERVERS;
17648
17907
  // RD-3: the remote drop guard's browser-side counters, read at render
17649
17908
  // time — no snapshot state of its own: the initial mount, the 15s
17650
17909
  // connected-status poll and the countdown ticker all re-render this page,
17651
17910
  // and each render takes the values as they are right then.
17652
17911
  const dropDiag = (0, remote_drop_ts_2.remoteDropDiagnostics)();
17653
- const clientRetryAt = clientView?.nextRetryAt;
17912
+ // MS-7: the SOONEST armed retry across the top level and every pool row —
17913
+ // each offline row counts down in its own badge, and the follow refresh
17914
+ // below fires after whichever retry comes due first.
17915
+ const clientRetryAt = [clientView?.nextRetryAt, ...serverRows.map((server) => server.nextRetryAt)]
17916
+ .reduce((soonest, at) => (at !== undefined && (soonest === undefined || at < soonest) ? at : soonest), undefined);
17654
17917
  const clientCountingDown = clientRetryAt !== undefined;
17918
+ // MS-7 diagnostics gates: the per-server group, the pre-pool fallback to
17919
+ // the top-level mirrors, and whether the section has any line at all.
17920
+ const legacyTopDiag = clientView !== undefined && serverRows.length === 0;
17921
+ const hasServerDiag = serverRows.some(serverHasDiag);
17922
+ const diagVisible = clientView !== undefined && (clientView.intercept !== undefined
17923
+ || clientView.pluginRouteIntercept !== undefined
17924
+ || clientView.uploadProbe !== undefined
17925
+ || dropDiag.installed
17926
+ || dropDiag.notInstalledReason !== undefined
17927
+ || hasServerDiag
17928
+ || (legacyTopDiag && (clientView.lastError !== '' || clientView.compat !== undefined || (clientView.pluginBridge?.length ?? 0) > 0)));
17655
17929
  // Offline with a pending retry: refresh the status shortly after the
17656
17930
  // retry comes due, so a server that came back flips the line to connected
17657
17931
  // without a manual refresh. A still-offline answer carries the NEXT
@@ -17957,23 +18231,29 @@ function SettingsSectionPage({ config, shares, t }) {
17957
18231
  setPairUrl(rowServerUrl);
17958
18232
  }, [pairUrlTouched, pairUrl, rowServerUrl]);
17959
18233
  const codeNormalized = (0, settings_form_ts_1.normalizePairingCode)(pairCode);
17960
- // T60: an INVALID device-name draft (red box — blank-but-nonempty or over
17961
- // the cap) blocks the pairing: the claim would silently substitute the
17962
- // default copy and the server would register a name the user never asked
17963
- // for. A VALID draft pairs fine — it lands in the row with the write.
17964
- const claimAllowed = form.available && form.writable && !claimBusy && codeNormalized.length === 8 && !form.serverName.invalid;
18234
+ // MS-7: the whole gate is the MS-6 pure truth table — an address and a
18235
+ // complete code, a quiet page, a valid device-name draft (T60: a red-boxed
18236
+ // name would silently pair under the fallback copy), and the 8-server cap,
18237
+ // which blocks only a NEW address: re-entering an already-paired one
18238
+ // re-pairs it in place, at 8/8 too.
18239
+ const claimVerdict = (0, settings_form_ts_1.canSubmitClientPairing)({
18240
+ pairUrl,
18241
+ pairCode: codeNormalized,
18242
+ servers: serverRows,
18243
+ maxServers,
18244
+ busy: claimBusy,
18245
+ deviceNameInvalid: form.serverName.invalid,
18246
+ });
18247
+ const claimAllowed = form.available && form.writable && claimVerdict.allowed;
17965
18248
  const runClaim = async () => {
17966
18249
  setClaimBusy(true);
17967
18250
  setClaimFail(null);
17968
- setClaimWriteFailed(false);
17969
18251
  // UX-1: captured BEFORE the pairing — whether the page ran as client, and
17970
18252
  // whether the backend's stream interceptor was already active per the
17971
18253
  // last client/status diagnostics.
17972
18254
  const wasEffectiveClient = clientRole;
17973
18255
  const streamInterceptAlreadyActive = clientView?.intercept?.installed === true;
17974
- // T60: one name for both halves of the round-trip — trimmed and legal
17975
- // ({@link claimDeviceNameOf}), sent to the server AND written into the
17976
- // row when the claim lands.
18256
+ // T60: the name registers trimmed and legal ({@link claimDeviceNameOf}).
17977
18257
  const claimName = (0, settings_form_ts_1.claimDeviceNameOf)(form.serverName.text, t('settings.client.deviceNameDefault'));
17978
18258
  try {
17979
18259
  const res = await fetch(settings_form_ts_1.CLIENT_CLAIM_ROUTE, {
@@ -17981,8 +18261,7 @@ function SettingsSectionPage({ config, shares, t }) {
17981
18261
  headers: { 'Content-Type': 'application/json' },
17982
18262
  // T57: the name registers under the shared 设备名称 field's current
17983
18263
  // displayed value (a staged draft counts), falling back to the
17984
- // default copy when the field is empty — the client page reads no
17985
- // admin baseline (T55), so an unstored name displays empty here.
18264
+ // default copy when the field is empty.
17986
18265
  body: JSON.stringify({
17987
18266
  serverUrl: pairUrl.trim(),
17988
18267
  code: codeNormalized,
@@ -17990,51 +18269,32 @@ function SettingsSectionPage({ config, shares, t }) {
17990
18269
  }),
17991
18270
  });
17992
18271
  const body = await res.json().catch(() => ({}));
17993
- if (res.ok && body.ok === true && typeof body.token === 'string' && body.token !== '') {
17994
- // The backend echoes the address it validated; write the credentials
17995
- // AND the registered name through ONE mutate, then clear the code
17996
- // and re-read the connection state. The name is the SERVER's
17997
- // confirmed echo when the claim carried one (the gateway may have
17998
- // trimmed or uniquified it at registration), else the name we
17999
- // actually sent (T60) — either way the row now matches the server.
18000
- const urlToWrite = typeof body.serverUrl === 'string' && body.serverUrl !== '' ? body.serverUrl : pairUrl.trim();
18001
- // T59-fix: the echo is judged by the SAME rules as our own draft
18002
- // (claimDeviceNameOf trims and validates) — a server answer we
18003
- // could not store falls back to the name we actually sent.
18004
- const echoed = typeof body.deviceName === 'string' && body.deviceName !== '' ? (0, settings_form_ts_1.claimDeviceNameOf)(body.deviceName, claimName) : claimName;
18005
- const landed = await config.writeClientPairing(urlToWrite, body.token, echoed);
18006
- if (landed && (0, settings_form_ts_1.shouldReloadAfterClientActivation)({
18272
+ if (res.ok && body.ok === true) {
18273
+ // MS-5/MS-7: the backend landed the pairing straight into
18274
+ // zen-remote-state.json (the token never reaches the browser), so
18275
+ // there is nothing to write here — only the FIRST activation still
18276
+ // reloads, so the page's resident global streams reopen wrapped.
18277
+ if ((0, settings_form_ts_1.shouldReloadAfterClientActivation)({
18007
18278
  wasEffectiveClient,
18008
18279
  nowClientRole: true,
18009
18280
  savedOrPaired: true,
18010
18281
  streamInterceptAlreadyActive,
18011
18282
  })) {
18012
- // UX-1: FIRST pairing on a page whose resident global streams were
18013
- // opened unwrapped (or with the interceptor inactive) — wait out the
18014
- // backend's row-reload window, then reload so the streams reopen
18015
- // wrapped and the server's sessions enter the sidebar.
18016
18283
  setClaimBusy(false);
18017
18284
  void (0, settings_form_ts_1.awaitClientStatusReady)().then(() => window.location.reload());
18018
18285
  return;
18019
18286
  }
18020
- if (landed) {
18021
- // The draft did its job — it now equals the displayed value, and
18022
- // keeping it would be a phantom edit (T60).
18023
- config.discardField('serverName');
18024
- setPairCode('');
18025
- setPairUrl(urlToWrite);
18026
- setUnpairDone(false);
18027
- loadClientStatus();
18028
- }
18029
- else {
18030
- setClaimWriteFailed(true);
18031
- }
18287
+ setPairCode('');
18288
+ if (typeof body.serverUrl === 'string' && body.serverUrl !== '')
18289
+ setPairUrl(body.serverUrl);
18290
+ setUnpairDone(false);
18291
+ loadClientStatus();
18032
18292
  }
18033
18293
  else {
18034
- // Every other shape is a failure (T16-fix): a non-OK status, a body
18035
- // whose `ok` is not true, a missing token — shown with the classified
18036
- // code when the route sent one. The body's `message` is ignored: the
18037
- // wording is this build's dictionary (pairFailText).
18294
+ // Every other shape is a failure (T16-fix), shown with the classified
18295
+ // code when the route sent one (MS-5 adds `limit-reached`). The
18296
+ // body's `message` is ignored: the wording is this build's
18297
+ // dictionary (pairFailText).
18038
18298
  const fail = { code: typeof body.code === 'string' && body.code !== '' ? body.code : 'unexpected' };
18039
18299
  if (typeof body.retryAfterMs === 'number')
18040
18300
  fail.retryAfterMs = body.retryAfterMs;
@@ -18057,6 +18317,70 @@ function SettingsSectionPage({ config, shares, t }) {
18057
18317
  loadClientStatus();
18058
18318
  }
18059
18319
  };
18320
+ /** MS-7: one POST of the pool list's row actions — whether it answered ok. */
18321
+ const postClientRoute = async (route, payload) => {
18322
+ try {
18323
+ const res = await fetch(route, {
18324
+ method: 'POST',
18325
+ headers: { 'Content-Type': 'application/json' },
18326
+ body: payload,
18327
+ });
18328
+ return res.ok;
18329
+ }
18330
+ catch {
18331
+ return false;
18332
+ }
18333
+ };
18334
+ // MS-7 立即重连 for ONE server: refresh now (the state is at least
18335
+ // "connecting") and once more after the attempt can have settled — the
18336
+ // T43 top-level button's cadence, per entry.
18337
+ const runEntryReconnect = async (entryId) => {
18338
+ setEntryBusy(entryId);
18339
+ setEntryActionFailed(false);
18340
+ if (await postClientRoute(settings_form_ts_1.CLIENT_RECONNECT_ROUTE, JSON.stringify({ entryId }))) {
18341
+ loadClientStatus();
18342
+ refreshTimers.current.push(window.setTimeout(() => { loadClientStatus(); }, 1500));
18343
+ }
18344
+ else {
18345
+ setEntryActionFailed(true);
18346
+ }
18347
+ setEntryBusy(null);
18348
+ };
18349
+ // MS-7 移除: forget one paired server (the server's own device record is
18350
+ // untouched — the confirm copy says so).
18351
+ const runUnpairEntry = async (entryId) => {
18352
+ if (!window.confirm(t('settings.client.unpairServerConfirm')))
18353
+ return;
18354
+ setEntryBusy(entryId);
18355
+ setEntryActionFailed(false);
18356
+ if (await postClientRoute(settings_form_ts_1.CLIENT_UNPAIR_ROUTE, JSON.stringify({ entryId })))
18357
+ loadClientStatus();
18358
+ else
18359
+ setEntryActionFailed(true);
18360
+ setEntryBusy(null);
18361
+ };
18362
+ // MS-7 全部移除: forget every paired server at once.
18363
+ const runUnpairAllServers = async () => {
18364
+ if (!window.confirm(t('settings.client.unpairAllServersConfirm')))
18365
+ return;
18366
+ setEntryBusy('all');
18367
+ setEntryActionFailed(false);
18368
+ if (await postClientRoute(settings_form_ts_1.CLIENT_UNPAIR_ROUTE, JSON.stringify({ all: true })))
18369
+ loadClientStatus();
18370
+ else
18371
+ setEntryActionFailed(true);
18372
+ setEntryBusy(null);
18373
+ };
18374
+ // MS-7 重新配对: point the pairing form at this server — its address in the
18375
+ // box (an already-paired address passes the cap gate), a blank code, the
18376
+ // focus on the code box so the user only types the fresh code.
18377
+ const repurposeServer = (server) => {
18378
+ setPairUrl(server.serverUrl);
18379
+ setPairUrlTouched(true);
18380
+ setPairCode('');
18381
+ setClaimFail(null);
18382
+ pairCodeRef.current?.focus();
18383
+ };
18060
18384
  const formDisabled = !form.available || !form.writable;
18061
18385
  // T17: saving any restart-required field (the role among them) moves the
18062
18386
  // row, and the host half's watcher reloads it — the gateway child restarts
@@ -18095,7 +18419,8 @@ function SettingsSectionPage({ config, shares, t }) {
18095
18419
  // One-of control (the shared fields have no select): the shared field chrome, a select body.
18096
18420
  const select = (state, field, label, hint, options, optionLabel) => ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-head", children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: `zr-settings-${field}`, children: label }), state.overridden && (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-badge", children: t('settings.overridden') }), state.overridden && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-settings-reset", disabled: formDisabled || state.locked, onClick: () => { config.resetField(field); }, children: t('settings.reset') }))] }), (0, jsx_runtime_1.jsx)("select", { id: `zr-settings-${field}`, className: "zr-settings-input", disabled: formDisabled || state.locked, value: state.text, onChange: (e) => { config.stage(field, e.currentTarget.value); }, children: options.map((option) => (0, jsx_runtime_1.jsx)("option", { value: option, children: optionLabel(option) }, option)) }), hint !== '' && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: hint }), clearPreviewNote(state), annotations(field)] }, field));
18097
18421
  // Boolean control (the shared fields have no checkbox): label row + switch.
18098
- const bool = (state, field, label, hint) => ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-head", children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: `zr-settings-${field}`, children: label }), state.overridden && (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-badge", children: t('settings.overridden') }), state.overridden && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-settings-reset", disabled: formDisabled || state.locked, onClick: () => { config.resetField(field); }, children: t('settings.reset') })), (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-check-row", children: (0, jsx_runtime_1.jsx)("input", { id: `zr-settings-${field}`, type: "checkbox", disabled: formDisabled || state.locked, checked: state.text === 'true', onChange: (e) => { config.stage(field, e.currentTarget.checked ? 'true' : 'false'); } }) })] }), (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: hint }), clearPreviewNote(state), annotations(field)] }, field));
18422
+ const bool = (state, field, label, hint, parentDisabled = false) => ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", "data-disabled": parentDisabled ? 'true' : undefined, children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-head", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-settings-head-title", children: label }), state.overridden && (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-badge", children: t('settings.overridden') }), state.overridden && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-settings-reset", disabled: formDisabled || state.locked || parentDisabled, onClick: () => { config.resetField(field); }, children: t('settings.reset') })), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Switch, { disabled: formDisabled || state.locked || parentDisabled, checked: !parentDisabled && state.text === 'true', label: label, onChange: (checked) => { config.stage(field, checked ? 'true' : 'false'); } })] }), (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: hint }), clearPreviewNote(state), annotations(field)] }, field));
18423
+ const pushOff = form.pushEnabled.text !== 'true';
18099
18424
  const deviceRow = (device) => ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-device", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-device-head", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-settings-device-name", children: device.name }), (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-badge", children: device.role === 'web' ? t('settings.deviceRoleWeb') : t('settings.deviceRoleDesktop') }), (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-badge", children: device.hasPush ? t('settings.devicePushOn') : t('settings.devicePushOff') }), (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-hint", children: t('settings.deviceLastSeen', { time: new Date(device.lastSeen).toLocaleString() }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-device-actions", children: [(0, jsx_runtime_1.jsxs)("select", { className: "zr-settings-input", "aria-label": t('settings.deviceRoleSelect', { name: device.name }), disabled: !localOps || localBusy, value: device.role, onChange: (e) => { void runDeviceAction({ action: 'set-role', id: device.id, role: e.currentTarget.value }); }, children: [(0, jsx_runtime_1.jsx)("option", { value: "web", children: t('settings.deviceRoleWeb') }), (0, jsx_runtime_1.jsx)("option", { value: "desktop-client", children: t('settings.deviceRoleDesktop') })] }), (0, jsx_runtime_1.jsx)("select", { className: "zr-settings-input", "aria-label": t('settings.deviceKindSelect', { name: device.name }), disabled: !localOps || localBusy, value: device.kind, onChange: (e) => { void runDeviceAction({ action: 'set-kind', id: device.id, kind: e.currentTarget.value }); }, children: DEVICE_KINDS.map((kind) => ((0, jsx_runtime_1.jsx)("option", { value: kind, children: kind === 'auto' ? t('settings.kindAuto') : kind === 'phone' ? t('settings.kindPhone') : t('settings.kindDesktop') }, kind))) }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "ghost", size: "sm", disabled: !localOps || localBusy, onClick: () => { setRenaming({ id: device.id, draft: device.name }); }, children: t('settings.deviceRename') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "ghost", size: "sm", disabled: !localOps || localBusy, onClick: () => {
18100
18425
  if (window.confirm(t('settings.revokeConfirm')))
18101
18426
  void runDeviceAction({ action: 'revoke', id: device.id });
@@ -18168,7 +18493,7 @@ function SettingsSectionPage({ config, shares, t }) {
18168
18493
  }
18169
18494
  saveLanded(reloadsRow);
18170
18495
  });
18171
- }, onDiscard: () => { config.discard(); }, children: [(0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.roleTitle') }), select(form.role, 'role', t('settings.role'), t('settings.roleHint'), ['host', 'client'], (option) => (option === 'host' ? t('settings.roleHost') : t('settings.roleClient'))), text(form.serverName, 'serverName', t('settings.fieldServerName'), t(clientRole ? 'settings.fieldServerNameHintClient' : 'settings.fieldServerNameHint'), t('settings.invalidName')), reloadPending && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-status-line", children: t('settings.reloadNote') }), !clientRole && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.gatewayTitle') }), text(form.host, 'host', t('settings.fieldHost'), t('settings.fieldHostHint'), '', false, t('settings.fieldHostNote')), text(form.port, 'port', t('settings.fieldPort'), t('settings.fieldPortHint'), t('settings.invalidPort'), true), text(form.trustedProxies, 'trustedProxies', t('settings.fieldTrustedProxies'), t('settings.fieldTrustedProxiesHint'), ''), text(form.rateLimit, 'rateLimit', t('settings.fieldRateLimit'), t('settings.fieldRateLimitHint'), t('settings.invalidPositiveInt'), true), text(form.vapidSubject, 'vapidSubject', t('settings.fieldVapidSubject'), t('settings.fieldVapidSubjectHint'), ''), (0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.pushTitle') }), bool(form.pushSummary, 'pushSummary', t('settings.fieldPushSummary'), t('settings.fieldPushSummaryHint')), bool(form.pushTurnEnd, 'pushTurnEnd', t('settings.fieldPushTurnEnd'), t('settings.fieldPushTurnEndHint')), bool(form.pushTool, 'pushTool', t('settings.fieldPushTool'), t('settings.fieldPushToolHint')), text(form.pushDebounceMs, 'pushDebounceMs', t('settings.fieldPushDebounceMs'), t('settings.fieldPushDebounceMsHint'), t('settings.invalidNonNegativeInt'), true), select(form.lang, 'lang', t('settings.fieldLang'), t('settings.fieldLangHint'), ['auto', 'zh', 'en'], (option) => (option === 'auto' ? t('settings.langAuto') : option === 'zh' ? t('settings.langZh') : t('settings.langEn'))), (0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.shareTitle') }), text(form.idleHours, 'idleHours', t('settings.fieldIdleHours'), t('settings.fieldIdleHoursHint'), t('settings.invalidHours'), true), bool(form.autoShareNewSessions, 'autoShareNewSessions', t('settings.fieldAutoShare'), t('settings.fieldAutoShareHint')), (0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.bridge.title') }), bridgeState === 'loading' && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: t('settings.bridge.loading') }), bridgeState === 'error' && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.bridge.loadFailed') }), bridgeState === 'empty' && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: t('settings.bridge.empty') }), bridgeLoad.rows.map((row) => {
18496
+ }, onDiscard: () => { config.discard(); }, children: [(0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.roleTitle') }), select(form.role, 'role', t('settings.role'), t('settings.roleHint'), ['host', 'client'], (option) => (option === 'host' ? t('settings.roleHost') : t('settings.roleClient'))), text(form.serverName, 'serverName', t('settings.fieldServerName'), t(clientRole ? 'settings.fieldServerNameHintClient' : 'settings.fieldServerNameHint'), t('settings.invalidName')), reloadPending && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-status-line", children: t('settings.reloadNote') }), !clientRole && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.gatewayTitle') }), text(form.host, 'host', t('settings.fieldHost'), t('settings.fieldHostHint'), '', false, t('settings.fieldHostNote')), text(form.port, 'port', t('settings.fieldPort'), t('settings.fieldPortHint'), t('settings.invalidPort'), true), text(form.trustedProxies, 'trustedProxies', t('settings.fieldTrustedProxies'), t('settings.fieldTrustedProxiesHint'), ''), text(form.rateLimit, 'rateLimit', t('settings.fieldRateLimit'), t('settings.fieldRateLimitHint'), t('settings.invalidPositiveInt'), true), text(form.vapidSubject, 'vapidSubject', t('settings.fieldVapidSubject'), t('settings.fieldVapidSubjectHint'), ''), (0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.pushTitle') }), bool(form.pushEnabled, 'pushEnabled', t('settings.fieldPushEnabled'), t('settings.fieldPushEnabledHint')), bool(form.pushSessionName, 'pushSessionName', t('settings.fieldPushSessionName'), t('settings.fieldPushSessionNameHint'), pushOff), bool(form.pushSummary, 'pushSummary', t('settings.fieldPushSummary'), t('settings.fieldPushSummaryHint'), pushOff), bool(form.pushTurnEnd, 'pushTurnEnd', t('settings.fieldPushTurnEnd'), t('settings.fieldPushTurnEndHint'), pushOff), bool(form.pushTool, 'pushTool', t('settings.fieldPushTool'), t('settings.fieldPushToolHint'), pushOff), text(form.pushDebounceMs, 'pushDebounceMs', t('settings.fieldPushDebounceMs'), t('settings.fieldPushDebounceMsHint'), t('settings.invalidNonNegativeInt'), true), select(form.lang, 'lang', t('settings.fieldLang'), t('settings.fieldLangHint'), ['auto', 'zh', 'en'], (option) => (option === 'auto' ? t('settings.langAuto') : option === 'zh' ? t('settings.langZh') : t('settings.langEn'))), (0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.shareTitle') }), text(form.idleHours, 'idleHours', t('settings.fieldIdleHours'), t('settings.fieldIdleHoursHint'), t('settings.invalidHours'), true), bool(form.autoShareNewSessions, 'autoShareNewSessions', t('settings.fieldAutoShare'), t('settings.fieldAutoShareHint')), (0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.bridge.title') }), bridgeState === 'loading' && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: t('settings.bridge.loading') }), bridgeState === 'error' && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.bridge.loadFailed') }), bridgeState === 'empty' && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: t('settings.bridge.empty') }), bridgeLoad.rows.map((row) => {
18172
18497
  let current = {};
18173
18498
  try {
18174
18499
  const parsed = JSON.parse(form.pluginBridgeConsent.text);
@@ -18189,12 +18514,15 @@ function SettingsSectionPage({ config, shares, t }) {
18189
18514
  : t('settings.gatewayDown') }), (0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.pairingTitle') }), (0, jsx_runtime_1.jsx)("div", { className: "zr-settings-role-cards", children: PAIR_ROLES.map((role) => ((0, jsx_runtime_1.jsxs)("label", { className: "zr-settings-role-card", "data-active": pairRole === role ? 'true' : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "zr-settings-role-card-title", children: [(0, jsx_runtime_1.jsx)("input", { type: "radio", name: "zr-settings-pair-role", checked: pairRole === role, onChange: () => { setPairRole(role); } }), role === 'web' ? t('settings.deviceRoleWeb') : t('settings.deviceRoleDesktop')] }), (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-hint", children: role === 'web' ? t('settings.pairingWebGuide') : t('settings.pairingDesktopGuide') })] }, role))) }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-row", style: { paddingBottom: 12 }, children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", disabled: !localOps || pairBusy, onClick: () => { void runPair(); }, children: pairBusy ? t('settings.pairingGenerating') : t('settings.pairingGenerate') }), pairFailed && (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.pairingFail') })] }), shownPairing !== null && ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-head", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-settings-head-title", children: t('settings.pairingCodeLabel') }), (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-badge", children: shownPairing.role === 'web' ? t('settings.deviceRoleWeb') : t('settings.deviceRoleDesktop') }), (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-badge", "data-warn": "true", children: t('settings.pairingRemaining', { count: shownPairing.remainingSeconds }) })] }), (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-pair-code", children: shownPairing.code })] })), (0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.devicesTitle') }), (data?.devices.length ?? 0) === 0 && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: t('settings.devicesEmpty') }), data?.devices.map(deviceRow), deviceFailed && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.actionFail') }), (data?.devices.length ?? 0) > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "zr-settings-row", style: { paddingBottom: 12 }, children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "ghost", size: "sm", disabled: !localOps || localBusy, onClick: () => {
18190
18515
  if (window.confirm(t('settings.revokeAllConfirm')))
18191
18516
  void runDeviceAction({ action: 'revoke-all' });
18192
- }, children: t('settings.revokeAll') }) })), (0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-head", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-settings-head-title", children: t('settings.pushTestLabel') }), (0, jsx_runtime_1.jsx)("span", { style: { flex: 1 } }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", disabled: !localOps || pushTest.state === 'busy', onClick: () => { void runPushTest(); }, children: pushTest.state === 'busy' ? t('settings.pushTestRunning') : t('settings.pushTestRun') })] }), pushTest.state === 'ok' && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: t('settings.pushTestOk', { sent: pushTest.sent, failed: pushTest.failed }) }), pushTest.state === 'fail' && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.pushTestFail') })] }), (0, jsx_runtime_1.jsx)(SharesList, { shares: shares, t: t })] })), clientRole && ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-card", children: [(0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.client.connectTitle') }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-settings-head", children: (0, jsx_runtime_1.jsx)("label", { htmlFor: "zr-settings-client-server", children: t('settings.client.serverUrl') }) }), (0, jsx_runtime_1.jsx)("input", { id: "zr-settings-client-server", className: "zr-settings-input", type: "text", autoComplete: "off", spellCheck: false, value: pairUrl, onChange: (e) => { setPairUrl(e.currentTarget.value); setPairUrlTouched(true); } }), (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: t('settings.client.serverUrlHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-settings-head", children: (0, jsx_runtime_1.jsx)("label", { htmlFor: "zr-settings-client-code", children: t('settings.client.pairingCode') }) }), (0, jsx_runtime_1.jsx)("input", { id: "zr-settings-client-code", className: "zr-settings-input zr-settings-code-input", type: "text", autoComplete: "off", spellCheck: false,
18517
+ }, children: t('settings.revokeAll') }) })), (0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-head", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-settings-head-title", children: t('settings.pushTestLabel') }), (0, jsx_runtime_1.jsx)("span", { style: { flex: 1 } }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", disabled: !localOps || pushTest.state === 'busy', onClick: () => { void runPushTest(); }, children: pushTest.state === 'busy' ? t('settings.pushTestRunning') : t('settings.pushTestRun') })] }), pushTest.state === 'ok' && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: t('settings.pushTestOk', { sent: pushTest.sent, failed: pushTest.failed }) }), pushTest.state === 'fail' && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.pushTestFail') })] }), (0, jsx_runtime_1.jsx)(SharesList, { shares: shares, t: t })] })), clientRole && ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-card", children: [(0, jsx_runtime_1.jsxs)("h3", { className: "zr-settings-card-title", children: [t('settings.client.connectTitle'), (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-badge", "data-invalid": clientView?.limitReached === true ? 'true' : undefined, children: t('settings.client.poolCount', { count: serverRows.length, max: maxServers }) })] }), (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", style: { paddingTop: 4 }, children: t('settings.client.poolCapNote', { max: maxServers }) }), clientView?.limitReached === true && ((0, jsx_runtime_1.jsx)("p", { className: "zr-settings-limit-alert", "data-invalid": "true", children: t('settings.client.limitReached', { count: serverRows.length, max: maxServers }) })), (0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-settings-head", children: (0, jsx_runtime_1.jsx)("label", { htmlFor: "zr-settings-client-server", children: t('settings.client.serverUrl') }) }), (0, jsx_runtime_1.jsx)("input", { id: "zr-settings-client-server", className: "zr-settings-input", type: "text", autoComplete: "off", spellCheck: false, value: pairUrl, onChange: (e) => { setPairUrl(e.currentTarget.value); setPairUrlTouched(true); } }), (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: t('settings.client.serverUrlHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-settings-head", children: (0, jsx_runtime_1.jsx)("label", { htmlFor: "zr-settings-client-code", children: t('settings.client.pairingCode') }) }), (0, jsx_runtime_1.jsx)("input", { id: "zr-settings-client-code", ref: pairCodeRef, className: "zr-settings-input zr-settings-code-input", type: "text", autoComplete: "off", spellCheck: false,
18193
18518
  // No maxLength: the draft is normalized (uppercased, spaces and
18194
18519
  // hyphens stripped) on every keystroke, and only the
18195
18520
  // NORMALIZED length gates the pair button — a code pasted with
18196
18521
  // separators must survive pasting intact.
18197
- value: pairCode, onChange: (e) => { setPairCode((0, settings_form_ts_1.normalizePairingCode)(e.currentTarget.value)); } }), (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: t('settings.client.pairingCodeHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-row", style: { paddingBottom: 12 }, children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", disabled: !claimAllowed, onClick: () => { void runClaim(); }, children: claimBusy ? t('settings.client.pairing') : t('settings.client.pair') }), form.serverName.invalid && (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.client.fixDeviceName') }), claimFail !== null && (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-hint", "data-invalid": "true", children: pairFailText(claimFail, t) }), claimWriteFailed && (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.client.failWrite') })] }), (0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.client.statusTitle') }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-row", style: { paddingBottom: 4 }, children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-settings-status-line", style: { flex: 1 }, "data-down": clientView !== undefined && (clientView.state === 'unreachable' || clientView.state === 'unexpected' || clientView.state === 'revoked' || clientView.state === 'invalid-url' || clientView.state === 'incompatible') ? 'true' : undefined, children: clientView === undefined ? '' : clientStatusText(clientView, now, t) }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", disabled: clientView === undefined || clientView.nextRetryAt === undefined || reconnectBusy, onClick: () => { void runReconnect(); }, children: reconnectBusy ? t('settings.client.reconnectBusy') : t('settings.client.reconnect') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", disabled: clientLoad.state === 'loading', onClick: loadClientStatus, children: clientLoad.state === 'loading' ? t('settings.client.statusRefreshing') : t('settings.client.statusRefresh') })] }), form.deviceToken.configured && ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-row", style: { paddingBottom: 12 }, children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-settings-badge", children: t('settings.client.tokenSet') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "ghost", size: "sm", disabled: !form.available || !form.writable || unpairBusy, onClick: () => { void runUnpair(); }, children: unpairBusy ? t('settings.client.unpairBusy') : t('settings.client.unpair') })] })), unpairDone && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", style: { paddingBottom: 12 }, children: t('settings.client.unpairDone') }), clientView !== undefined && (clientView.lastError !== '' || clientView.intercept !== undefined || clientView.compat !== undefined || (clientView.pluginBridge?.length ?? 0) > 0 || clientView.pluginRouteIntercept !== undefined || dropDiag.installed || dropDiag.notInstalledReason !== undefined) && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.client.diagTitle') }), clientView.lastError !== '' && ((0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": clientView.state === 'unreachable' ? 'true' : undefined, children: t('settings.client.diagLastError', { code: clientView.lastError }) })), clientView.intercept !== undefined && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: clientView.intercept.installed
18522
+ value: pairCode, onChange: (e) => { setPairCode((0, settings_form_ts_1.normalizePairingCode)(e.currentTarget.value)); } }), (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: t('settings.client.pairingCodeHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-row", style: { paddingBottom: 12 }, children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", disabled: !claimAllowed, onClick: () => { void runClaim(); }, children: claimBusy ? t('settings.client.pairing') : t('settings.client.pair') }), form.serverName.invalid && (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.client.fixDeviceName') }), claimFail !== null && (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-hint", "data-invalid": "true", children: pairFailText(claimFail, t) })] }), (0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.client.serversTitle') }), serverRows.length === 0 && ((0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", style: { padding: '8px 0 12px' }, children: t('settings.client.serversEmpty', { max: maxServers }) })), serverRows.map((server) => {
18523
+ const badge = serverStateBadge(server, now, t);
18524
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-device", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-device-head", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-settings-device-name", children: server.displayName !== '' ? server.displayName : server.serverUrl }), (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-badge", "data-live": badge.tone === 'live' ? 'true' : undefined, "data-warn": badge.tone === 'warn' ? 'true' : undefined, "data-invalid": badge.tone === 'invalid' ? 'true' : undefined, children: badge.text }), (0, jsx_runtime_1.jsx)("span", { className: "zr-settings-hint", children: server.serverUrl })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-device-actions", children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "ghost", size: "sm", disabled: entryBusy !== null || server.nextRetryAt === undefined, onClick: () => { void runEntryReconnect(server.entryId); }, children: entryBusy === server.entryId ? t('settings.client.reconnectBusy') : t('settings.client.reconnect') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "ghost", size: "sm", disabled: entryBusy !== null, onClick: () => { repurposeServer(server); }, children: t('settings.client.repair') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "ghost", size: "sm", disabled: entryBusy !== null, onClick: () => { void runUnpairEntry(server.entryId); }, children: t('settings.client.serverRemove') })] })] }, server.entryId));
18525
+ }), entryActionFailed && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.actionFail') }), serverRows.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "zr-settings-row", style: { paddingBottom: 12 }, children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "ghost", size: "sm", disabled: entryBusy !== null, onClick: () => { void runUnpairAllServers(); }, children: t('settings.client.unpairAllServers') }) })), (0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.client.statusTitle') }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-row", style: { paddingBottom: 4 }, children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-settings-status-line", style: { flex: 1 }, "data-down": clientView !== undefined && (clientView.state === 'unreachable' || clientView.state === 'unexpected' || clientView.state === 'revoked' || clientView.state === 'invalid-url' || clientView.state === 'incompatible') ? 'true' : undefined, children: clientView === undefined ? '' : clientStatusText(clientView, now, t) }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", disabled: clientView === undefined || clientView.nextRetryAt === undefined || reconnectBusy, onClick: () => { void runReconnect(); }, children: reconnectBusy ? t('settings.client.reconnectBusy') : t('settings.client.reconnect') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", disabled: clientLoad.state === 'loading', onClick: loadClientStatus, children: clientLoad.state === 'loading' ? t('settings.client.statusRefreshing') : t('settings.client.statusRefresh') })] }), form.deviceToken.configured && ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-row", style: { paddingBottom: 12 }, children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-settings-badge", children: t('settings.client.tokenSet') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "ghost", size: "sm", disabled: !form.available || !form.writable || unpairBusy, onClick: () => { void runUnpair(); }, children: unpairBusy ? t('settings.client.unpairBusy') : t('settings.client.unpair') })] })), unpairDone && (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", style: { paddingBottom: 12 }, children: t('settings.client.unpairDone') }), diagVisible && clientView !== undefined && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("h3", { className: "zr-settings-card-title", children: t('settings.client.diagTitle') }), legacyTopDiag && clientView.lastError !== '' && ((0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": clientView.state === 'unreachable' ? 'true' : undefined, children: t('settings.client.diagLastError', { code: clientView.lastError }) })), clientView.intercept !== undefined && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: clientView.intercept.installed
18198
18526
  ? t('settings.client.diagInterceptInstalled')
18199
18527
  : t('settings.client.diagInterceptMissing') }), clientView.intercept.reasons.length > 0 && ((0, jsx_runtime_1.jsxs)("p", { className: "zr-settings-hint", "data-invalid": "true", children: [t('settings.client.diagInterceptDisabled'), clientView.intercept.reasons.map((reason, index) => ((0, jsx_runtime_1.jsx)("span", { className: "zr-settings-hint", children: reason }, index)))] })), clientView.intercept.recentFailures.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-settings-head", children: (0, jsx_runtime_1.jsx)("label", { children: t('settings.client.diagRecentFailures') }) }), clientView.intercept.recentFailures.map((failure, index) => ((0, jsx_runtime_1.jsxs)("p", { className: "zr-settings-hint", children: [new Date(failure.time).toLocaleString(), " \u00B7 ", failure.method !== '' ? failure.method : '—', " \u00B7 ", failure.code] }, index)))] }))] })), (dropDiag.installed || dropDiag.notInstalledReason !== undefined) && ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-settings-head", children: (0, jsx_runtime_1.jsx)("label", { children: t('settings.client.diagRemoteDrop') }) }), (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: dropDiag.installed
18200
18528
  ? t('settings.client.diagRemoteDropOk', {
@@ -18225,9 +18553,11 @@ function SettingsSectionPage({ config, shares, t }) {
18225
18553
  fileUploads: typeof probe.fileUploadsService === 'string' ? probe.fileUploadsService : '—',
18226
18554
  });
18227
18555
  return ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-settings-head", children: (0, jsx_runtime_1.jsx)("label", { children: t('settings.client.diagUploadRedirect') }) }), (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: text })] }));
18228
- })(), clientView.compat !== undefined && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [clientView.compat.mismatchedGroups.length > 0 && ((0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.client.diagCompatGroups', { groups: clientView.compat.mismatchedGroups.join(', ') }) })), clientView.compat.recentCalls.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-settings-head", children: (0, jsx_runtime_1.jsx)("label", { children: t('settings.client.diagCompatCalls') }) }), clientView.compat.recentCalls.map((failure, index) => ((0, jsx_runtime_1.jsxs)("p", { className: "zr-settings-hint", children: [new Date(failure.time).toLocaleString(), " \u00B7 ", failure.method !== '' ? failure.method : '—', " \u00B7 ", failure.code] }, index)))] }))] })), (clientView.pluginBridge?.length ?? 0) > 0 && clientView.pluginBridge !== undefined && ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-settings-head", children: (0, jsx_runtime_1.jsx)("label", { children: t('settings.bridge.title') }) }), clientView.pluginBridge.map((row) => ((0, jsx_runtime_1.jsxs)("p", { className: "zr-settings-hint", children: [row.plugin, " \u00B7 ", row.localVersion !== '' ? `v${row.localVersion}` : '—', " \u00B7", ' ', row.stateKey === 'settings.bridge.stateVersionMismatch'
18556
+ })(), legacyTopDiag && clientView.compat !== undefined && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [clientView.compat.mismatchedGroups.length > 0 && ((0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.client.diagCompatGroups', { groups: clientView.compat.mismatchedGroups.join(', ') }) })), clientView.compat.recentCalls.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-settings-head", children: (0, jsx_runtime_1.jsx)("label", { children: t('settings.client.diagCompatCalls') }) }), clientView.compat.recentCalls.map((failure, index) => ((0, jsx_runtime_1.jsxs)("p", { className: "zr-settings-hint", children: [new Date(failure.time).toLocaleString(), " \u00B7 ", failure.method !== '' ? failure.method : '—', " \u00B7 ", failure.code] }, index)))] }))] })), legacyTopDiag && (clientView.pluginBridge?.length ?? 0) > 0 && clientView.pluginBridge !== undefined && ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-settings-head", children: (0, jsx_runtime_1.jsx)("label", { children: t('settings.bridge.title') }) }), clientView.pluginBridge.map((row) => ((0, jsx_runtime_1.jsxs)("p", { className: "zr-settings-hint", children: [row.plugin, " \u00B7 ", row.localVersion !== '' ? `v${row.localVersion}` : '—', " \u00B7", ' ', row.stateKey === 'settings.bridge.stateVersionMismatch'
18229
18557
  ? t(row.stateKey, { version: row.serverVersion ?? '?' })
18230
- : t(row.stateKey)] }, row.plugin)))] })), clientView.pluginRouteIntercept !== undefined && !clientView.pluginRouteIntercept.installed && ((0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.bridge.interceptOff', { reason: clientView.pluginRouteIntercept.reasons.join('; ') || '—' }) }))] }))] })), (0, jsx_runtime_1.jsx)("div", { className: "zr-settings-card", children: (0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-head", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-settings-head-title", children: t('settings.debug.title') }), (0, jsx_runtime_1.jsx)("span", { style: { flex: 1 } }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", disabled: debugBusy, onClick: () => { void runDebugExport(); }, children: debugBusy ? t('settings.debug.exporting') : t('settings.debug.export') })] }), (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: t('settings.debug.hint') })] }) })] }));
18558
+ : t(row.stateKey)] }, row.plugin)))] })), clientView.pluginRouteIntercept !== undefined && !clientView.pluginRouteIntercept.installed && ((0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.bridge.interceptOff', { reason: clientView.pluginRouteIntercept.reasons.join('; ') || '—' }) })), hasServerDiag && ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-settings-head", children: (0, jsx_runtime_1.jsx)("label", { children: t('settings.client.serverDiagTitle') }) }), serverRows.filter(serverHasDiag).map((server) => ((0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-device", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-settings-device-name", children: server.displayName !== '' ? server.displayName : server.serverUrl }), server.lastError !== '' && ((0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": server.state === 'unreachable' ? 'true' : undefined, children: t('settings.client.diagLastError', { code: server.lastError }) })), server.compat !== undefined && server.compat.mismatchedGroups.length > 0 && ((0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", "data-invalid": "true", children: t('settings.client.diagCompatGroups', { groups: server.compat.mismatchedGroups.join(', ') }) })), server.compat?.recentCalls.map((failure, index) => ((0, jsx_runtime_1.jsxs)("p", { className: "zr-settings-hint", children: [t('settings.client.diagCompatCalls'), " \u00B7 ", new Date(failure.time).toLocaleString(), " \u00B7 ", failure.method !== '' ? failure.method : '—', " \u00B7 ", failure.code] }, index))), server.pluginBridge.map((row) => ((0, jsx_runtime_1.jsxs)("p", { className: "zr-settings-hint", children: [t('settings.bridge.title'), " \u00B7 ", row.plugin, " \u00B7 ", row.localVersion !== '' ? `v${row.localVersion}` : '—', " \u00B7", ' ', row.stateKey === 'settings.bridge.stateVersionMismatch'
18559
+ ? t(row.stateKey, { version: row.serverVersion ?? '?' })
18560
+ : t(row.stateKey)] }, row.plugin)))] }, server.entryId)))] }))] }))] })), (0, jsx_runtime_1.jsx)("div", { className: "zr-settings-card", children: (0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-field", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-settings-head", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-settings-head-title", children: t('settings.debug.title') }), (0, jsx_runtime_1.jsx)("span", { style: { flex: 1 } }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", size: "sm", disabled: debugBusy, onClick: () => { void runDebugExport(); }, children: debugBusy ? t('settings.debug.exporting') : t('settings.debug.export') })] }), (0, jsx_runtime_1.jsx)("p", { className: "zr-settings-hint", children: t('settings.debug.hint') })] }) })] }));
18231
18561
  }
18232
18562
  /** Display name of a value's source layer for the invalid-saved-value note. */
18233
18563
  function sourceName(source, t) {
@@ -18342,6 +18672,9 @@ exports.SETTINGS_CSS = `
18342
18672
  [data-zen-remote="settings"] .zr-settings-field + .zr-settings-field {
18343
18673
  border-top: 1px solid var(--dsw-alias-border-l2);
18344
18674
  }
18675
+ [data-zen-remote="settings"] .zr-settings-field[data-disabled="true"] {
18676
+ opacity: 0.5;
18677
+ }
18345
18678
  [data-zen-remote="settings"] .zr-settings-head {
18346
18679
  display: flex;
18347
18680
  align-items: center;
@@ -18517,6 +18850,21 @@ exports.SETTINGS_CSS = `
18517
18850
  [data-zen-remote="settings"] .zr-settings-badge[data-live="true"] {
18518
18851
  color: var(--dsw-alias-state-success-primary, #16a34a);
18519
18852
  }
18853
+ [data-zen-remote="settings"] .zr-settings-badge[data-invalid="true"] {
18854
+ color: var(--dsw-alias-state-error-primary, #d5304f);
18855
+ }
18856
+ /* MS-7: the full-pool alert must be impossible to miss — red, bold, framed. */
18857
+ [data-zen-remote="settings"] .zr-settings-limit-alert,
18858
+ [data-zen-remote="settings"] .zr-settings-limit-alert[data-invalid="true"] {
18859
+ font-size: 13px;
18860
+ font-weight: 700;
18861
+ line-height: 1.5;
18862
+ color: var(--dsw-alias-state-error-primary, #d5304f);
18863
+ border: 1px solid var(--dsw-alias-state-error-primary, #d5304f);
18864
+ border-radius: 8px;
18865
+ padding: 8px 12px;
18866
+ margin: 8px 0 4px;
18867
+ }
18520
18868
  [data-zen-remote="settings"] .zr-settings-footer {
18521
18869
  display: flex;
18522
18870
  align-items: center;
@@ -18896,6 +19244,12 @@ __modules["client-data/wizard.js"] = function (require, module, exports) {
18896
19244
  * row layer applies the same rule when the fields land there, so a client
18897
19245
  * that accepts something the resolver would silently drop would only set
18898
19246
  * the user up for a mystery.
19247
+ *
19248
+ * MS-6 adds the paired-pool overview pieces: the pool count is a first-class
19249
+ * "configured" signal (MS-5 pairs straight into the pool store, not the
19250
+ * row), and {@link deriveClientOverviewSummary} derives the overview card's
19251
+ * counters and its two lights (connection health / pool fill) off the
19252
+ * `client/status` pool list.
18899
19253
  */
18900
19254
  Object.defineProperty(exports, "__esModule", { value: true });
18901
19255
  exports.SNIPPET_KINDS = exports.WIZARD_TUNNEL_STATUS_ROUTE = exports.WIZARD_TUNNEL_STOP_ROUTE = exports.WIZARD_TUNNEL_SETUP_ROUTE = exports.WIZARD_TUNNEL_CHECK_ROUTE = exports.WIZARD_CADDY_STATUS_ROUTE = exports.WIZARD_CADDY_STOP_ROUTE = exports.WIZARD_CADDY_START_ROUTE = exports.WIZARD_TUNNEL_TOKEN_ROUTE = exports.WIZARD_CF_TOKEN_ROUTE = exports.WIZARD_VERIFY_ROUTE = exports.WIZARD_STATUS_ROUTE = void 0;
@@ -18911,12 +19265,15 @@ exports.isValidPort = isValidPort;
18911
19265
  exports.publicUrlFrom = publicUrlFrom;
18912
19266
  exports.configureReadiness = configureReadiness;
18913
19267
  exports.deriveWizardLights = deriveWizardLights;
19268
+ exports.deriveClientOverviewSummary = deriveClientOverviewSummary;
18914
19269
  exports.deriveClientWizardLights = deriveClientWizardLights;
19270
+ exports.deriveClientServerLights = deriveClientServerLights;
18915
19271
  exports.pairingSecondsLeft = pairingSecondsLeft;
18916
19272
  exports.caddyFailureCopy = caddyFailureCopy;
18917
19273
  exports.nginxSnippetText = nginxSnippetText;
18918
19274
  exports.caddySnippetText = caddySnippetText;
18919
19275
  exports.luckyStepTexts = luckyStepTexts;
19276
+ const settings_form_ts_1 = require("./client-data/settings-form.js");
18920
19277
  /** Wizard route constants — mirror of src/wizard/routes.ts. */
18921
19278
  exports.WIZARD_STATUS_ROUTE = '/_dsh/zen-remote/admin/wizard/status';
18922
19279
  exports.WIZARD_VERIFY_ROUTE = '/_dsh/zen-remote/admin/wizard/verify';
@@ -18967,9 +19324,16 @@ function initialStep(status) {
18967
19324
  }
18968
19325
  /**
18969
19326
  * Whether the inline overview card should render the configured status
18970
- * dashboard instead of the first-time setup prompt.
19327
+ * dashboard instead of the first-time setup prompt. MS-6: the paired pool's
19328
+ * size is the client's first-class signal — since MS-5 a claim lands in the
19329
+ * pool store and never touches the row's `serverUrl` / `deviceToken`, so a
19330
+ * freshly multi-paired machine with an empty row must still read as
19331
+ * configured. `> 0` answers true no matter what the other inputs say; the
19332
+ * field is optional and an omitted one (or a zero) changes nothing.
18971
19333
  */
18972
19334
  function isWizardConfigured(input) {
19335
+ if ((input.pairedServerCount ?? 0) > 0)
19336
+ return true;
18973
19337
  if (input.effectiveClient) {
18974
19338
  return input.tokenConfigured || input.clientServerUrl.trim() !== '' || input.clientState === 'connected';
18975
19339
  }
@@ -19100,6 +19464,46 @@ const CLIENT_FAIL_STATES = new Set([
19100
19464
  'invalid-url',
19101
19465
  'unexpected',
19102
19466
  ]);
19467
+ /**
19468
+ * The view states that will NOT come back on their own (MS-6): a revoked
19469
+ * token, an incompatible interface, an address that no longer parses — each
19470
+ * needs the settings page before that server serves again. They turn the
19471
+ * overview's connection light red; a merely `unreachable` server is mid-retry
19472
+ * and stays amber.
19473
+ */
19474
+ const CLIENT_DEAD_STATES = new Set(['revoked', 'incompatible', 'invalid-url']);
19475
+ /**
19476
+ * Derive the sub-client overview summary off one derived pool list: green
19477
+ * only when EVERY paired server is connected; red when any server is in a
19478
+ * state that will not recover on its own (revoked / incompatible /
19479
+ * invalid-url) or when the pool is FULL — the cap is a servers fact, so a
19480
+ * full pool of healthy connections still shows a red servers light while its
19481
+ * connection light stays green; amber for the rest (nothing paired yet, or
19482
+ * servers the retry ladder is still working on).
19483
+ */
19484
+ function deriveClientOverviewSummary(servers, maxServers = settings_form_ts_1.MAX_PAIRED_SERVERS) {
19485
+ const totalCount = servers.length;
19486
+ let onlineCount = 0;
19487
+ let revokedCount = 0;
19488
+ let deadCount = 0;
19489
+ for (const server of servers) {
19490
+ if (server.state === 'connected')
19491
+ onlineCount += 1;
19492
+ if (server.state === 'revoked')
19493
+ revokedCount += 1;
19494
+ if (CLIENT_DEAD_STATES.has(server.state))
19495
+ deadCount += 1;
19496
+ }
19497
+ const offlineCount = totalCount - onlineCount - revokedCount;
19498
+ const limitReached = totalCount >= maxServers;
19499
+ const connectionLight = totalCount === 0
19500
+ ? 'pending'
19501
+ : onlineCount === totalCount
19502
+ ? 'pass'
19503
+ : deadCount > 0 ? 'fail' : 'pending';
19504
+ const serversLight = limitReached ? 'fail' : totalCount > 0 ? 'pass' : 'pending';
19505
+ return { totalCount, onlineCount, offlineCount, revokedCount, maxServers, limitReached, connectionLight, serversLight };
19506
+ }
19103
19507
  /**
19104
19508
  * Derive the three status lights for the desktop sub-client verification
19105
19509
  * step (`role === 'client'`).
@@ -19113,6 +19517,27 @@ function deriveClientWizardLights(input) {
19113
19517
  sync: connected ? (input.interceptInstalled === false ? 'fail' : 'pass') : 'pending',
19114
19518
  };
19115
19519
  }
19520
+ /**
19521
+ * The wizard step-3's three lights for ONE selected paired server (MS-8):
19522
+ * the credential is green by the row's own existence (the token lives in the
19523
+ * pool store, never in the row) and red only when the server refused it
19524
+ * (`revoked`); the connection follows the same fail-set the global lights
19525
+ * use (an offline server mid-retry is red here too — the step is staring at
19526
+ * THAT one server, not the pool); sync is green on a connected row whose
19527
+ * interface groups all match, red on a compat mismatch, amber otherwise.
19528
+ * `undefined` (nothing paired, or nothing selected yet) keeps all three
19529
+ * amber.
19530
+ */
19531
+ function deriveClientServerLights(server) {
19532
+ if (server === undefined)
19533
+ return { credential: 'pending', connection: 'pending', sync: 'pending' };
19534
+ const connected = server.state === 'connected';
19535
+ return {
19536
+ credential: server.state === 'revoked' ? 'fail' : 'pass',
19537
+ connection: connected ? 'pass' : CLIENT_FAIL_STATES.has(server.state) ? 'fail' : 'pending',
19538
+ sync: connected ? ((server.compat?.mismatchedGroups.length ?? 0) > 0 ? 'fail' : 'pass') : 'pending',
19539
+ };
19540
+ }
19116
19541
  /** Pairing-code countdown, whole seconds, never negative. */
19117
19542
  function pairingSecondsLeft(expiresAt, now) {
19118
19543
  return Math.max(0, Math.ceil((expiresAt - now) / 1000));
@@ -19152,8 +19577,11 @@ function nginxSnippetText(input) {
19152
19577
  const parts = valid(input);
19153
19578
  if (parts === null)
19154
19579
  return null;
19580
+ const headerComment = input.lang === 'en'
19581
+ ? '# Place this in the http {} block (skip if you already have this map)'
19582
+ : '# 这段放在 http {} 层(若已有同名 map 则跳过)';
19155
19583
  return [
19156
- '# 这段放在 http {} 层(若已有同名 map 则跳过)',
19584
+ headerComment,
19157
19585
  'map $http_upgrade $connection_upgrade {',
19158
19586
  ' default upgrade;',
19159
19587
  " '' close;",
@@ -19192,6 +19620,13 @@ function luckyStepTexts(input) {
19192
19620
  const parts = valid(input);
19193
19621
  if (parts === null)
19194
19622
  return null;
19623
+ if (input.lang === 'en') {
19624
+ return [
19625
+ 'In Lucky "Web Service", create a new rule listening on port 443 and bind your domain.',
19626
+ `Set the backend (reverse proxy target) to http://${parts.upstream}:${parts.port} and enable WebSocket support.`,
19627
+ 'Turn on the catch-all switch to allow all paths, then save.',
19628
+ ];
19629
+ }
19195
19630
  return [
19196
19631
  '在 Lucky 的「Web 服务」里新建一条规则,监听 443 端口,绑定你的域名。',
19197
19632
  `后端(反代目标)填 http://${parts.upstream}:${parts.port},开启 WebSocket 支持。`,
@@ -19292,7 +19727,8 @@ const wizard_ts_1 = require("./client-data/wizard.js");
19292
19727
  const WizardCard_tsx_1 = require("./client/wizard/WizardCard.js");
19293
19728
  function ProxyCard({ t, domain, upstreamPort, onCheck, onClose }) {
19294
19729
  const [tab, setTab] = (0, react_1.useState)('nginx');
19295
- const input = { domain, upstreamHost: '127.0.0.1', upstreamPort };
19730
+ const lang = t('wizard.card.close') === 'Close' ? 'en' : 'zh';
19731
+ const input = { domain, upstreamHost: '127.0.0.1', upstreamPort, lang };
19296
19732
  const text = tab === 'nginx'
19297
19733
  ? (0, wizard_ts_1.nginxSnippetText)(input)
19298
19734
  : tab === 'caddy' ? (0, wizard_ts_1.caddySnippetText)(input) : ((0, wizard_ts_1.luckyStepTexts)(input) ?? []).join('\n');
@@ -19407,21 +19843,6 @@ const PATH_OPTIONS = [
19407
19843
  ['cloudflared', 'wizard.path.cloudflared', 'wizard.path.cloudflaredDesc'],
19408
19844
  ['lan', 'wizard.path.lan', 'wizard.path.lanDesc'],
19409
19845
  ];
19410
- function clientStatusCopy(view, now, t) {
19411
- const line = (0, settings_form_ts_1.clientStatusLineOf)(view, now);
19412
- switch (line.kind) {
19413
- case 'connectedName': return t('settings.client.statusConnectedName', { serverName: line.serverName });
19414
- case 'connected': return t('settings.client.statusConnected', { serverUrl: line.serverUrl });
19415
- case 'offlineRetry': return t('settings.client.statusOfflineRetry', { seconds: line.seconds });
19416
- case 'offlineRetrySoon': return t('settings.client.statusOfflineRetrySoon');
19417
- case 'unreachable': return t('settings.client.statusUnreachable');
19418
- case 'revoked': return t('settings.client.statusRevoked');
19419
- case 'incompatible': return t('settings.client.statusIncompatible');
19420
- case 'unexpected': return t('settings.client.statusUnexpected');
19421
- case 'invalidUrl': return t('settings.client.statusInvalidUrl');
19422
- default: return t('settings.client.statusUnpaired');
19423
- }
19424
- }
19425
19846
  function claimFailCopy(code, retryAfterMs, t) {
19426
19847
  switch (code) {
19427
19848
  case 'invalid': return t('settings.client.failInvalid');
@@ -19433,9 +19854,43 @@ function claimFailCopy(code, retryAfterMs, t) {
19433
19854
  }
19434
19855
  case 'unreachable': return t('settings.client.failUnreachable');
19435
19856
  case 'role-mismatch': return t('settings.client.failRoleMismatch');
19857
+ case 'limit-reached': return t('settings.client.failLimitReached');
19436
19858
  default: return t('settings.client.failUnexpected');
19437
19859
  }
19438
19860
  }
19861
+ /** The verdicts that will not recover on their own (MS-8): a pool row's dot
19862
+ * turns red on these, amber on everything else, green only when connected. */
19863
+ const SERVER_DEAD_STATES = new Set(['revoked', 'incompatible', 'invalid-url']);
19864
+ /** One paired server's dot state in the management surfaces (MS-8). */
19865
+ function serverDotState(state) {
19866
+ if (state === 'connected')
19867
+ return 'pass';
19868
+ return SERVER_DEAD_STATES.has(state) ? 'fail' : 'pending';
19869
+ }
19870
+ /**
19871
+ * One paired server's badge copy + tone (MS-8): the settings page's
19872
+ * per-server wording on the same keys, so the wizard's management modal and
19873
+ * the settings block cannot drift apart in what a state is CALLED.
19874
+ */
19875
+ function serverBadgeCopy(server, now, t) {
19876
+ switch (server.state) {
19877
+ case 'connected': return { text: t('settings.client.serverStateConnected'), tone: 'live' };
19878
+ case 'unreachable':
19879
+ case 'unexpected': {
19880
+ if (server.nextRetryAt === undefined)
19881
+ return { text: t('settings.client.serverStateOffline'), tone: 'warn' };
19882
+ const seconds = Math.max(0, Math.ceil((server.nextRetryAt - now) / 1000));
19883
+ return {
19884
+ text: seconds > 0 ? t('settings.client.serverStateRetry', { seconds }) : t('settings.client.serverStateRetrySoon'),
19885
+ tone: 'warn',
19886
+ };
19887
+ }
19888
+ case 'revoked': return { text: t('settings.client.serverStateRevoked'), tone: 'invalid' };
19889
+ case 'incompatible': return { text: t('settings.client.serverStateIncompatible'), tone: 'invalid' };
19890
+ case 'invalid-url': return { text: t('settings.client.serverStateInvalidUrl'), tone: 'invalid' };
19891
+ default: return { text: t('settings.client.serverStateUnpaired'), tone: undefined };
19892
+ }
19893
+ }
19439
19894
  function GatewayWizard(props) {
19440
19895
  const { t, config } = props;
19441
19896
  const [modalOpen, setModalOpen] = (0, react_1.useState)(false);
@@ -19506,8 +19961,14 @@ function GatewayWizard(props) {
19506
19961
  const [clientClaimFail, setClientClaimFail] = (0, react_1.useState)(null);
19507
19962
  const [clientView, setClientView] = (0, react_1.useState)();
19508
19963
  const [clientLoading, setClientLoading] = (0, react_1.useState)(false);
19509
- const [clientReconnectBusy, setClientReconnectBusy] = (0, react_1.useState)(false);
19510
19964
  const [clientUnpairBusy, setClientUnpairBusy] = (0, react_1.useState)(false);
19965
+ // MS-8: the paired-server pool's management surfaces — the 已配对服务端
19966
+ // 管理弹窗, the entry the wizard's step 3 is staring at, and the per-entry
19967
+ // action busy/failed markers both the modal and step 3 share.
19968
+ const [serversModalOpen, setServersModalOpen] = (0, react_1.useState)(false);
19969
+ const [activeClientEntryId, setActiveClientEntryId] = (0, react_1.useState)();
19970
+ const [serverBusyEntry, setServerBusyEntry] = (0, react_1.useState)(null);
19971
+ const [serverActionFailed, setServerActionFailed] = (0, react_1.useState)(false);
19511
19972
  // The saved role, live.
19512
19973
  const roleSnapshot = (0, react_1.useSyncExternalStore)((0, react_1.useCallback)((cb) => config.subscribe(cb), [config]), () => config.getSnapshot());
19513
19974
  const savedClient = config.savedRoleIsClient();
@@ -19637,9 +20098,9 @@ function GatewayWizard(props) {
19637
20098
  setDeviceBusy(false);
19638
20099
  }
19639
20100
  };
19640
- // Close the wizard modal or devices modal on Escape when no sub-card is open and not mid-save.
20101
+ // Close the wizard modal, devices modal or servers modal on Escape when no sub-card is open and not mid-save.
19641
20102
  (0, react_1.useEffect)(() => {
19642
- if ((!modalOpen && !devicesModalOpen) || card !== undefined)
20103
+ if ((!modalOpen && !devicesModalOpen && !serversModalOpen) || card !== undefined)
19643
20104
  return;
19644
20105
  const onKey = (event) => {
19645
20106
  if (event.key === 'Escape' && !savingConfig && !clientClaimBusy && !deviceBusy) {
@@ -19651,12 +20112,16 @@ function GatewayWizard(props) {
19651
20112
  setDevicesModalOpen(false);
19652
20113
  return;
19653
20114
  }
20115
+ if (serversModalOpen) {
20116
+ setServersModalOpen(false);
20117
+ return;
20118
+ }
19654
20119
  setModalOpen(false);
19655
20120
  }
19656
20121
  };
19657
20122
  document.addEventListener('keydown', onKey);
19658
20123
  return () => document.removeEventListener('keydown', onKey);
19659
- }, [modalOpen, devicesModalOpen, card, savingConfig, clientClaimBusy, deviceBusy, renamingDevice]);
20124
+ }, [modalOpen, devicesModalOpen, serversModalOpen, card, savingConfig, clientClaimBusy, deviceBusy, renamingDevice]);
19660
20125
  // Seed the entry step AND the configure forms from the FIRST ready
19661
20126
  // wizard status on a host deployment.
19662
20127
  const seededRef = (0, react_1.useRef)(false);
@@ -20016,12 +20481,12 @@ function GatewayWizard(props) {
20016
20481
  if (wizardStatus?.lanAddress !== undefined && wizardStatus.lanAddress !== '') {
20017
20482
  return `http://${wizardStatus.lanAddress}:${port}`;
20018
20483
  }
20019
- return `http://<服务端局域网IP>:${port}`;
20020
- }, [path, serverUrl, readiness, wizardStatus]);
20484
+ return t('wizard.desktop.addressFallback', { port: String(port) });
20485
+ }, [path, serverUrl, readiness, wizardStatus, t]);
20021
20486
  const copyDesktopInfo = async () => {
20022
20487
  if (desktopPairing === undefined)
20023
20488
  return;
20024
- const text = `服务端地址:${resolvedHostEndpoint}\n桌面客户端配对码:${desktopPairing.code}`;
20489
+ const text = t('wizard.desktop.copiedText', { address: resolvedHostEndpoint, code: desktopPairing.code });
20025
20490
  try {
20026
20491
  await navigator.clipboard?.writeText(text);
20027
20492
  setDesktopCopied(true);
@@ -20049,6 +20514,14 @@ function GatewayWizard(props) {
20049
20514
  setFormError('bad-client-code');
20050
20515
  return;
20051
20516
  }
20517
+ // MS-8: a NEW address at a full pool is refused locally (the button is
20518
+ // gated too — this backs it up); an already-paired address still
20519
+ // re-pairs its entry in place, which is how a revoked token at 8/8
20520
+ // gets back in.
20521
+ if (claimGate.blockedByLimit) {
20522
+ setClientClaimFail({ code: 'limit-reached' });
20523
+ return;
20524
+ }
20052
20525
  setClientClaimBusy(true);
20053
20526
  try {
20054
20527
  // If the deployment is currently running as 'host', switch role to 'client'
@@ -20070,18 +20543,29 @@ function GatewayWizard(props) {
20070
20543
  const response = await fetch(settings_form_ts_1.CLIENT_CLAIM_ROUTE, {
20071
20544
  method: 'POST',
20072
20545
  headers: { 'Content-Type': 'application/json' },
20546
+ // MS-5 pool mode: the route reads `name` (the settings page's field).
20073
20547
  body: JSON.stringify({
20074
20548
  serverUrl: trimmedUrl,
20075
20549
  code: normalizedCode,
20076
- deviceName,
20550
+ name: deviceName,
20077
20551
  }),
20078
20552
  });
20079
20553
  const body = (await response.json());
20080
- if (body?.ok === true && typeof body.token === 'string' && body.token !== '') {
20081
- const urlToWrite = typeof body.serverUrl === 'string' && body.serverUrl !== '' ? body.serverUrl : trimmedUrl;
20082
- const echoed = typeof body.deviceName === 'string' && body.deviceName.trim() !== '' ? body.deviceName.trim() : deviceName;
20083
- const landed = await config.writeClientPairing(urlToWrite, body.token, echoed);
20084
- if (landed && (0, settings_form_ts_1.shouldReloadAfterClientActivation)({
20554
+ if (body?.ok === true) {
20555
+ if (typeof body.entryId === 'string' && body.entryId !== '') {
20556
+ // Pool mode: the pairing landed straight in the pool store (the
20557
+ // token never rides the answer) — remember WHICH entry the wizard
20558
+ // just touched so step 3 opens on it.
20559
+ setActiveClientEntryId(body.entryId);
20560
+ }
20561
+ else if (typeof body.token === 'string' && body.token !== '') {
20562
+ // Legacy single-server route: the token rides the answer and the
20563
+ // row is the store.
20564
+ const urlToWrite = typeof body.serverUrl === 'string' && body.serverUrl !== '' ? body.serverUrl : trimmedUrl;
20565
+ const echoed = typeof body.deviceName === 'string' && body.deviceName.trim() !== '' ? body.deviceName.trim() : deviceName;
20566
+ await config.writeClientPairing(urlToWrite, body.token, echoed);
20567
+ }
20568
+ if ((0, settings_form_ts_1.shouldReloadAfterClientActivation)({
20085
20569
  wasEffectiveClient,
20086
20570
  nowClientRole: true,
20087
20571
  savedOrPaired: true,
@@ -20112,32 +20596,42 @@ function GatewayWizard(props) {
20112
20596
  setClientClaimBusy(false);
20113
20597
  }
20114
20598
  };
20115
- const runClientReconnect = async () => {
20116
- setClientReconnectBusy(true);
20599
+ const runClientUnpair = async () => {
20600
+ setClientUnpairBusy(true);
20117
20601
  try {
20118
- await fetch(settings_form_ts_1.CLIENT_RECONNECT_ROUTE, { method: 'POST' });
20602
+ await config.clearDeviceToken();
20119
20603
  await loadClientStatus();
20604
+ setStep('configure');
20120
20605
  }
20121
20606
  catch {
20122
20607
  // ignore
20123
20608
  }
20124
20609
  finally {
20125
- setClientReconnectBusy(false);
20610
+ setClientUnpairBusy(false);
20126
20611
  }
20127
20612
  };
20128
- const runClientUnpair = async () => {
20129
- setClientUnpairBusy(true);
20613
+ // MS-8: one paired server's management action — the reconnect route's
20614
+ // `{ entryId }` shape or the unpair route's. The status refresh runs
20615
+ // either way so the surface re-renders on the pool's verdict.
20616
+ const runServerAction = async (route, entryId, payload) => {
20617
+ setServerBusyEntry(entryId);
20618
+ setServerActionFailed(false);
20130
20619
  try {
20131
- await config.clearDeviceToken();
20132
- await loadClientStatus();
20133
- setStep('configure');
20620
+ const response = await fetch(route, {
20621
+ method: 'POST',
20622
+ headers: { 'Content-Type': 'application/json' },
20623
+ body: JSON.stringify(payload),
20624
+ });
20625
+ if (!response.ok)
20626
+ setServerActionFailed(true);
20134
20627
  }
20135
20628
  catch {
20136
- // ignore
20629
+ setServerActionFailed(true);
20137
20630
  }
20138
20631
  finally {
20139
- setClientUnpairBusy(false);
20632
+ setServerBusyEntry(null);
20140
20633
  }
20634
+ await loadClientStatus();
20141
20635
  };
20142
20636
  // Managed-Caddy & Tunnel controls (GW-13a / GW-13b): keyed strictly off the
20143
20637
  // currently selected `path` so switching away from Tunnel or Caddy never
@@ -20269,6 +20763,22 @@ function GatewayWizard(props) {
20269
20763
  clientState: clientView?.state,
20270
20764
  interceptInstalled: clientView?.intercept?.installed,
20271
20765
  });
20766
+ // MS-8: the paired-server pool as the overview grid, wizard steps 2-3 and
20767
+ // the management modal all read it. The step-3 selection falls back to the
20768
+ // first row (pairing order) when nothing is recorded yet or the recorded
20769
+ // entry was removed.
20770
+ const serverRows = clientView?.servers ?? [];
20771
+ const poolSummary = (0, wizard_ts_1.deriveClientOverviewSummary)(serverRows, clientView?.maxServers);
20772
+ const activeServer = serverRows.find((server) => server.entryId === activeClientEntryId) ?? serverRows[0];
20773
+ const serverLights = (0, wizard_ts_1.deriveClientServerLights)(activeServer);
20774
+ // The pairing form's cap gate (MS-8): blockedByLimit is true only for a NEW
20775
+ // address at a full pool — an already-paired address never trips it.
20776
+ const claimGate = (0, settings_form_ts_1.canSubmitClientPairing)({
20777
+ pairUrl: clientServerUrl,
20778
+ pairCode: clientPairCode,
20779
+ servers: serverRows,
20780
+ ...(clientView !== undefined ? { maxServers: clientView.maxServers } : {}),
20781
+ });
20272
20782
  const verified = poll?.result === 'pass';
20273
20783
  const qr = startResult?.ok === true ? (0, qr_ts_1.qrMatrix)(startResult.publicUrl) : null;
20274
20784
  const stepLabel = (name) => t(name === 'pick' ? 'wizard.step.pick' : name === 'configure' ? 'wizard.step.configure' : 'wizard.step.verify');
@@ -20291,9 +20801,16 @@ function GatewayWizard(props) {
20291
20801
  tokenConfigured: roleSnapshot.deviceToken.configured,
20292
20802
  clientServerUrl,
20293
20803
  clientState: clientView?.state,
20804
+ pairedServerCount: serverRows.length,
20294
20805
  });
20295
20806
  const statusSummary = (() => {
20296
20807
  if (effectiveClient) {
20808
+ if (poolSummary.totalCount > 0) {
20809
+ return {
20810
+ state: (poolSummary.onlineCount > 0 ? 'pass' : 'pending'),
20811
+ text: `${t('wizard.role.client')} · ${t('wizard.overview.clientOnlineCount', { online: poolSummary.onlineCount, total: poolSummary.totalCount })}`,
20812
+ };
20813
+ }
20297
20814
  if (clientView?.state === 'connected') {
20298
20815
  const label = clientView.serverName !== '' ? clientView.serverName : (clientView.serverUrl || t('wizard.role.client'));
20299
20816
  return { state: 'pass', text: `${t('wizard.role.client')} · ${label}` };
@@ -20328,9 +20845,41 @@ function GatewayWizard(props) {
20328
20845
  setStep(targetStep);
20329
20846
  setModalOpen(true);
20330
20847
  };
20331
- return ((0, jsx_runtime_1.jsxs)("div", { "data-zen-remote": "wizard", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-header", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-headerText", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-title", children: t('wizard.title') }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-subtitle", children: t('wizard.subtitle') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-statusBadge", "data-state": statusSummary.state, children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-dot", "data-state": statusSummary.state === 'pass' ? 'pass' : 'pending' }), (0, jsx_runtime_1.jsx)("span", { children: statusSummary.text })] })] }), statusLoaded && viaGateway && !effectiveClient ? (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t('wizard.remoteOnly') }) : null, notWired ? (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", children: t('wizard.notWired') }) : null, configured ? (effectiveClient ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-overviewGrid", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-overviewItem", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewLabel", children: t('wizard.overview.roleLabel') }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewValue", children: t('wizard.role.client') }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewSub", children: clientDeviceName.trim() !== '' ? clientDeviceName : t('settings.client.deviceNameDefault') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-overviewItem", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewLabel", children: t('wizard.overview.clientCredLabel') }), (0, jsx_runtime_1.jsxs)("p", { className: "zr-wizard-overviewValue", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-dot", "data-state": clientLights.credential }), (0, jsx_runtime_1.jsx)("span", { children: roleSnapshot.deviceToken.configured ? t('settings.client.tokenSet') : t('settings.client.statusUnpaired') })] }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewSub", children: t(LIGHT_LABEL[clientLights.credential]) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-overviewItem", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewLabel", children: t('wizard.overview.clientConnLabel') }), (0, jsx_runtime_1.jsxs)("p", { className: "zr-wizard-overviewValue", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-dot", "data-state": clientLights.connection }), (0, jsx_runtime_1.jsx)("span", { children: clientView === undefined
20332
- ? (clientLoading ? t('settings.client.statusRefreshing') : t('settings.client.statusUnpaired'))
20333
- : clientStatusCopy(clientView, now, t) })] }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewSub", children: t(LIGHT_LABEL[clientLights.connection]) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-overviewItem", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewLabel", children: t('wizard.overview.clientServerLabel') }), (0, jsx_runtime_1.jsxs)("p", { className: "zr-wizard-overviewValue", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-dot", "data-state": clientLights.sync }), (0, jsx_runtime_1.jsx)("span", { children: clientView?.serverUrl || clientServerUrl || '—' })] }), (0, jsx_runtime_1.jsxs)("p", { className: "zr-wizard-overviewSub", children: [t('wizard.client.lightSync'), "\uFF1A", t(LIGHT_LABEL[clientLights.sync])] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-footer", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-row", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: clientLoading, onClick: () => { void loadClientStatus(); }, children: clientLoading ? t('settings.client.statusRefreshing') : t('settings.client.statusRefresh') }), clientView?.nextRetryAt !== undefined ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: clientReconnectBusy, onClick: () => { void runClientReconnect(); }, children: clientReconnectBusy ? t('settings.client.reconnectBusy') : t('settings.client.reconnect') })) : null] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-row", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", onClick: () => openWizardAt('pick', { role: 'client' }), children: t('wizard.overview.reconfigure') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", onClick: () => openWizardAt(roleSnapshot.deviceToken.configured ? 'verify' : 'configure', { role: 'client' }), children: t('wizard.overview.clientDetail') })] })] })] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-overviewGrid", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-overviewItem", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewLabel", children: t('wizard.overview.roleLabel') }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewValue", children: t('wizard.role.host') }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewSub", children: hostName.trim() !== '' ? hostName : t('settings.sourceDefault') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-overviewItem", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewLabel", children: t('wizard.overview.gatewayLabel') }), (0, jsx_runtime_1.jsxs)("p", { className: "zr-wizard-overviewValue", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-dot", "data-state": gatewayReachable ? 'pass' : 'fail' }), (0, jsx_runtime_1.jsx)("span", { children: gatewayReachable
20848
+ // MS-8: 「+ 添加服务端」 — a BLANK pairing form on step 2, so a stale
20849
+ // address or code from an earlier pairing can never ride into the new one.
20850
+ const openAddServerWizard = () => {
20851
+ setClientServerUrl('');
20852
+ setClientPairCode('');
20853
+ setClientClaimFail(null);
20854
+ openWizardAt('configure', { role: 'client' });
20855
+ };
20856
+ return ((0, jsx_runtime_1.jsxs)("div", { "data-zen-remote": "wizard", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-header", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-headerText", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-title", children: t('wizard.title') }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-subtitle", children: t('wizard.subtitle') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-statusBadge", "data-state": statusSummary.state, children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-dot", "data-state": statusSummary.state === 'pass' ? 'pass' : 'pending' }), (0, jsx_runtime_1.jsx)("span", { children: statusSummary.text })] })] }), statusLoaded && viaGateway && !effectiveClient ? (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t('wizard.remoteOnly') }) : null, notWired ? (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", children: t('wizard.notWired') }) : null, configured ? (effectiveClient ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-overviewGrid", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-overviewItem", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewLabel", children: t('wizard.overview.roleLabel') }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewValue", children: t('wizard.role.client') }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewSub", children: clientDeviceName.trim() !== '' ? clientDeviceName : t('settings.client.deviceNameDefault') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-overviewItem", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewLabel", children: t('wizard.overview.clientSyncLabel') }), (0, jsx_runtime_1.jsxs)("p", { className: "zr-wizard-overviewValue", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-dot", "data-state": clientLights.sync }), (0, jsx_runtime_1.jsx)("span", { children: clientLights.sync === 'pass'
20857
+ ? t('wizard.overview.syncOk')
20858
+ : clientLights.sync === 'fail'
20859
+ ? t('wizard.overview.syncBad')
20860
+ : t('wizard.overview.syncPending') })] }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewSub", children: t(LIGHT_LABEL[clientLights.sync]) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-overviewItem", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewLabel", children: t('wizard.overview.clientConnLabel') }), (0, jsx_runtime_1.jsxs)("p", { className: "zr-wizard-overviewValue", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-dot", "data-state": poolSummary.connectionLight }), (0, jsx_runtime_1.jsx)("span", { children: t('wizard.overview.clientOnlineCount', { online: poolSummary.onlineCount, total: poolSummary.totalCount }) })] }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewSub", children: poolSummary.totalCount === 0
20861
+ ? t('settings.client.statusUnpaired')
20862
+ : poolSummary.offlineCount === 0 && poolSummary.revokedCount === 0
20863
+ ? t('wizard.overview.clientAllOnline')
20864
+ : [
20865
+ poolSummary.offlineCount > 0 ? t('wizard.overview.clientOfflineCount', { count: poolSummary.offlineCount }) : '',
20866
+ poolSummary.revokedCount > 0 ? t('wizard.overview.clientRevokedCount', { count: poolSummary.revokedCount }) : '',
20867
+ ].filter((line) => line !== '').join(' · ') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-overviewItem", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-overviewHead", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewLabel", children: t('wizard.overview.clientServersLabel') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-linkBtn", onClick: () => {
20868
+ setServerActionFailed(false);
20869
+ setServersModalOpen(true);
20870
+ }, children: t('wizard.overview.manageServers') })] }), (0, jsx_runtime_1.jsxs)("p", { className: "zr-wizard-overviewValue", "data-limit": poolSummary.limitReached ? 'true' : undefined, children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-dot", "data-state": poolSummary.serversLight }), (0, jsx_runtime_1.jsx)("span", { children: poolSummary.limitReached
20871
+ ? t('wizard.overview.clientPairedFull', { count: poolSummary.totalCount, max: poolSummary.maxServers })
20872
+ : t('settings.client.poolCount', { count: poolSummary.totalCount, max: poolSummary.maxServers }) })] }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewSub", "data-limit": poolSummary.limitReached ? 'true' : undefined, children: poolSummary.limitReached
20873
+ ? t('wizard.overview.limitNote', { max: poolSummary.maxServers })
20874
+ : serverRows.length === 0
20875
+ ? t('settings.client.serversEmpty', { max: poolSummary.maxServers })
20876
+ : t('wizard.overview.clientPairedNames', {
20877
+ names: serverRows.map((server) => (server.displayName !== '' ? server.displayName : server.serverUrl)).join('、'),
20878
+ max: poolSummary.maxServers,
20879
+ }) })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-footer", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-row", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: clientLoading, onClick: () => { void loadClientStatus(); }, children: clientLoading ? t('settings.client.statusRefreshing') : t('settings.client.statusRefresh') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", onClick: () => openWizardAt('pick', { role: 'client' }), children: t('wizard.overview.reconfigure') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-row", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", onClick: () => {
20880
+ setServerActionFailed(false);
20881
+ setServersModalOpen(true);
20882
+ }, children: t('wizard.overview.manageServers') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", disabled: poolSummary.limitReached, onClick: openAddServerWizard, children: t('wizard.overview.addServer') })] })] })] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-overviewGrid", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-overviewItem", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewLabel", children: t('wizard.overview.roleLabel') }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewValue", children: t('wizard.role.host') }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewSub", children: hostName.trim() !== '' ? hostName : t('settings.sourceDefault') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-overviewItem", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewLabel", children: t('wizard.overview.gatewayLabel') }), (0, jsx_runtime_1.jsxs)("p", { className: "zr-wizard-overviewValue", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-dot", "data-state": gatewayReachable ? 'pass' : 'fail' }), (0, jsx_runtime_1.jsx)("span", { children: gatewayReachable
20334
20883
  ? t('wizard.overview.gatewayReady', { port: String((0, wizard_ts_1.upstreamPortOf)(wizardStatus)) })
20335
20884
  : t('wizard.overview.gatewayDown') })] }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewSub", children: autoShare ? t('wizard.autoShare') : t('settings.fieldAutoShare') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-overviewItem", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewLabel", children: t('wizard.overview.ingressLabel') }), (0, jsx_runtime_1.jsxs)("p", { className: "zr-wizard-overviewValue", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-dot", "data-state": statusSummary.state === 'pass' ? 'pass' : 'pending' }), (0, jsx_runtime_1.jsx)("span", { children: pathName(path) })] }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewSub", children: resolvedHostEndpoint })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-overviewItem", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-overviewHead", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewLabel", children: t('wizard.overview.devicesLabel') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-linkBtn", disabled: disabled, onClick: () => {
20336
20885
  setDeviceError(false);
@@ -20373,10 +20922,10 @@ function GatewayWizard(props) {
20373
20922
  plan: wizardRole === 'host'
20374
20923
  ? `${t('wizard.role.host')} · ${pathName(path)}`
20375
20924
  : t('wizard.role.client'),
20376
- }) }) }), wizardRole === 'host' ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-formStack", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-field", children: [(0, jsx_runtime_1.jsx)("label", { className: "zr-wizard-label", htmlFor: "zr-wizard-host-name", children: t('wizard.hostName') }), (0, jsx_runtime_1.jsx)("input", { id: "zr-wizard-host-name", className: "zr-wizard-input", type: "text", placeholder: t('wizard.hostNamePlaceholder'), value: hostName, disabled: disabled, onChange: (event) => setHostName(event.target.value) }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.hostNameHint') })] }), (0, jsx_runtime_1.jsxs)("label", { className: "zr-wizard-checkRow", children: [(0, jsx_runtime_1.jsx)("input", { type: "checkbox", checked: autoShare, disabled: disabled, onChange: (event) => setAutoShare(event.target.checked) }), (0, jsx_runtime_1.jsx)("span", { children: t('wizard.autoShare') })] })] }), path === 'lan' ? ((0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-field", children: [(0, jsx_runtime_1.jsx)("label", { className: "zr-wizard-label", htmlFor: "zr-wizard-lan-url", children: t('wizard.lanUrl') }), (0, jsx_runtime_1.jsx)("input", { id: "zr-wizard-lan-url", className: "zr-wizard-input", type: "url", placeholder: t('wizard.lanUrlPlaceholder'), value: serverUrl, disabled: disabled, onChange: (event) => setServerUrl(event.target.value) }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.lanUrlHint') })] })) : path === 'external' ? ((0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-field", children: [(0, jsx_runtime_1.jsx)("label", { className: "zr-wizard-label", htmlFor: "zr-wizard-server-url", children: t('wizard.serverUrl') }), (0, jsx_runtime_1.jsx)("input", { id: "zr-wizard-server-url", className: "zr-wizard-input", type: "url", placeholder: "https://dsh.example.com", value: serverUrl, disabled: disabled, onChange: (event) => setServerUrl(event.target.value) }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.serverUrlHint') }), (0, jsx_runtime_1.jsx)("div", { className: "zr-wizard-row", children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-linkBtn", disabled: disabled, onClick: () => setCard('proxy'), children: t('wizard.proxySnippet') }) })] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-formStack", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-formGrid", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-field", children: [(0, jsx_runtime_1.jsx)("label", { className: "zr-wizard-label", htmlFor: "zr-wizard-domain", children: t('wizard.domain') }), (0, jsx_runtime_1.jsx)("input", { id: "zr-wizard-domain", className: "zr-wizard-input", type: "text", placeholder: "dsh.example.com", value: domain, disabled: disabled, onChange: (event) => setDomain(event.target.value) }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.domainHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-field zr-wizard-fieldPort", children: [(0, jsx_runtime_1.jsx)("label", { className: "zr-wizard-label", htmlFor: "zr-wizard-port", children: t('wizard.httpsPort') }), (0, jsx_runtime_1.jsx)("input", { id: "zr-wizard-port", className: "zr-wizard-input", type: "text", inputMode: "numeric", value: httpsPort, disabled: disabled, onChange: (event) => setHttpsPort(event.target.value) })] })] }), path === 'caddy' && (0, wizard_ts_1.isValidPort)(httpsPort.trim()) && httpsPort.trim() !== '443' ? ((0, jsx_runtime_1.jsx)("div", { className: "zr-wizard-row", children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-linkBtn", disabled: disabled, onClick: () => setCard('portforward'), children: t('wizard.portForwardEntry') }) })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-field", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-labelRow", children: [(0, jsx_runtime_1.jsx)("label", { className: "zr-wizard-label", htmlFor: "zr-wizard-token", children: t('wizard.cfToken') }), tokenSaved ? (0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-savedTag", children: t('wizard.token.saved') }) : null] }), (0, jsx_runtime_1.jsx)("input", { id: "zr-wizard-token", className: "zr-wizard-input", type: "password", placeholder: tokenSaved ? '••••••••••••••••' : '', value: cfToken, disabled: disabled, onChange: (event) => setCfToken(event.target.value) }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.cfTokenHint') }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-row", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-linkBtn", disabled: disabled, onClick: () => setCard('token'), children: t('wizard.cfTokenHowTo') }), path === 'cloudflared' ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-linkBtn", disabled: disabled, onClick: () => setCard('migrate'), children: t('wizard.migrateEntry') })) : null] })] })] })), tokenNotice === 'failed' ? ((0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t('wizard.token.saveFailed') })) : null, formError !== null ? ((0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t(formError === 'bad-domain' ? 'wizard.badDomain' : formError === 'bad-port' ? 'wizard.badPort' : formError === 'missing-token' ? 'wizard.missingToken' : 'wizard.badServerUrl') })) : null] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-formStack", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-field", children: [(0, jsx_runtime_1.jsx)("label", { className: "zr-wizard-label", htmlFor: "zr-wizard-client-device", children: t('wizard.client.deviceName') }), (0, jsx_runtime_1.jsx)("input", { id: "zr-wizard-client-device", className: "zr-wizard-input", type: "text", placeholder: t('wizard.client.deviceNamePlaceholder'), value: clientDeviceName, disabled: clientClaimBusy, onChange: (event) => setClientDeviceName(event.target.value) }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.client.deviceNameHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-field", children: [(0, jsx_runtime_1.jsx)("label", { className: "zr-wizard-label", htmlFor: "zr-wizard-client-url", children: t('wizard.client.serverUrl') }), (0, jsx_runtime_1.jsx)("input", { id: "zr-wizard-client-url", className: "zr-wizard-input", type: "url", placeholder: t('wizard.client.serverUrlPlaceholder'), value: clientServerUrl, disabled: clientClaimBusy, onChange: (event) => setClientServerUrl(event.target.value) }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.client.serverUrlHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-field", children: [(0, jsx_runtime_1.jsx)("label", { className: "zr-wizard-label", htmlFor: "zr-wizard-client-code", children: t('wizard.client.pairingCode') }), (0, jsx_runtime_1.jsx)("input", { id: "zr-wizard-client-code", className: "zr-wizard-input", type: "text", autoComplete: "off", spellCheck: false, placeholder: t('wizard.client.pairingCodePlaceholder'), value: clientPairCode, disabled: clientClaimBusy, onChange: (event) => setClientPairCode((0, settings_form_ts_1.normalizePairingCode)(event.target.value)) }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.client.pairingCodeHint') })] })] }), formError === 'bad-client-url' ? ((0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t('settings.client.failInvalid') })) : null, formError === 'bad-client-code' ? ((0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t('settings.client.failBadCode') })) : null, clientClaimFail !== null ? ((0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: claimFailCopy(clientClaimFail.code, clientClaimFail.retryAfterMs, t) })) : null] }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-modalFooter", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: disabled || savingConfig || clientClaimBusy, onClick: () => {
20925
+ }) }) }), wizardRole === 'host' ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-formStack", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-field", children: [(0, jsx_runtime_1.jsx)("label", { className: "zr-wizard-label", htmlFor: "zr-wizard-host-name", children: t('wizard.hostName') }), (0, jsx_runtime_1.jsx)("input", { id: "zr-wizard-host-name", className: "zr-wizard-input", type: "text", placeholder: t('wizard.hostNamePlaceholder'), value: hostName, disabled: disabled, onChange: (event) => setHostName(event.target.value) }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.hostNameHint') })] }), (0, jsx_runtime_1.jsxs)("label", { className: "zr-wizard-checkRow", children: [(0, jsx_runtime_1.jsx)("input", { type: "checkbox", checked: autoShare, disabled: disabled, onChange: (event) => setAutoShare(event.target.checked) }), (0, jsx_runtime_1.jsx)("span", { children: t('wizard.autoShare') })] })] }), path === 'lan' ? ((0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-field", children: [(0, jsx_runtime_1.jsx)("label", { className: "zr-wizard-label", htmlFor: "zr-wizard-lan-url", children: t('wizard.lanUrl') }), (0, jsx_runtime_1.jsx)("input", { id: "zr-wizard-lan-url", className: "zr-wizard-input", type: "url", placeholder: t('wizard.lanUrlPlaceholder'), value: serverUrl, disabled: disabled, onChange: (event) => setServerUrl(event.target.value) }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.lanUrlHint') })] })) : path === 'external' ? ((0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-field", children: [(0, jsx_runtime_1.jsx)("label", { className: "zr-wizard-label", htmlFor: "zr-wizard-server-url", children: t('wizard.serverUrl') }), (0, jsx_runtime_1.jsx)("input", { id: "zr-wizard-server-url", className: "zr-wizard-input", type: "url", placeholder: "https://dsh.example.com", value: serverUrl, disabled: disabled, onChange: (event) => setServerUrl(event.target.value) }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.serverUrlHint') }), (0, jsx_runtime_1.jsx)("div", { className: "zr-wizard-row", children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-linkBtn", disabled: disabled, onClick: () => setCard('proxy'), children: t('wizard.proxySnippet') }) })] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-formStack", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-formGrid", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-field", children: [(0, jsx_runtime_1.jsx)("label", { className: "zr-wizard-label", htmlFor: "zr-wizard-domain", children: t('wizard.domain') }), (0, jsx_runtime_1.jsx)("input", { id: "zr-wizard-domain", className: "zr-wizard-input", type: "text", placeholder: "dsh.example.com", value: domain, disabled: disabled, onChange: (event) => setDomain(event.target.value) }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.domainHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-field zr-wizard-fieldPort", children: [(0, jsx_runtime_1.jsx)("label", { className: "zr-wizard-label", htmlFor: "zr-wizard-port", children: t('wizard.httpsPort') }), (0, jsx_runtime_1.jsx)("input", { id: "zr-wizard-port", className: "zr-wizard-input", type: "text", inputMode: "numeric", value: httpsPort, disabled: disabled, onChange: (event) => setHttpsPort(event.target.value) })] })] }), path === 'caddy' && (0, wizard_ts_1.isValidPort)(httpsPort.trim()) && httpsPort.trim() !== '443' ? ((0, jsx_runtime_1.jsx)("div", { className: "zr-wizard-row", children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-linkBtn", disabled: disabled, onClick: () => setCard('portforward'), children: t('wizard.portForwardEntry') }) })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-field", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-labelRow", children: [(0, jsx_runtime_1.jsx)("label", { className: "zr-wizard-label", htmlFor: "zr-wizard-token", children: t('wizard.cfToken') }), tokenSaved ? (0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-savedTag", children: t('wizard.token.saved') }) : null] }), (0, jsx_runtime_1.jsx)("input", { id: "zr-wizard-token", className: "zr-wizard-input", type: "password", placeholder: tokenSaved ? '••••••••••••••••' : '', value: cfToken, disabled: disabled, onChange: (event) => setCfToken(event.target.value) }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.cfTokenHint') }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-row", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-linkBtn", disabled: disabled, onClick: () => setCard('token'), children: t('wizard.cfTokenHowTo') }), path === 'cloudflared' ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-linkBtn", disabled: disabled, onClick: () => setCard('migrate'), children: t('wizard.migrateEntry') })) : null] })] })] })), tokenNotice === 'failed' ? ((0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t('wizard.token.saveFailed') })) : null, formError !== null ? ((0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t(formError === 'bad-domain' ? 'wizard.badDomain' : formError === 'bad-port' ? 'wizard.badPort' : formError === 'missing-token' ? 'wizard.missingToken' : 'wizard.badServerUrl') })) : null] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", children: t('wizard.client.poolCapNote', { count: serverRows.length, max: poolSummary.maxServers }) }), poolSummary.limitReached ? ((0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t('wizard.overview.limitNote', { max: poolSummary.maxServers }) })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-formStack", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-field", children: [(0, jsx_runtime_1.jsx)("label", { className: "zr-wizard-label", htmlFor: "zr-wizard-client-device", children: t('wizard.client.deviceName') }), (0, jsx_runtime_1.jsx)("input", { id: "zr-wizard-client-device", className: "zr-wizard-input", type: "text", placeholder: t('wizard.client.deviceNamePlaceholder'), value: clientDeviceName, disabled: clientClaimBusy, onChange: (event) => setClientDeviceName(event.target.value) }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.client.deviceNameHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-field", children: [(0, jsx_runtime_1.jsx)("label", { className: "zr-wizard-label", htmlFor: "zr-wizard-client-url", children: t('wizard.client.serverUrl') }), (0, jsx_runtime_1.jsx)("input", { id: "zr-wizard-client-url", className: "zr-wizard-input", type: "url", placeholder: t('wizard.client.serverUrlPlaceholder'), value: clientServerUrl, disabled: clientClaimBusy, onChange: (event) => setClientServerUrl(event.target.value) }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.client.serverUrlHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-field", children: [(0, jsx_runtime_1.jsx)("label", { className: "zr-wizard-label", htmlFor: "zr-wizard-client-code", children: t('wizard.client.pairingCode') }), (0, jsx_runtime_1.jsx)("input", { id: "zr-wizard-client-code", className: "zr-wizard-input", type: "text", autoComplete: "off", spellCheck: false, placeholder: t('wizard.client.pairingCodePlaceholder'), value: clientPairCode, disabled: clientClaimBusy, onChange: (event) => setClientPairCode((0, settings_form_ts_1.normalizePairingCode)(event.target.value)) }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.client.pairingCodeHint') })] })] }), formError === 'bad-client-url' ? ((0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t('settings.client.failInvalid') })) : null, formError === 'bad-client-code' ? ((0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t('settings.client.failBadCode') })) : null, clientClaimFail !== null ? ((0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: claimFailCopy(clientClaimFail.code, clientClaimFail.retryAfterMs, t) })) : null] }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-modalFooter", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: disabled || savingConfig || clientClaimBusy, onClick: () => {
20377
20926
  setFormError(null);
20378
20927
  setStep('pick');
20379
- }, children: t('wizard.back') }), wizardRole === 'host' ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", disabled: disabled || savingConfig, onClick: () => { void advanceFromConfigure(); }, children: t('wizard.modal.saveAndNext') })) : ((0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-row", children: [roleSnapshot.deviceToken.configured ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: clientClaimBusy, onClick: () => setStep('verify'), children: t('wizard.client.skipToStatus') })) : null, (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", disabled: clientClaimBusy, onClick: () => { void runClientClaim(); }, children: clientClaimBusy ? t('wizard.client.pairingBusy') : t('wizard.client.pairAndConnect') })] }))] })] })) : null, step === 'verify' ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-modalBody", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-stepIntro", children: t('wizard.modal.step3Desc') }), wizardRole === 'host' ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [caddyPath ? ((0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-serviceBar", "data-zen-remote": "wizard-caddy-controls", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-serviceInfo", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-dot", "data-state": caddyState === 'running' ? 'pass' : 'pending' }), (0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-label", children: t('wizard.ingress') }), (0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-hint", children: caddyBusy
20928
+ }, children: t('wizard.back') }), wizardRole === 'host' ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", disabled: disabled || savingConfig, onClick: () => { void advanceFromConfigure(); }, children: t('wizard.modal.saveAndNext') })) : ((0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-row", children: [roleSnapshot.deviceToken.configured ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: clientClaimBusy, onClick: () => setStep('verify'), children: t('wizard.client.skipToStatus') })) : null, (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", disabled: clientClaimBusy || claimGate.blockedByLimit, onClick: () => { void runClientClaim(); }, children: clientClaimBusy ? t('wizard.client.pairingBusy') : t('wizard.client.pairAndConnect') })] }))] })] })) : null, step === 'verify' ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-modalBody", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-stepIntro", children: t('wizard.modal.step3Desc') }), wizardRole === 'host' ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [caddyPath ? ((0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-serviceBar", "data-zen-remote": "wizard-caddy-controls", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-serviceInfo", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-dot", "data-state": caddyState === 'running' ? 'pass' : 'pending' }), (0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-label", children: t('wizard.ingress') }), (0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-hint", children: caddyBusy
20380
20929
  ? t('wizard.caddy.starting')
20381
20930
  : t(caddyState === 'running' ? 'wizard.caddy.running' : caddyState === 'stopped' ? 'wizard.caddy.stopped' : 'wizard.caddy.neverStarted') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-row", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", disabled: disabled || caddyBusy || caddyState === 'running', onClick: () => { void startCaddy(); }, children: caddyBusy ? t('wizard.caddy.starting') : t('wizard.caddy.start') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: disabled || caddyBusy || caddyState !== 'running', onClick: () => { void stopCaddy(); }, children: t('wizard.caddy.stop') })] })] })) : null, tunnelPath ? ((0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-serviceBar", "data-zen-remote": "wizard-tunnel-controls", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-serviceInfo", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-dot", "data-state": tunnelState === 'running' ? 'pass' : 'pending' }), (0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-label", children: t('wizard.ingress') }), (0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-hint", children: t(tunnelState === 'running' ? 'wizard.tunnel.running' : tunnelState === 'stopped' ? 'wizard.tunnel.stopped' : 'wizard.tunnel.neverStarted') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-row", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", disabled: disabled || tunnelBusy, onClick: () => { void setupTunnelNow(adoptTunnelId); }, children: tunnelBusy ? t('wizard.tunnel.checking') : tunnelState === 'running' ? t('wizard.reverify') : t('wizard.tunnel.setup') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: disabled || tunnelState !== 'running', onClick: () => {
20382
20931
  void (async () => {
@@ -20391,7 +20940,7 @@ function GatewayWizard(props) {
20391
20940
  return (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": copy.tone === 'error' ? 'error' : undefined, children: copy.key === 'wizard.check.failed'
20392
20941
  ? t('wizard.check.failed', { reason: tunnelCheck.ok ? '' : String(tunnelCheck.reason) })
20393
20942
  : t(copy.key) });
20394
- })() : null, tunnelPath && tunnelResult !== undefined && !tunnelResult.ok && tunnelResult.reason === 'tunnel-conflict' && tunnelResult.existing !== undefined ? ((0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-field", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-label", children: t('wizard.tunnel.conflict') }), (0, jsx_runtime_1.jsx)("div", { className: "zr-wizard-row", children: tunnelResult.existing.map((tunnel) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "zr-wizard-linkBtn", disabled: disabled || tunnelBusy, onClick: () => { setAdoptTunnelId(tunnel.id); void setupTunnelNow(tunnel.id); }, children: [t('wizard.tunnel.useExisting'), "\uFF1A", tunnel.name, "\uFF08", tunnel.status, "\uFF09"] }, tunnel.id))) })] })) : null, tunnelPath && tunnelResult !== undefined && tunnelResult.ok ? (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", children: t('wizard.tunnel.ok') }) : null, tunnelPath && tunnelResult !== undefined && !tunnelResult.ok && tunnelResult.reason !== 'tunnel-conflict' ? (() => {
20943
+ })() : null, tunnelPath && tunnelResult !== undefined && !tunnelResult.ok && tunnelResult.reason === 'tunnel-conflict' && tunnelResult.existing !== undefined ? ((0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-field", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-label", children: t('wizard.tunnel.conflict') }), (0, jsx_runtime_1.jsx)("div", { className: "zr-wizard-row", children: tunnelResult.existing.map((tunnel) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "zr-wizard-linkBtn", disabled: disabled || tunnelBusy, onClick: () => { setAdoptTunnelId(tunnel.id); void setupTunnelNow(tunnel.id); }, children: [t('wizard.tunnel.useExisting'), ": ", tunnel.name, " (", tunnel.status, ")"] }, tunnel.id))) })] })) : null, tunnelPath && tunnelResult !== undefined && tunnelResult.ok ? (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", children: t('wizard.tunnel.ok') }) : null, tunnelPath && tunnelResult !== undefined && !tunnelResult.ok && tunnelResult.reason !== 'tunnel-conflict' ? (() => {
20395
20944
  const copy = (0, wizard_ts_1.tunnelFailureCopy)(tunnelResult.reason, tunnelResult.detail);
20396
20945
  return (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t(copy.key, copy.params) });
20397
20946
  })() : null, caddyPath && caddyStartError !== undefined ? ((0, jsx_runtime_1.jsxs)("p", { className: "zr-wizard-notice", "data-tone": "error", children: [t('wizard.caddy.startFailed', { reason: t(caddyStartError.key, caddyStartError.params) }), caddyStartError.installUrl !== undefined ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [" ", (0, jsx_runtime_1.jsx)("a", { className: "zr-wizard-link", href: caddyStartError.installUrl, target: "_blank", rel: "noreferrer", children: caddyStartError.installUrl })] })) : null] })) : null, (0, jsx_runtime_1.jsx)("ul", { className: "zr-wizard-lights", children: LIGHT_ORDER.map(({ key, light }) => {
@@ -20403,9 +20952,17 @@ function GatewayWizard(props) {
20403
20952
  }) }), startResult !== undefined && !startResult.ok ? (() => {
20404
20953
  const copy = startFailureCopy(startResult);
20405
20954
  return (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t('wizard.startFailed', { reason: t(copy.key, copy.params) }) });
20406
- })() : null, (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-targetTabs", role: "tablist", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", role: "tab", "aria-selected": pairTarget === 'web', className: "zr-wizard-targetTab", "data-selected": pairTarget === 'web', disabled: disabled, onClick: () => setPairTarget('web'), children: t('wizard.pairTarget.web') }), (0, jsx_runtime_1.jsx)("button", { type: "button", role: "tab", "aria-selected": pairTarget === 'desktop-client', className: "zr-wizard-targetTab", "data-selected": pairTarget === 'desktop-client', disabled: disabled, onClick: () => setPairTarget('desktop-client'), children: t('wizard.pairTarget.desktop') })] }), pairTarget === 'web' ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [webPairRequested && startResult?.ok === true && startResult.pairingCode !== '' ? ((0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-pairPanel", children: [(0, jsx_runtime_1.jsx)("svg", { className: "zr-wizard-qr", "data-usable": qr !== null, role: "img", "aria-label": t('wizard.qrHint'), shapeRendering: "crispEdges", viewBox: qr === null ? undefined : `-16 -16 ${(qr.length + 8) * 4} ${(qr.length + 8) * 4}`, children: qr === null ? null : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("rect", { x: -16, y: -16, width: (qr.length + 8) * 4, height: (qr.length + 8) * 4, fill: "#fff" }), (0, jsx_runtime_1.jsx)("path", { d: qrSvgPath(qr, 4), fill: "#000" })] })) }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-pairMeta", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-label", children: t('wizard.pairingCode') }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-code", children: startResult.pairingCode }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-countdown", children: t('wizard.pairingSeconds', { seconds: String((0, wizard_ts_1.pairingSecondsLeft)(startResult.expiresAt, now)) }) }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.qrHint') })] })] })) : null, poll?.result === 'pass' ? (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "ok", children: t('wizard.verifyPass', { deviceId: poll.deviceId }) }) : null, poll?.result === 'cookie-insecure' ? (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t('wizard.verifyCookieInsecure') }) : null, poll?.result === 'code-expired' ? (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t('wizard.verifyExpired') }) : null, poll?.result === 'stale' ? (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t('wizard.verifyStale') }) : null, webPairRequested && (poll?.result === 'pending' || poll?.result === 'not-started') ? (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.verifyPending') }) : null, !webPairRequested && poll === undefined ? (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.verifyNotStarted') }) : null] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-desktopBox", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.desktop.guide') }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-field", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-label", children: t('wizard.desktop.addressLabel') }), (0, jsx_runtime_1.jsx)("code", { className: "zr-wizard-input", children: resolvedHostEndpoint })] }), desktopPairing !== undefined && desktopPairing.expiresAt > now ? ((0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-pairMeta", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-label", children: t('wizard.pairingCode') }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-code", children: desktopPairing.code }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-countdown", children: t('wizard.pairingSeconds', { seconds: String((0, wizard_ts_1.pairingSecondsLeft)(desktopPairing.expiresAt, now)) }) })] })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-row", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", disabled: disabled || desktopPairBusy, onClick: () => { void runDesktopPair(); }, children: desktopPairing !== undefined && desktopPairing.expiresAt > now ? t('wizard.desktop.regenerate') : t('wizard.desktop.generate') }), desktopPairing !== undefined && desktopPairing.expiresAt > now ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: disabled, onClick: () => { void copyDesktopInfo(); }, children: desktopCopied ? t('wizard.card.copied') : t('wizard.desktop.copyInfo') })) : null] })] }))] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("ul", { className: "zr-wizard-lights", children: CLIENT_LIGHT_ORDER.map(({ key, light }) => ((0, jsx_runtime_1.jsxs)("li", { className: "zr-wizard-light", "data-state": clientLights[light], children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-lightMain", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-dot", "data-state": clientLights[light] }), (0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-lightName", children: t(key) })] }), (0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-lightState", "data-state": clientLights[light], children: t(LIGHT_LABEL[clientLights[light]]) })] }, key))) }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-clientBox", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-label", children: t('settings.client.statusTitle') }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": clientView?.state === 'connected' ? 'ok' : clientLights.connection === 'fail' ? 'error' : undefined, children: clientView === undefined
20407
- ? (clientLoading ? t('settings.client.statusRefreshing') : t('settings.client.statusUnpaired'))
20408
- : clientStatusCopy(clientView, now, t) }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-row", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: clientLoading, onClick: () => { void loadClientStatus(); }, children: clientLoading ? t('settings.client.statusRefreshing') : t('settings.client.statusRefresh') }), clientView?.nextRetryAt !== undefined ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", disabled: clientReconnectBusy, onClick: () => { void runClientReconnect(); }, children: clientReconnectBusy ? t('settings.client.reconnectBusy') : t('settings.client.reconnect') })) : null, roleSnapshot.deviceToken.configured ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: clientUnpairBusy, onClick: () => { void runClientUnpair(); }, children: clientUnpairBusy ? t('settings.client.unpairBusy') : t('settings.client.unpair') })) : null] })] })] }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-modalFooter", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: disabled, onClick: () => setStep('configure'), children: t('wizard.back') }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-row", children: [wizardRole === 'host' && resolvedVerifyUrl !== '' ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: disabled || probingUrl, onClick: () => { void probeAccessUrl(resolvedVerifyUrl); }, children: probingUrl ? t('wizard.light.probing') : t('wizard.devicesModal.retest') })) : null, wizardRole === 'host' && pairTarget === 'web' ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", disabled: disabled || probingUrl, onClick: () => { void startVerification(); }, children: verified ? t('wizard.reverify') : t('wizard.startVerify') })) : null, wizardRole === 'host' && pairTarget === 'desktop-client' ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", disabled: disabled || desktopPairBusy, onClick: () => { void runDesktopPair(); }, children: desktopPairing !== undefined && desktopPairing.expiresAt > now ? t('wizard.desktop.regenerate') : t('wizard.desktop.generate') })) : null, (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", onClick: () => setModalOpen(false), children: t('wizard.modal.finish') })] })] })] })) : null] }) })) : null, devicesModalOpen ? ((0, jsx_runtime_1.jsx)("div", { "data-zen-remote": "wizard-modal", onClick: () => {
20955
+ })() : null, (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-targetTabs", role: "tablist", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", role: "tab", "aria-selected": pairTarget === 'web', className: "zr-wizard-targetTab", "data-selected": pairTarget === 'web', disabled: disabled, onClick: () => setPairTarget('web'), children: t('wizard.pairTarget.web') }), (0, jsx_runtime_1.jsx)("button", { type: "button", role: "tab", "aria-selected": pairTarget === 'desktop-client', className: "zr-wizard-targetTab", "data-selected": pairTarget === 'desktop-client', disabled: disabled, onClick: () => setPairTarget('desktop-client'), children: t('wizard.pairTarget.desktop') })] }), pairTarget === 'web' ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [webPairRequested && startResult?.ok === true && startResult.pairingCode !== '' ? ((0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-pairPanel", children: [(0, jsx_runtime_1.jsx)("svg", { className: "zr-wizard-qr", "data-usable": qr !== null, role: "img", "aria-label": t('wizard.qrHint'), shapeRendering: "crispEdges", viewBox: qr === null ? undefined : `-16 -16 ${(qr.length + 8) * 4} ${(qr.length + 8) * 4}`, children: qr === null ? null : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("rect", { x: -16, y: -16, width: (qr.length + 8) * 4, height: (qr.length + 8) * 4, fill: "#fff" }), (0, jsx_runtime_1.jsx)("path", { d: qrSvgPath(qr, 4), fill: "#000" })] })) }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-pairMeta", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-label", children: t('wizard.pairingCode') }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-code", children: startResult.pairingCode }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-countdown", children: t('wizard.pairingSeconds', { seconds: String((0, wizard_ts_1.pairingSecondsLeft)(startResult.expiresAt, now)) }) }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.qrHint') })] })] })) : null, poll?.result === 'pass' ? (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "ok", children: t('wizard.verifyPass', { deviceId: poll.deviceId }) }) : null, poll?.result === 'cookie-insecure' ? (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t('wizard.verifyCookieInsecure') }) : null, poll?.result === 'code-expired' ? (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t('wizard.verifyExpired') }) : null, poll?.result === 'stale' ? (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t('wizard.verifyStale') }) : null, webPairRequested && (poll?.result === 'pending' || poll?.result === 'not-started') ? (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.verifyPending') }) : null, !webPairRequested && poll === undefined ? (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.verifyNotStarted') }) : null] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-desktopBox", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-hint", children: t('wizard.desktop.guide') }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-field", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-label", children: t('wizard.desktop.addressLabel') }), (0, jsx_runtime_1.jsx)("code", { className: "zr-wizard-input", children: resolvedHostEndpoint })] }), desktopPairing !== undefined && desktopPairing.expiresAt > now ? ((0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-pairMeta", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-label", children: t('wizard.pairingCode') }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-code", children: desktopPairing.code }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-countdown", children: t('wizard.pairingSeconds', { seconds: String((0, wizard_ts_1.pairingSecondsLeft)(desktopPairing.expiresAt, now)) }) })] })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-row", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", disabled: disabled || desktopPairBusy, onClick: () => { void runDesktopPair(); }, children: desktopPairing !== undefined && desktopPairing.expiresAt > now ? t('wizard.desktop.regenerate') : t('wizard.desktop.generate') }), desktopPairing !== undefined && desktopPairing.expiresAt > now ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: disabled, onClick: () => { void copyDesktopInfo(); }, children: desktopCopied ? t('wizard.card.copied') : t('wizard.desktop.copyInfo') })) : null] })] }))] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [serverRows.length > 1 ? ((0, jsx_runtime_1.jsx)("div", { className: "zr-wizard-targetTabs", role: "tablist", "aria-label": t('wizard.client.serverPicker'), children: serverRows.map((server) => {
20956
+ const selected = activeServer?.entryId === server.entryId;
20957
+ return ((0, jsx_runtime_1.jsx)("button", { type: "button", role: "tab", "aria-selected": selected, className: "zr-wizard-targetTab", "data-selected": selected, onClick: () => setActiveClientEntryId(server.entryId), children: server.displayName !== '' ? server.displayName : server.serverUrl }, server.entryId));
20958
+ }) })) : null, activeServer !== undefined ? ((0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-serviceInfo", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-dot", "data-state": serverDotState(activeServer.state) }), (0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-lightName", children: activeServer.displayName !== '' ? activeServer.displayName : activeServer.serverUrl }), (0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-hint", children: activeServer.serverUrl })] })) : null, (0, jsx_runtime_1.jsx)("ul", { className: "zr-wizard-lights", children: CLIENT_LIGHT_ORDER.map(({ key, light }) => ((0, jsx_runtime_1.jsxs)("li", { className: "zr-wizard-light", "data-state": serverLights[light], children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-lightMain", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-dot", "data-state": serverLights[light] }), (0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-lightName", children: t(key) })] }), (0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-lightState", "data-state": serverLights[light], children: t(LIGHT_LABEL[serverLights[light]]) })] }, key))) }), activeServer !== undefined ? ((0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-row", children: [activeServer.nextRetryAt !== undefined ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", disabled: serverBusyEntry !== null, onClick: () => { void runServerAction(settings_form_ts_1.CLIENT_RECONNECT_ROUTE, activeServer.entryId, { entryId: activeServer.entryId }); }, children: serverBusyEntry === activeServer.entryId ? t('settings.client.reconnectBusy') : t('settings.client.reconnect') })) : null, (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: serverBusyEntry !== null, onClick: () => {
20959
+ if (!window.confirm(t('settings.client.unpairServerConfirm')))
20960
+ return;
20961
+ const entryId = activeServer.entryId;
20962
+ void runServerAction(settings_form_ts_1.CLIENT_UNPAIR_ROUTE, entryId, { entryId }).then(() => {
20963
+ setActiveClientEntryId((current) => (current === entryId ? undefined : current));
20964
+ });
20965
+ }, children: t('settings.client.serverRemove') })] })) : null, serverRows.length === 0 && roleSnapshot.deviceToken.configured ? ((0, jsx_runtime_1.jsx)("div", { className: "zr-wizard-row", children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: clientUnpairBusy, onClick: () => { void runClientUnpair(); }, children: clientUnpairBusy ? t('settings.client.unpairBusy') : t('settings.client.unpair') }) })) : null] }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-modalFooter", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: disabled, onClick: () => setStep('configure'), children: t('wizard.back') }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-row", children: [wizardRole === 'host' && resolvedVerifyUrl !== '' ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: disabled || probingUrl, onClick: () => { void probeAccessUrl(resolvedVerifyUrl); }, children: probingUrl ? t('wizard.light.probing') : t('wizard.devicesModal.retest') })) : null, wizardRole === 'host' && pairTarget === 'web' ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", disabled: disabled || probingUrl, onClick: () => { void startVerification(); }, children: verified ? t('wizard.reverify') : t('wizard.startVerify') })) : null, wizardRole === 'host' && pairTarget === 'desktop-client' ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", disabled: disabled || desktopPairBusy, onClick: () => { void runDesktopPair(); }, children: desktopPairing !== undefined && desktopPairing.expiresAt > now ? t('wizard.desktop.regenerate') : t('wizard.desktop.generate') })) : null, wizardRole === 'client' && !poolSummary.limitReached ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", onClick: openAddServerWizard, children: t('wizard.client.continueAdd') })) : null, (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", onClick: () => setModalOpen(false), children: t('wizard.modal.finish') })] })] })] })) : null] }) })) : null, devicesModalOpen ? ((0, jsx_runtime_1.jsx)("div", { "data-zen-remote": "wizard-modal", onClick: () => {
20409
20966
  if (!deviceBusy)
20410
20967
  setDevicesModalOpen(false);
20411
20968
  }, children: (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-modalDialog", role: "dialog", "aria-modal": "true", "aria-label": t('wizard.devicesModal.title'), onClick: (event) => event.stopPropagation(), children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-modalHeader", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-headerText", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-modalTitle", children: t('wizard.devicesModal.title') }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-subtitle", children: t('wizard.devicesModal.subtitle') })] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-cardCloseIcon", "aria-label": t('wizard.card.close'), disabled: deviceBusy, onClick: () => setDevicesModalOpen(false), children: "\u00D7" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-modalBody", children: [deviceError ? ((0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t('settings.actionFail') })) : null, allDevices.length === 0 ? ((0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", children: t('settings.devicesEmpty') })) : ((0, jsx_runtime_1.jsx)("ul", { className: "zr-wizard-deviceList", children: allDevices.map((device) => ((0, jsx_runtime_1.jsxs)("li", { className: "zr-wizard-deviceItem", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-deviceHead", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-deviceTitleRow", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-lightName", children: device.name }), (0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-deviceBadge", children: device.role === 'web' ? t('settings.deviceRoleWeb') : t('settings.deviceRoleDesktop') }), device.role === 'web' ? ((0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-deviceBadge", children: device.hasPush ? t('settings.devicePushOn') : t('settings.devicePushOff') })) : null] }), (0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-hint", children: t('settings.deviceLastSeen', { time: new Date(device.lastSeen).toLocaleString() }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-deviceActions", children: [device.role === 'web' ? ((0, jsx_runtime_1.jsxs)("select", { className: "zr-wizard-select", "aria-label": t('settings.deviceKindSelect', { name: device.name }), disabled: disabled || deviceBusy, value: device.kind, onChange: (event) => {
@@ -20440,7 +20997,43 @@ function GatewayWizard(props) {
20440
20997
  }, children: t('wizard.overview.pairDesktop') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", disabled: disabled || deviceBusy, onClick: () => {
20441
20998
  setDevicesModalOpen(false);
20442
20999
  openWizardAt('verify', { role: 'host', target: 'web' });
20443
- }, children: t('wizard.overview.pairPhone') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: deviceBusy, onClick: () => setDevicesModalOpen(false), children: t('wizard.card.close') })] })] })] }) })) : null, card === 'proxy' ? ((0, jsx_runtime_1.jsx)(ProxyCard_tsx_1.ProxyCard, { t: t, domain: (() => {
21000
+ }, children: t('wizard.overview.pairPhone') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: deviceBusy, onClick: () => setDevicesModalOpen(false), children: t('wizard.card.close') })] })] })] }) })) : null, serversModalOpen ? ((0, jsx_runtime_1.jsx)("div", { "data-zen-remote": "wizard-modal", onClick: () => {
21001
+ if (serverBusyEntry === null)
21002
+ setServersModalOpen(false);
21003
+ }, children: (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-modalDialog", role: "dialog", "aria-modal": "true", "aria-label": t('wizard.serversModal.title'), onClick: (event) => event.stopPropagation(), children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-modalHeader", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-headerText", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-modalTitle", children: t('wizard.serversModal.title') }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-subtitle", "data-limit": poolSummary.limitReached ? 'true' : undefined, children: t('wizard.serversModal.subtitle', { count: serverRows.length, max: poolSummary.maxServers }) })] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-cardCloseIcon", "aria-label": t('wizard.card.close'), disabled: serverBusyEntry !== null, onClick: () => setServersModalOpen(false), children: "\u00D7" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-modalBody", children: [poolSummary.limitReached ? ((0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t('wizard.overview.limitNote', { max: poolSummary.maxServers }) })) : null, serverActionFailed ? ((0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t('settings.actionFail') })) : null, serverRows.length === 0 ? ((0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", children: t('settings.client.serversEmpty', { max: poolSummary.maxServers }) })) : ((0, jsx_runtime_1.jsx)("ul", { className: "zr-wizard-deviceList", children: serverRows.map((server) => {
21004
+ const badge = serverBadgeCopy(server, now, t);
21005
+ const name = server.displayName !== '' ? server.displayName : server.serverUrl;
21006
+ return ((0, jsx_runtime_1.jsxs)("li", { className: "zr-wizard-deviceItem", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-deviceHead", children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-deviceTitleRow", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-dot", "data-state": serverDotState(server.state) }), (0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-lightName", children: name }), (0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-deviceBadge", "data-tone": badge.tone, children: badge.text })] }), (0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-hint", children: server.serverUrl })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-deviceActions", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-linkBtn", disabled: serverBusyEntry !== null, onClick: () => {
21007
+ setServersModalOpen(false);
21008
+ setActiveClientEntryId(server.entryId);
21009
+ openWizardAt('verify', { role: 'client' });
21010
+ }, children: t('wizard.serversModal.viewStatus') }), server.nextRetryAt !== undefined ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-linkBtn", disabled: serverBusyEntry !== null, onClick: () => { void runServerAction(settings_form_ts_1.CLIENT_RECONNECT_ROUTE, server.entryId, { entryId: server.entryId }); }, children: serverBusyEntry === server.entryId ? t('settings.client.reconnectBusy') : t('settings.client.reconnect') })) : null, (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-linkBtn", disabled: serverBusyEntry !== null, onClick: () => {
21011
+ // 覆盖重配 pre-fills the entry's own address, so
21012
+ // it stays usable at a FULL pool (an already-paired
21013
+ // address never trips the cap).
21014
+ setServersModalOpen(false);
21015
+ setClientServerUrl(server.serverUrl);
21016
+ setClientPairCode('');
21017
+ setClientClaimFail(null);
21018
+ openWizardAt('configure', { role: 'client' });
21019
+ }, children: t('settings.client.repair') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-linkBtn", disabled: serverBusyEntry !== null, onClick: () => {
21020
+ if (!window.confirm(t('settings.client.unpairServerConfirm')))
21021
+ return;
21022
+ const entryId = server.entryId;
21023
+ void runServerAction(settings_form_ts_1.CLIENT_UNPAIR_ROUTE, entryId, { entryId }).then(() => {
21024
+ setActiveClientEntryId((current) => (current === entryId ? undefined : current));
21025
+ });
21026
+ }, children: t('settings.client.serverRemove') })] })] }, server.entryId));
21027
+ }) }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-modalFooter", children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-wizard-row", children: serverRows.length > 0 ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: serverBusyEntry !== null, onClick: () => {
21028
+ if (window.confirm(t('settings.client.unpairAllServersConfirm'))) {
21029
+ void runServerAction(settings_form_ts_1.CLIENT_UNPAIR_ROUTE, 'all', { all: true }).then(() => {
21030
+ setActiveClientEntryId(undefined);
21031
+ });
21032
+ }
21033
+ }, children: t('settings.client.unpairAllServers') })) : null }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-row", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", disabled: poolSummary.limitReached || serverBusyEntry !== null, onClick: () => {
21034
+ setServersModalOpen(false);
21035
+ openAddServerWizard();
21036
+ }, children: t('wizard.overview.addServer') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: serverBusyEntry !== null, onClick: () => setServersModalOpen(false), children: t('wizard.card.close') })] })] })] }) })) : null, card === 'proxy' ? ((0, jsx_runtime_1.jsx)(ProxyCard_tsx_1.ProxyCard, { t: t, domain: (() => {
20444
21037
  if (domain.trim() !== '')
20445
21038
  return domain.trim();
20446
21039
  try {
@@ -20607,6 +21200,18 @@ exports.WIZARD_CSS = `
20607
21200
  overflow-wrap: anywhere;
20608
21201
  word-break: break-word;
20609
21202
  }
21203
+ /* MS-8: the pool cap's loud form — the 已配对服务端 cell's value and sub
21204
+ wear data-limit="true" at 8/8 and turn bold red (the same loudness the
21205
+ settings page's limit alert uses). */
21206
+ [data-zen-remote="wizard"] .zr-wizard-overviewValue[data-limit="true"] {
21207
+ color: var(--dsw-alias-state-error-primary, #d5304f);
21208
+ font-weight: 700;
21209
+ }
21210
+ [data-zen-remote="wizard"] .zr-wizard-overviewSub[data-limit="true"],
21211
+ [data-zen-remote="wizard-modal"] .zr-wizard-subtitle[data-limit="true"] {
21212
+ color: var(--dsw-alias-state-error-primary, #d5304f);
21213
+ font-weight: 600;
21214
+ }
20610
21215
  [data-zen-remote="wizard"] .zr-wizard-emptyBanner {
20611
21216
  display: flex;
20612
21217
  align-items: center;
@@ -21242,6 +21847,17 @@ exports.WIZARD_CSS = `
21242
21847
  background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-module-platform));
21243
21848
  color: var(--dsw-alias-label-secondary);
21244
21849
  }
21850
+ /* MS-8: the servers-modal state badge tones — green online, amber offline,
21851
+ red for a verdict only a re-pair or an upgrade fixes. */
21852
+ [data-zen-remote="wizard"] .zr-wizard-deviceBadge[data-tone="live"] {
21853
+ color: var(--dsw-alias-state-success-primary, #16a34a);
21854
+ }
21855
+ [data-zen-remote="wizard"] .zr-wizard-deviceBadge[data-tone="warn"] {
21856
+ color: var(--dsw-alias-state-warning-primary, #b8860b);
21857
+ }
21858
+ [data-zen-remote="wizard"] .zr-wizard-deviceBadge[data-tone="invalid"] {
21859
+ color: var(--dsw-alias-state-error-primary, #d5304f);
21860
+ }
21245
21861
  [data-zen-remote="wizard"] .zr-wizard-deviceActions {
21246
21862
  display: flex;
21247
21863
  align-items: center;
@@ -21834,6 +22450,10 @@ __modules["client-data/remote-status.js"] = function (require, module, exports)
21834
22450
  * - `parseRemoteStatusBody(body, now)` maps the
21835
22451
  * `GET /_dsh/zen-remote/client/remote-status` body
21836
22452
  * (`{ state, versionMismatch, serverName, closed }`) into a tolerant view;
22453
+ * since MS-6 the body's `servers` dict (per handshake serverId) parses
22454
+ * alongside, and `resolveServerStatusView(view, sessionId)` picks the
22455
+ * session's OWN server entry — falling back to the top-level mirror (the
22456
+ * FIRST pool entry) whenever the dict cannot answer;
21837
22457
  * - `describeRemoteStatus(view)` maps one view to the icon's three states —
21838
22458
  * online (lit) / offline (grey; `revoked` / `unpaired` count as offline —
21839
22459
  * the link is not serving either way) / mismatch (yellow, only ever while
@@ -21857,6 +22477,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
21857
22477
  exports.REMOTE_STATUS_POLL_MS = exports.windowNetworkHint = exports.documentVisibility = exports.BRIDGE_STAMP_ATTRIBUTE = exports.BRIDGE_STAMP_PLUGIN = exports.CLIENT_UNSHARE_ROUTE = exports.NETWORK_HINT_ROUTE = exports.REMOTE_STATUS_ROUTE = void 0;
21858
22478
  exports.parseRemoteStatusBody = parseRemoteStatusBody;
21859
22479
  exports.shouldStampBridgeAttribute = shouldStampBridgeAttribute;
22480
+ exports.resolveServerStatusView = resolveServerStatusView;
22481
+ exports.shouldStampBridgeForSession = shouldStampBridgeForSession;
21860
22482
  exports.createZhRemoteStatusFormatter = createZhRemoteStatusFormatter;
21861
22483
  exports.describeRemoteStatus = describeRemoteStatus;
21862
22484
  exports.bannerText = bannerText;
@@ -21873,12 +22495,25 @@ exports.REMOTE_STATUS_ROUTE = '/_dsh/zen-remote/client/remote-status';
21873
22495
  exports.NETWORK_HINT_ROUTE = '/_dsh/zen-remote/client/network-hint';
21874
22496
  /** Same-origin client route closing one remote session from this machine. */
21875
22497
  exports.CLIENT_UNSHARE_ROUTE = '/_dsh/zen-remote/client/unshare';
22498
+ /** The tolerant per-entry parse the top level and the `servers` dict share:
22499
+ * a garbage shape is "not online yet", a garbage field degrades to its
22500
+ * default — never a throw into the polling loop. */
22501
+ function parseServerStatusEntry(value) {
22502
+ const record = value !== null && typeof value === 'object' ? value : {};
22503
+ return {
22504
+ state: record.state === 'online' || record.state === 'revoked' || record.state === 'unpaired' ? record.state : 'offline',
22505
+ versionMismatch: record.versionMismatch === true,
22506
+ serverName: typeof record.serverName === 'string' ? record.serverName : '',
22507
+ bridgedPlugins: Array.isArray(record.bridgedPlugins)
22508
+ ? record.bridgedPlugins.filter((name) => typeof name === 'string' && name !== '')
22509
+ : [],
22510
+ };
22511
+ }
21876
22512
  /** Parse one GET body tolerantly: a garbage shape is "not online yet", a
21877
22513
  * garbage field degrades to its default — never a throw into the polling
21878
22514
  * loop. */
21879
22515
  function parseRemoteStatusBody(body, now) {
21880
22516
  const record = body !== null && typeof body === 'object' ? body : {};
21881
- const state = record.state === 'online' || record.state === 'revoked' || record.state === 'unpaired' ? record.state : 'offline';
21882
22517
  const closed = {};
21883
22518
  if (record.closed !== null && typeof record.closed === 'object') {
21884
22519
  for (const [sessionId, reason] of Object.entries(record.closed)) {
@@ -21887,14 +22522,21 @@ function parseRemoteStatusBody(body, now) {
21887
22522
  closed[sessionId] = reason === 'client' || reason === 'idle' ? reason : 'manual';
21888
22523
  }
21889
22524
  }
22525
+ // MS-6: the per-server dict, keyed by the handshake serverId. A legacy
22526
+ // body (or a broken one) parses to the EMPTY dict — the parts then read
22527
+ // the top-level mirror, the pre-MS-6 answer.
22528
+ const servers = {};
22529
+ if (record.servers !== null && typeof record.servers === 'object' && !Array.isArray(record.servers)) {
22530
+ for (const [serverId, entry] of Object.entries(record.servers)) {
22531
+ if (serverId === '')
22532
+ continue;
22533
+ servers[serverId] = parseServerStatusEntry(entry);
22534
+ }
22535
+ }
21890
22536
  return {
21891
- state,
21892
- versionMismatch: record.versionMismatch === true,
21893
- serverName: typeof record.serverName === 'string' ? record.serverName : '',
22537
+ ...parseServerStatusEntry(record),
21894
22538
  closed,
21895
- bridgedPlugins: Array.isArray(record.bridgedPlugins)
21896
- ? record.bridgedPlugins.filter((name) => typeof name === 'string' && name !== '')
21897
- : [],
22539
+ servers,
21898
22540
  asOf: now,
21899
22541
  };
21900
22542
  }
@@ -21912,6 +22554,41 @@ function parseRemoteStatusBody(body, now) {
21912
22554
  function shouldStampBridgeAttribute(bridgedPlugins, plugin) {
21913
22555
  return Array.isArray(bridgedPlugins) && bridgedPlugins.includes(plugin);
21914
22556
  }
22557
+ /**
22558
+ * Which server's facts ONE session reads (MS-6): given the session id on
22559
+ * screen, its virtual form names the owning serverId, and THAT server's
22560
+ * entry answers — per-server isolation for the icon, the banner and the
22561
+ * bridge stamp. Everything else falls back to the TOP-LEVEL view, the
22562
+ * pre-MS-6 answer: a session id that is not virtual (a local session), a
22563
+ * serverId the dict never answered (its entry never handshook), a legacy
22564
+ * body without a dict, or no session id at all. Tolerant the same way as
22565
+ * the parse: hand-rolled views without `servers` never throw here.
22566
+ */
22567
+ function resolveServerStatusView(view, sessionId) {
22568
+ if (view === undefined || view === null)
22569
+ return undefined;
22570
+ if (sessionId !== undefined) {
22571
+ const parts = (0, virtual_id_ts_1.fromVirtual)(sessionId);
22572
+ if (parts !== undefined) {
22573
+ const server = view.servers?.[parts.serverId];
22574
+ if (server !== undefined)
22575
+ return server;
22576
+ }
22577
+ }
22578
+ // RemoteStatusView structurally carries every RemoteServerStatusView field.
22579
+ return view;
22580
+ }
22581
+ /**
22582
+ * Whether the remote session CURRENTLY ON SCREEN has its own server's bridge
22583
+ * verdict going its way (MS-6): the verdict comes from the session's server
22584
+ * entry ({@link resolveServerStatusView}), not the page-wide mirror of the
22585
+ * FIRST pool entry — with several servers paired, A bridging a plugin must
22586
+ * not un-hide it over B's sessions. Tolerant like the 2-argument core it
22587
+ * wraps: a garbage or missing list reads as "not bridged".
22588
+ */
22589
+ function shouldStampBridgeForSession(view, sessionId, plugin) {
22590
+ return shouldStampBridgeAttribute(resolveServerStatusView(view, sessionId)?.bridgedPlugins, plugin);
22591
+ }
21915
22592
  /** The plugin the bridge stamp names. */
21916
22593
  exports.BRIDGE_STAMP_PLUGIN = 'dsh-better-sidebar';
21917
22594
  /** The html attribute the stamp writes (`data-zr-bridge-dsh-better-sidebar`). */
@@ -21922,15 +22599,20 @@ exports.BRIDGE_STAMP_ATTRIBUTE = `data-zr-bridge-${exports.BRIDGE_STAMP_PLUGIN}`
21922
22599
  function createZhRemoteStatusFormatter() {
21923
22600
  return (key) => locales_ts_1.zh[key];
21924
22601
  }
21925
- function describeRemoteStatus(view, t = createZhRemoteStatusFormatter()) {
21926
- if (view === undefined || view === null)
22602
+ function describeRemoteStatus(view, t = createZhRemoteStatusFormatter(),
22603
+ /** MS-6: the session on screen — its virtual id picks its OWN server's
22604
+ * entry; omitted (or unanswered by the dict) keeps the pre-MS-6 top-level
22605
+ * read. */
22606
+ sessionId) {
22607
+ const status = resolveServerStatusView(view, sessionId);
22608
+ if (status === undefined)
21927
22609
  return { state: 'offline', hoverText: t('remoteStatusOffline') };
21928
- if (view.state === 'revoked')
22610
+ if (status.state === 'revoked')
21929
22611
  return { state: 'revoked', hoverText: t('remoteStatusRevoked') };
21930
- if (view.state === 'unpaired')
22612
+ if (status.state === 'unpaired')
21931
22613
  return { state: 'unpaired', hoverText: t('remoteStatusUnpaired') };
21932
- if (view.state === 'online') {
21933
- return view.versionMismatch
22614
+ if (status.state === 'online') {
22615
+ return status.versionMismatch
21934
22616
  ? { state: 'mismatch', hoverText: t('remoteStatusMismatch') }
21935
22617
  : { state: 'online', hoverText: t('remoteStatusOnline') };
21936
22618
  }
@@ -21948,6 +22630,8 @@ function describeRemoteStatus(view, t = createZhRemoteStatusFormatter()) {
21948
22630
  * no banner.
21949
22631
  */
21950
22632
  function bannerText(view, sessionId, t = (key) => locales_ts_1.zh[key]) {
22633
+ // The closed map is SESSION-keyed (virtual ids embed their server, so one
22634
+ // global dict cannot collide) and outranks every link-level line.
21951
22635
  const reason = view?.closed[sessionId];
21952
22636
  if (reason === 'idle')
21953
22637
  return t('remoteBannerClosedIdle');
@@ -21957,14 +22641,17 @@ function bannerText(view, sessionId, t = (key) => locales_ts_1.zh[key]) {
21957
22641
  // vocabulary): a closure with no certain reason reads as the manual close.
21958
22642
  if (reason !== undefined)
21959
22643
  return t('remoteBannerClosedManual');
21960
- if (view !== undefined && view !== null) {
21961
- if (view.state === 'offline')
22644
+ // MS-6: the link-level line reads the SESSION'S OWN server, not the page's
22645
+ // first-entry mirror.
22646
+ const status = resolveServerStatusView(view, sessionId);
22647
+ if (status !== undefined) {
22648
+ if (status.state === 'offline')
21962
22649
  return t('remoteBannerOffline');
21963
22650
  // T41a-fix2: a revoked token / an unpaired client is not "reconnecting"
21964
22651
  // — the composer must not sit disabled on a promise no reconnect keeps.
21965
- if (view.state === 'revoked')
22652
+ if (status.state === 'revoked')
21966
22653
  return t('remoteBannerRevoked');
21967
- if (view.state === 'unpaired')
22654
+ if (status.state === 'unpaired')
21968
22655
  return t('remoteBannerUnpaired');
21969
22656
  }
21970
22657
  return undefined;
@@ -22274,7 +22961,10 @@ function RemoteStatusIcon({ sessionId, status, t }) {
22274
22961
  // still be ready from a PREVIOUS remote session when a local one opens.
22275
22962
  if (!(0, virtual_id_js_1.isVirtual)(sessionId) || !snap.ready)
22276
22963
  return null;
22277
- const description = (0, remote_status_ts_1.describeRemoteStatus)(snap.view, t);
22964
+ // MS-6: the session's virtual id picks its OWN server's entry — with
22965
+ // several servers paired, A online and B offline light and grey their own
22966
+ // sessions independently.
22967
+ const description = (0, remote_status_ts_1.describeRemoteStatus)(snap.view, t, sessionId);
22278
22968
  const close = () => {
22279
22969
  // Offline recovers on its own; revoked / unpaired do not (T34-fix) —
22280
22970
  // each says its own word instead of "reconnecting".
@@ -22545,9 +23235,20 @@ function startCurrentSessionReporter(options = {}) {
22545
23235
  return undefined;
22546
23236
  }
22547
23237
  };
22548
- const post = (report) => {
23238
+ const readLang = () => {
23239
+ if (options.getLang !== undefined)
23240
+ return options.getLang();
23241
+ const doc = globalThis.document;
23242
+ const htmlLang = doc?.documentElement?.lang;
23243
+ if (typeof htmlLang === 'string' && htmlLang !== '') {
23244
+ return htmlLang.toLowerCase().startsWith('zh') ? 'zh' : 'en';
23245
+ }
23246
+ return undefined;
23247
+ };
23248
+ const post = (report, lang) => {
22549
23249
  const doFetch = options.fetchImpl ?? fetch;
22550
- const body = report.kind === 'open' ? { sessionId: report.sessionId } : { sessionId: null };
23250
+ const base = report.kind === 'open' ? { sessionId: report.sessionId } : { sessionId: null };
23251
+ const body = lang !== undefined ? { ...base, lang } : base;
22551
23252
  return doFetch(options.route ?? CURRENT_SESSION_ROUTE, {
22552
23253
  method: 'POST',
22553
23254
  headers: { 'content-type': 'application/json; charset=utf-8' },
@@ -22561,6 +23262,7 @@ function startCurrentSessionReporter(options = {}) {
22561
23262
  // T66: the timestamp of the last accepted post drives the slow unconditional
22562
23263
  // re-send (0 = due immediately, which is the page-load post).
22563
23264
  let lastReported;
23265
+ let lastReportedLang;
22564
23266
  let lastSentAt = 0;
22565
23267
  const resendEveryMs = options.resendIntervalMs ?? RESEND_INTERVAL_MS;
22566
23268
  let role;
@@ -22578,16 +23280,22 @@ function startCurrentSessionReporter(options = {}) {
22578
23280
  const report = (0, current_session_ts_1.parseCurrentSessionStorage)(readRaw());
22579
23281
  if (report.kind === 'unavailable')
22580
23282
  return;
23283
+ const lang = readLang();
22581
23284
  const now = Date.now();
22582
23285
  // T66: inside the re-send window an unchanged value says nothing; once
22583
23286
  // the window passes, the CURRENT value re-posts even unchanged — the
22584
23287
  // backend may have been reset underneath the page.
22585
23288
  const resendDue = now - lastSentAt >= resendEveryMs;
22586
- if (!resendDue && lastReported !== undefined && (0, current_session_ts_1.currentSessionReportEquals)(report, lastReported))
23289
+ if (!resendDue &&
23290
+ lastReported !== undefined &&
23291
+ (0, current_session_ts_1.currentSessionReportEquals)(report, lastReported) &&
23292
+ lang === lastReportedLang) {
22587
23293
  return;
23294
+ }
22588
23295
  try {
22589
- await post(report);
23296
+ await post(report, lang);
22590
23297
  lastReported = report;
23298
+ lastReportedLang = lang;
22591
23299
  lastSentAt = now;
22592
23300
  }
22593
23301
  catch {
@@ -23291,13 +23999,27 @@ html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [d
23291
23999
 
23292
24000
  KNOWN BOUNDARY (accepted, not fixed): in a remote session, searching the
23293
24001
  model menu and pressing Enter can commit a HIDDEN local model — the
23294
- interceptor refuses it with its usual message. */
24002
+ interceptor refuses it with its usual message.
24003
+
24004
+ MS-4, several paired servers: each remote group heading also carries its
24005
+ server id (zero-width bits after the mark, virtual-id.ts
24006
+ encodeServerIdZeroWidth), and the effect reads the open session's server
24007
+ off html[data-zr-remote-server] (effects/remote-session.ts). A remote
24008
+ group of a DIFFERENT server is stamped "remote-other": hidden inside a
24009
+ remote session (only the open session's own server's models can run
24010
+ there) and, like every remote group, outside one. */
23295
24011
  html[data-zr-remote-session="1"] section[role="group"][data-zr-group="local"] {
23296
24012
  display: none !important;
23297
24013
  }
24014
+ html[data-zr-remote-session="1"] section[role="group"][data-zr-group="remote-other"] {
24015
+ display: none !important;
24016
+ }
23298
24017
  html:not([data-zr-remote-session]) section[role="group"][data-zr-group="remote"] {
23299
24018
  display: none !important;
23300
24019
  }
24020
+ html:not([data-zr-remote-session]) section[role="group"][data-zr-group="remote-other"] {
24021
+ display: none !important;
24022
+ }
23301
24023
 
23302
24024
  /* ---------- T52-fix: hide the virtual groups in NATIVE <option> pickers ----
23303
24025
  The model menu above is React DOM, but dsh-vision-router's settings render
@@ -23396,6 +24118,85 @@ html[data-zr-remote-session="1"] .zr-download-action[data-phase="error"] {
23396
24118
  }
23397
24119
  `;
23398
24120
  };
24121
+ __modules["client-data/model-group-side.js"] = function (require, module, exports) {
24122
+ "use strict";
24123
+ /**
24124
+ * T74: which side of the pairing a model-menu GROUP belongs to, as pure
24125
+ * functions — the readable core of the browser effect
24126
+ * (src/client/effects/model-group-side.ts).
24127
+ *
24128
+ * WHY: the merged catalog (merge-streams.ts mergeModelCatalogs) appends the
24129
+ * server's provider groups everywhere, but a selection only runs on the side
24130
+ * the open session lives on — the interceptor refuses the other side. Through
24131
+ * DSH 0.1.7 every menu group carried its id in `aria-labelledby`
24132
+ * (`<useId>-<group.id>`), so CSS keyed the hiding on the `zr~` prefix (T52).
24133
+ * DSH 0.2.0 renders the groups with the primitives' MenuGroup, whose
24134
+ * `aria-labelledby` is a bare useId — the id is gone — and the same component
24135
+ * renders OTHER menus' groups too, so attribute scoping misfires in both
24136
+ * directions. Classification therefore moved to the one mark every version
24137
+ * renders: virtual group NAMES start with VIRTUAL_GROUP_NAME_MARK
24138
+ * (virtual-id.ts), read off each group's heading text.
24139
+ *
24140
+ * The container rule is what keeps the effect inert wherever it cannot be
24141
+ * sure: a container with not ONE marked heading — the model menu of a
24142
+ * sub-client with no shared models, every other menu on the page, the whole
24143
+ * rest of the page — classifies as `undefined` for every group, which the
24144
+ * effect reads as "leave the DOM alone". Only a container holding at least
24145
+ * one marked heading is decided, and inside it the unmarked groups are
24146
+ * exactly the LOCAL ones: the merged catalog only ever APPENDS virtual
24147
+ * groups to the local list, never mixes them the other way.
24148
+ *
24149
+ * Defensive like the rest of client-data: an empty or missing heading never
24150
+ * counts as a mark, so it can never decide a container by accident.
24151
+ */
24152
+ Object.defineProperty(exports, "__esModule", { value: true });
24153
+ exports.REMOTE_SERVER_ATTRIBUTE = void 0;
24154
+ exports.classifyModelGroups = classifyModelGroups;
24155
+ exports.classifyModelGroupsForSession = classifyModelGroupsForSession;
24156
+ const virtual_id_ts_1 = require("./virtual-id.js");
24157
+ /** MS-4: the <html> attribute carrying the open REMOTE session's server id
24158
+ * (`zr~<serverId>~…`'s middle half). Written by effects/remote-session.ts
24159
+ * together with `data-zr-remote-session`, read by
24160
+ * effects/model-group-side.ts as the `activeRemoteServerId`. */
24161
+ exports.REMOTE_SERVER_ATTRIBUTE = 'data-zr-remote-server';
24162
+ function classifyModelGroups(groups) {
24163
+ return classifyModelGroupsForSession(groups);
24164
+ }
24165
+ /**
24166
+ * The MS-4 extension: with `activeRemoteServerId` set (the `zr~<serverId>~`
24167
+ * half of the session the browser has open, effects/remote-session.ts),
24168
+ * each marked heading's embedded server id (decodeServerIdZeroWidth)
24169
+ * decides between plain `remote` (the open session's own server, or a
24170
+ * heading whose server cannot be decoded — shown, never hidden) and
24171
+ * `remote-other` (a DIFFERENT paired server's group — hidden in a remote
24172
+ * session by remote-session.css.ts). Without the argument the verdicts
24173
+ * are exactly {@link classifyModelGroups}'s — the single-paired-server
24174
+ * behavior, byte for byte.
24175
+ */
24176
+ function classifyModelGroupsForSession(groups, activeRemoteServerId) {
24177
+ // Per container: did any heading carry the mark? Containers are decided
24178
+ // independently — one mixed model menu must not turn an all-local
24179
+ // container (a second menu elsewhere on the page) into "hide everything
24180
+ // local" there.
24181
+ const marked = new Map();
24182
+ for (const group of groups) {
24183
+ const had = marked.get(group.containerKey) ?? false;
24184
+ marked.set(group.containerKey, had || group.headingText.startsWith(virtual_id_ts_1.VIRTUAL_GROUP_NAME_MARK));
24185
+ }
24186
+ return groups.map((group) => {
24187
+ if (!marked.get(group.containerKey))
24188
+ return undefined;
24189
+ if (!group.headingText.startsWith(virtual_id_ts_1.VIRTUAL_GROUP_NAME_MARK))
24190
+ return 'local';
24191
+ if (activeRemoteServerId !== undefined) {
24192
+ const groupServerId = (0, virtual_id_ts_1.decodeServerIdZeroWidth)(group.headingText);
24193
+ if (groupServerId !== undefined && groupServerId !== activeRemoteServerId)
24194
+ return 'remote-other';
24195
+ }
24196
+ return 'remote';
24197
+ });
24198
+ }
24199
+ };
23399
24200
  __modules["client/effects/remote-session.js"] = function (require, module, exports) {
23400
24201
  "use strict";
23401
24202
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -23405,6 +24206,7 @@ const virtual_id_ts_1 = require("./virtual-id.js");
23405
24206
  const remote_session_css_ts_1 = require("./client/styles/remote-session.css.js");
23406
24207
  const remote_status_ts_1 = require("./client-data/remote-status.js");
23407
24208
  const locales_ts_1 = require("./client/locales.js");
24209
+ const model_group_side_ts_1 = require("./client-data/model-group-side.js");
23408
24210
  /**
23409
24211
  * The remote-session marker (T41b): the CURRENT main-view session id, read
23410
24212
  * the way the official uiSession `publishMain` reads it (RT
@@ -23414,6 +24216,12 @@ const locales_ts_1 = require("./client/locales.js");
23414
24216
  * every open-on-the-server-machine entry, so the decision lives at the one
23415
24217
  * place that knows the session and never inside component DOM.
23416
24218
  *
24219
+ * MS-4: with several servers paired, the virtual id's serverId half is
24220
+ * stamped alongside as `data-zr-remote-server="<serverId>"` — the model
24221
+ * menu's per-server isolation (effects/model-group-side.ts reads it to hide
24222
+ * the OTHER paired servers' groups; remote-session.css.ts hides them). Both
24223
+ * attributes move together: a local session (or unmount) clears both.
24224
+ *
23417
24225
  * The official tie-break is kept verbatim: when the previous answer is still
23418
24226
  * mainView-retained it STAYS the answer, and only otherwise does the first
23419
24227
  * mainView-retained row in catalog order win — so two simultaneously
@@ -23462,10 +24270,16 @@ function installRemoteSessionGuard(ctx) {
23462
24270
  const apply = () => {
23463
24271
  const sessionId = currentMainSessionIdOf(ctx);
23464
24272
  const root = document.documentElement;
23465
- if (sessionId !== undefined && (0, virtual_id_ts_1.isVirtual)(sessionId))
24273
+ const parts = sessionId !== undefined ? (0, virtual_id_ts_1.fromVirtual)(sessionId) : undefined;
24274
+ if (parts !== undefined) {
23466
24275
  root.setAttribute('data-zr-remote-session', '1');
23467
- else
24276
+ if (root.getAttribute(model_group_side_ts_1.REMOTE_SERVER_ATTRIBUTE) !== parts.serverId)
24277
+ root.setAttribute(model_group_side_ts_1.REMOTE_SERVER_ATTRIBUTE, parts.serverId);
24278
+ }
24279
+ else {
23468
24280
  root.removeAttribute('data-zr-remote-session');
24281
+ root.removeAttribute(model_group_side_ts_1.REMOTE_SERVER_ATTRIBUTE);
24282
+ }
23469
24283
  };
23470
24284
  apply();
23471
24285
  const offs = [ctx.sessions.list.subscribe(apply)];
@@ -23479,6 +24293,7 @@ function installRemoteSessionGuard(ctx) {
23479
24293
  for (const off of offs)
23480
24294
  off();
23481
24295
  document.documentElement.removeAttribute('data-zr-remote-session');
24296
+ document.documentElement.removeAttribute(model_group_side_ts_1.REMOTE_SERVER_ATTRIBUTE);
23482
24297
  };
23483
24298
  }, 'dsh-zen-remote: remote-session attribute');
23484
24299
  // The bridge stamp (PB-14): while a REMOTE session is on screen, the
@@ -23502,7 +24317,7 @@ function installRemoteSessionGuard(ctx) {
23502
24317
  };
23503
24318
  const applyBridge = () => {
23504
24319
  const view = store.getSnapshot().view;
23505
- if ((0, remote_status_ts_1.shouldStampBridgeAttribute)(view?.bridgedPlugins, remote_status_ts_1.BRIDGE_STAMP_PLUGIN)) {
24320
+ if ((0, remote_status_ts_1.shouldStampBridgeForSession)(view, currentMainSessionIdOf(ctx), remote_status_ts_1.BRIDGE_STAMP_PLUGIN)) {
23506
24321
  root.setAttribute(remote_status_ts_1.BRIDGE_STAMP_ATTRIBUTE, '1');
23507
24322
  root.style.removeProperty('--zr-bridge-note');
23508
24323
  }
@@ -23521,7 +24336,13 @@ function installRemoteSessionGuard(ctx) {
23521
24336
  applyBridge();
23522
24337
  return;
23523
24338
  }
23524
- if (!virtualNow && offStore !== undefined) {
24339
+ // MS-6: remote A → remote B keeps the subscription, but the verdict
24340
+ // belongs to the session's own server and must be re-judged.
24341
+ if (virtualNow) {
24342
+ applyBridge();
24343
+ return;
24344
+ }
24345
+ if (offStore !== undefined) {
23525
24346
  offStore();
23526
24347
  offStore = undefined;
23527
24348
  clear();
@@ -23541,57 +24362,6 @@ function installRemoteSessionGuard(ctx) {
23541
24362
  }, 'dsh-zen-remote: bridge stamp');
23542
24363
  }
23543
24364
  };
23544
- __modules["client-data/model-group-side.js"] = function (require, module, exports) {
23545
- "use strict";
23546
- /**
23547
- * T74: which side of the pairing a model-menu GROUP belongs to, as pure
23548
- * functions — the readable core of the browser effect
23549
- * (src/client/effects/model-group-side.ts).
23550
- *
23551
- * WHY: the merged catalog (merge-streams.ts mergeModelCatalogs) appends the
23552
- * server's provider groups everywhere, but a selection only runs on the side
23553
- * the open session lives on — the interceptor refuses the other side. Through
23554
- * DSH 0.1.7 every menu group carried its id in `aria-labelledby`
23555
- * (`<useId>-<group.id>`), so CSS keyed the hiding on the `zr~` prefix (T52).
23556
- * DSH 0.2.0 renders the groups with the primitives' MenuGroup, whose
23557
- * `aria-labelledby` is a bare useId — the id is gone — and the same component
23558
- * renders OTHER menus' groups too, so attribute scoping misfires in both
23559
- * directions. Classification therefore moved to the one mark every version
23560
- * renders: virtual group NAMES start with VIRTUAL_GROUP_NAME_MARK
23561
- * (virtual-id.ts), read off each group's heading text.
23562
- *
23563
- * The container rule is what keeps the effect inert wherever it cannot be
23564
- * sure: a container with not ONE marked heading — the model menu of a
23565
- * sub-client with no shared models, every other menu on the page, the whole
23566
- * rest of the page — classifies as `undefined` for every group, which the
23567
- * effect reads as "leave the DOM alone". Only a container holding at least
23568
- * one marked heading is decided, and inside it the unmarked groups are
23569
- * exactly the LOCAL ones: the merged catalog only ever APPENDS virtual
23570
- * groups to the local list, never mixes them the other way.
23571
- *
23572
- * Defensive like the rest of client-data: an empty or missing heading never
23573
- * counts as a mark, so it can never decide a container by accident.
23574
- */
23575
- Object.defineProperty(exports, "__esModule", { value: true });
23576
- exports.classifyModelGroups = classifyModelGroups;
23577
- const virtual_id_ts_1 = require("./virtual-id.js");
23578
- function classifyModelGroups(groups) {
23579
- // Per container: did any heading carry the mark? Containers are decided
23580
- // independently — one mixed model menu must not turn an all-local
23581
- // container (a second menu elsewhere on the page) into "hide everything
23582
- // local" there.
23583
- const marked = new Map();
23584
- for (const group of groups) {
23585
- const had = marked.get(group.containerKey) ?? false;
23586
- marked.set(group.containerKey, had || group.headingText.startsWith(virtual_id_ts_1.VIRTUAL_GROUP_NAME_MARK));
23587
- }
23588
- return groups.map((group) => marked.get(group.containerKey)
23589
- ? group.headingText.startsWith(virtual_id_ts_1.VIRTUAL_GROUP_NAME_MARK)
23590
- ? 'remote'
23591
- : 'local'
23592
- : undefined);
23593
- }
23594
- };
23595
24365
  __modules["client/effects/model-group-side.js"] = function (require, module, exports) {
23596
24366
  "use strict";
23597
24367
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -23608,6 +24378,13 @@ const model_group_side_ts_1 = require("./client-data/model-group-side.js");
23608
24378
  * virtual-id.ts) heading is decided at all, so every other menu and the
23609
24379
  * whole rest of the page is never touched.
23610
24380
  *
24381
+ * MS-4: the open remote session's server id (`data-zr-remote-server` on
24382
+ * <html>, written by effects/remote-session.ts) is the classifier's
24383
+ * `activeRemoteServerId` — a remote group whose heading decodes to a
24384
+ * DIFFERENT paired server is stamped `remote-other`. Switching between two
24385
+ * servers' sessions changes no group DOM at all, so a second observer
24386
+ * watches that one <html> attribute and rescans the moment it moves.
24387
+ *
23611
24388
  * Synchronous in the observer callback, never behind rAF: the callback is a
23612
24389
  * microtask and runs before this frame paints, so a group that must be
23613
24390
  * hidden is hidden the first time it is drawn. A rAF would land a frame late
@@ -23618,8 +24395,8 @@ const model_group_side_ts_1 = require("./client-data/model-group-side.js");
23618
24395
  */
23619
24396
  const GROUP_SELECTOR = 'section[role="group"]';
23620
24397
  /** The stamp this effect writes; remote-session.css.ts hides
23621
- * `[data-zr-group="local"]` in a remote session and
23622
- * `[data-zr-group="remote"]` everywhere else. */
24398
+ * `[data-zr-group="local"]` and `[data-zr-group="remote-other"]` in a
24399
+ * remote session and both remote stamps everywhere else. */
23623
24400
  const STAMP = 'data-zr-group';
23624
24401
  /**
23625
24402
  * The heading text a group section renders, across the two DSH layouts:
@@ -23701,7 +24478,8 @@ function installModelGroupSide(ctx) {
23701
24478
  const text = headingTextOf(section, (id) => document.getElementById(id));
23702
24479
  rows.push({ section, containerKey: key, headingText: text ?? '' });
23703
24480
  }
23704
- const verdicts = (0, model_group_side_ts_1.classifyModelGroups)(rows);
24481
+ const activeRemoteServerId = document.documentElement.getAttribute(model_group_side_ts_1.REMOTE_SERVER_ATTRIBUTE) ?? undefined;
24482
+ const verdicts = (0, model_group_side_ts_1.classifyModelGroupsForSession)(rows, activeRemoteServerId);
23705
24483
  rows.forEach((row, i) => applyVerdict(row.section, verdicts[i]));
23706
24484
  };
23707
24485
  // childList + subtree catches mounted/unmounted/re-parented groups,
@@ -23716,12 +24494,18 @@ function installModelGroupSide(ctx) {
23716
24494
  }
23717
24495
  });
23718
24496
  observer.observe(document.body, { childList: true, subtree: true, characterData: true });
24497
+ // MS-4: the session's server moves on <html> alone (A's session → B's
24498
+ // with the menu open touches no group) — rescan synchronously, same
24499
+ // no-rAF discipline as above.
24500
+ const serverObserver = new MutationObserver(() => scan());
24501
+ serverObserver.observe(document.documentElement, { attributes: true, attributeFilter: [model_group_side_ts_1.REMOTE_SERVER_ATTRIBUTE] });
23719
24502
  // The initial paint may already hold groups (session restored under an
23720
24503
  // open menu): stamp them synchronously at install, like every observer
23721
24504
  // effect here does.
23722
24505
  scan();
23723
24506
  return () => {
23724
24507
  observer.disconnect();
24508
+ serverObserver.disconnect();
23725
24509
  // Undo every stamp so the host DOM reads exactly as before the effect
23726
24510
  // ran — the CSS rules key on the stamp, so this un-hides everything.
23727
24511
  for (const section of document.querySelectorAll(`${GROUP_SELECTOR}[${STAMP}]`)) {