dsh-zen-remote 2.2.9 → 2.3.1

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 (149) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/docs/interface.md +1 -1
  4. package/docs/remote-access.md +20 -0
  5. package/lan-gate.mjs +11 -1
  6. package/lib/adapters/dsh-better-sidebar/index.js +6 -2
  7. package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
  8. package/lib/adapters/dsh-better-sidebar/methods.js +3 -0
  9. package/lib/adapters/dsh-better-sidebar/methods.js.map +1 -1
  10. package/lib/admin-routes.js +33 -2
  11. package/lib/admin-routes.js.map +1 -1
  12. package/lib/client.js +3289 -6
  13. package/lib/config.js +70 -0
  14. package/lib/config.js.map +1 -1
  15. package/lib/index.js +232 -2
  16. package/lib/index.js.map +1 -1
  17. package/lib/intercept.js +43 -3
  18. package/lib/intercept.js.map +1 -1
  19. package/lib/lan-gate-server.cjs +29 -3
  20. package/lib/merge-streams.js +42 -12
  21. package/lib/merge-streams.js.map +1 -1
  22. package/lib/types/adapters/dsh-better-sidebar/index.d.ts +1 -1
  23. package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
  24. package/lib/types/adapters/dsh-better-sidebar/methods.d.ts.map +1 -1
  25. package/lib/types/admin-routes.d.ts +13 -0
  26. package/lib/types/admin-routes.d.ts.map +1 -1
  27. package/lib/types/client/index.d.ts.map +1 -1
  28. package/lib/types/client/locales.d.ts +204 -1
  29. package/lib/types/client/locales.d.ts.map +1 -1
  30. package/lib/types/client/wizard/GatewayWizard.d.ts +29 -0
  31. package/lib/types/client/wizard/GatewayWizard.d.ts.map +1 -0
  32. package/lib/types/client/wizard/WizardCard.d.ts +30 -0
  33. package/lib/types/client/wizard/WizardCard.d.ts.map +1 -0
  34. package/lib/types/client/wizard/cards/MigrateCard.d.ts +15 -0
  35. package/lib/types/client/wizard/cards/MigrateCard.d.ts.map +1 -0
  36. package/lib/types/client/wizard/cards/PortForwardCard.d.ts +15 -0
  37. package/lib/types/client/wizard/cards/PortForwardCard.d.ts.map +1 -0
  38. package/lib/types/client/wizard/cards/ProxyCard.d.ts +19 -0
  39. package/lib/types/client/wizard/cards/ProxyCard.d.ts.map +1 -0
  40. package/lib/types/client/wizard/cards/TokenCard.d.ts +20 -0
  41. package/lib/types/client/wizard/cards/TokenCard.d.ts.map +1 -0
  42. package/lib/types/client/wizard/register-wizard.d.ts +51 -0
  43. package/lib/types/client/wizard/register-wizard.d.ts.map +1 -0
  44. package/lib/types/client/wizard/wizard-css.d.ts +10 -0
  45. package/lib/types/client/wizard/wizard-css.d.ts.map +1 -0
  46. package/lib/types/client-data/qr.d.ts +20 -0
  47. package/lib/types/client-data/qr.d.ts.map +1 -0
  48. package/lib/types/client-data/settings-form.d.ts +7 -2
  49. package/lib/types/client-data/settings-form.d.ts.map +1 -1
  50. package/lib/types/client-data/wizard.d.ts +247 -0
  51. package/lib/types/client-data/wizard.d.ts.map +1 -0
  52. package/lib/types/config.d.ts +27 -0
  53. package/lib/types/config.d.ts.map +1 -1
  54. package/lib/types/index.d.ts.map +1 -1
  55. package/lib/types/intercept.d.ts.map +1 -1
  56. package/lib/types/merge-streams.d.ts.map +1 -1
  57. package/lib/types/wizard/binary-io.d.ts +111 -0
  58. package/lib/types/wizard/binary-io.d.ts.map +1 -0
  59. package/lib/types/wizard/binary.d.ts +118 -0
  60. package/lib/types/wizard/binary.d.ts.map +1 -0
  61. package/lib/types/wizard/caddy-process.d.ts +101 -0
  62. package/lib/types/wizard/caddy-process.d.ts.map +1 -0
  63. package/lib/types/wizard/caddy.d.ts +53 -0
  64. package/lib/types/wizard/caddy.d.ts.map +1 -0
  65. package/lib/types/wizard/cloudflare.d.ts +112 -0
  66. package/lib/types/wizard/cloudflare.d.ts.map +1 -0
  67. package/lib/types/wizard/cloudflared.d.ts +71 -0
  68. package/lib/types/wizard/cloudflared.d.ts.map +1 -0
  69. package/lib/types/wizard/probe.d.ts +66 -0
  70. package/lib/types/wizard/probe.d.ts.map +1 -0
  71. package/lib/types/wizard/proxy-snippets.d.ts +60 -0
  72. package/lib/types/wizard/proxy-snippets.d.ts.map +1 -0
  73. package/lib/types/wizard/routes.d.ts +152 -0
  74. package/lib/types/wizard/routes.d.ts.map +1 -0
  75. package/lib/types/wizard/source-detect.d.ts +53 -0
  76. package/lib/types/wizard/source-detect.d.ts.map +1 -0
  77. package/lib/types/wizard/status.d.ts +115 -0
  78. package/lib/types/wizard/status.d.ts.map +1 -0
  79. package/lib/types/wizard/token-store.d.ts +27 -0
  80. package/lib/types/wizard/token-store.d.ts.map +1 -0
  81. package/lib/types/wizard/tunnel-orchestrator.d.ts +90 -0
  82. package/lib/types/wizard/tunnel-orchestrator.d.ts.map +1 -0
  83. package/lib/types/wizard/verify.d.ts +97 -0
  84. package/lib/types/wizard/verify.d.ts.map +1 -0
  85. package/lib/wizard/binary-io.js +386 -0
  86. package/lib/wizard/binary-io.js.map +1 -0
  87. package/lib/wizard/binary.js +138 -0
  88. package/lib/wizard/binary.js.map +1 -0
  89. package/lib/wizard/caddy-process.js +162 -0
  90. package/lib/wizard/caddy-process.js.map +1 -0
  91. package/lib/wizard/caddy.js +80 -0
  92. package/lib/wizard/caddy.js.map +1 -0
  93. package/lib/wizard/cloudflare.js +334 -0
  94. package/lib/wizard/cloudflare.js.map +1 -0
  95. package/lib/wizard/cloudflared.js +158 -0
  96. package/lib/wizard/cloudflared.js.map +1 -0
  97. package/lib/wizard/probe.js +118 -0
  98. package/lib/wizard/probe.js.map +1 -0
  99. package/lib/wizard/proxy-snippets.js +112 -0
  100. package/lib/wizard/proxy-snippets.js.map +1 -0
  101. package/lib/wizard/routes.js +462 -0
  102. package/lib/wizard/routes.js.map +1 -0
  103. package/lib/wizard/source-detect.js +77 -0
  104. package/lib/wizard/source-detect.js.map +1 -0
  105. package/lib/wizard/status.js +94 -0
  106. package/lib/wizard/status.js.map +1 -0
  107. package/lib/wizard/token-store.js +71 -0
  108. package/lib/wizard/token-store.js.map +1 -0
  109. package/lib/wizard/tunnel-orchestrator.js +155 -0
  110. package/lib/wizard/tunnel-orchestrator.js.map +1 -0
  111. package/lib/wizard/verify.js +171 -0
  112. package/lib/wizard/verify.js.map +1 -0
  113. package/package.json +37 -37
  114. package/scripts/check-settings-form.mjs +5 -4
  115. package/scripts/check-wizard-ui.mjs +690 -0
  116. package/src/adapters/dsh-better-sidebar/index.ts +6 -2
  117. package/src/adapters/dsh-better-sidebar/methods.ts +3 -0
  118. package/src/admin-routes.ts +44 -2
  119. package/src/client/index.tsx +9 -0
  120. package/src/client/locales.ts +410 -2
  121. package/src/client/wizard/GatewayWizard.tsx +2220 -0
  122. package/src/client/wizard/WizardCard.tsx +81 -0
  123. package/src/client/wizard/cards/MigrateCard.tsx +42 -0
  124. package/src/client/wizard/cards/PortForwardCard.tsx +37 -0
  125. package/src/client/wizard/cards/ProxyCard.tsx +56 -0
  126. package/src/client/wizard/cards/TokenCard.tsx +81 -0
  127. package/src/client/wizard/register-wizard.ts +99 -0
  128. package/src/client/wizard/wizard-css.ts +948 -0
  129. package/src/client-data/qr.ts +284 -0
  130. package/src/client-data/settings-form.ts +12 -3
  131. package/src/client-data/wizard.ts +432 -0
  132. package/src/config.ts +87 -0
  133. package/src/index.ts +227 -2
  134. package/src/intercept.ts +43 -3
  135. package/src/merge-streams.ts +41 -12
  136. package/src/wizard/binary-io.ts +449 -0
  137. package/src/wizard/binary.ts +177 -0
  138. package/src/wizard/caddy-process.ts +228 -0
  139. package/src/wizard/caddy.ts +97 -0
  140. package/src/wizard/cloudflare.ts +413 -0
  141. package/src/wizard/cloudflared.ts +196 -0
  142. package/src/wizard/probe.ts +161 -0
  143. package/src/wizard/proxy-snippets.ts +128 -0
  144. package/src/wizard/routes.ts +537 -0
  145. package/src/wizard/source-detect.ts +87 -0
  146. package/src/wizard/status.ts +180 -0
  147. package/src/wizard/token-store.ts +73 -0
  148. package/src/wizard/tunnel-orchestrator.ts +197 -0
  149. package/src/wizard/verify.ts +236 -0
package/lib/client.js CHANGED
@@ -2067,7 +2067,7 @@ __modules["client-data/settings-form.js"] = function (require, module, exports)
2067
2067
  * (devices / pairing) plus the T14 envelope (ok / gateway / config / viaGateway).
2068
2068
  */
2069
2069
  Object.defineProperty(exports, "__esModule", { value: true });
2070
- exports.ZenRemoteSettingsForm = exports.SETTINGS_FIELDS = exports.IDLE_HOURS_MAX = exports.SERVER_NAME_MAX = exports.RESTART_FIELDS = exports.DEVICE_TOKEN_FIELD = exports.CLIENT_CONFIG_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;
2070
+ 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_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;
2071
2071
  exports.deriveClientStatusView = deriveClientStatusView;
2072
2072
  exports.clientStatusLineOf = clientStatusLineOf;
2073
2073
  exports.normalizePairingCode = normalizePairingCode;
@@ -2246,10 +2246,18 @@ function clientStatusLineOf(view, now) {
2246
2246
  default: return { kind: 'unpaired' };
2247
2247
  }
2248
2248
  }
2249
- /** What the pairing-code box keeps as its draft: uppercase, no spaces or
2250
- * hyphens (the gateway strips every other character at claim time anyway). */
2249
+ /** The gateway's 32-character unambiguous pairing-code alphabet (no 0/O/1/I/L)
2250
+ * and fixed 8-character length (`lib/lan-gate-server.cjs:330-335`). */
2251
+ exports.PAIRING_CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
2252
+ exports.PAIRING_CODE_LENGTH = 8;
2253
+ /** What the pairing-code box keeps as its draft: uppercase, restricted to the
2254
+ * 32-character pairing-code alphabet (excluding 0/O/1/I/L and symbols), and
2255
+ * capped at 8 characters. */
2251
2256
  function normalizePairingCode(input) {
2252
- return input.toUpperCase().replace(/[\s-]/g, '');
2257
+ return input
2258
+ .toUpperCase()
2259
+ .replace(/[^ABCDEFGHJKMNPQRSTUVWXYZ23456789]/g, '')
2260
+ .slice(0, exports.PAIRING_CODE_LENGTH);
2253
2261
  }
2254
2262
  /**
2255
2263
  * Whether the settings page may follow the SERVER's record of this device's
@@ -3236,7 +3244,7 @@ exports.zh = {
3236
3244
  // T60: the pairing button's disabled reason while the device-name draft is invalid.
3237
3245
  'settings.client.fixDeviceName': '请先修正设备名称',
3238
3246
  'settings.client.pairingCode': '配对码',
3239
- 'settings.client.pairingCodeHint': '在服务端设置页生成「桌面应用端」配对码后填入(8 位,自动转大写)。',
3247
+ 'settings.client.pairingCodeHint': '在服务端设置页生成「桌面应用端」配对码后填入(8 位,自动转大写并过滤 0/O/1/I/L 与符号)。',
3240
3248
  'settings.client.pair': '配对',
3241
3249
  'settings.client.pairing': '配对中…',
3242
3250
  'settings.client.failInvalid': '服务端地址不合法',
@@ -3349,6 +3357,210 @@ exports.zh = {
3349
3357
  'downloadToLocal': '下载到本机',
3350
3358
  'downloadBusy': '下载中…',
3351
3359
  'downloadFailed': '下载失败',
3360
+ // --- GW-10: 远程访问与配对向导(服务端公网/局域网接入 + 手机/桌面客户端配对) ---
3361
+ 'wizard.title': '远程访问与设备配对',
3362
+ 'wizard.subtitle': '配置本机作为服务端(开放手机与另一台电脑连入)或作为客户端连接远端主机',
3363
+ 'wizard.off': '局域网模式 / 待配置',
3364
+ 'wizard.step.pick': '选择角色与接入',
3365
+ 'wizard.step.configure': '填写连接参数',
3366
+ 'wizard.step.verify': '连通验证与配对',
3367
+ 'wizard.role.sectionTitle': '本机角色',
3368
+ 'wizard.role.host': '作为服务端(主机)',
3369
+ 'wizard.role.hostDesc': '本机运行主力会话与网关,允许手机或另一台 DSH 电脑连入并操作会话',
3370
+ 'wizard.role.client': '作为桌面客户端(从机)',
3371
+ 'wizard.role.clientDesc': '本机连接到另一台 DSH 服务端,在侧边栏直接查看与操作远端共享的会话',
3372
+ 'wizard.role.clientPickNote': '切为客户端后,本机将连接到远端服务端的网关(支持公网 HTTPS 域名或局域网 / Tailscale 地址)。点击「下一步」填写服务端地址与配对码。',
3373
+ 'wizard.ingressSectionTitle': '服务端接入方式',
3374
+ 'wizard.path.caddy': '内置 Caddy(推荐)',
3375
+ 'wizard.path.caddyDesc': '适合有公网 IP 的环境。自动下载 Caddy 并通过 Cloudflare DNS 申请证书,无需手写反代配置',
3376
+ 'wizard.path.external': '使用已有反向代理',
3377
+ 'wizard.path.externalDesc': '适合已部署 Nginx、Caddy 或 Lucky 的环境。提供现成转发规则,直接填入现有反代即可',
3378
+ 'wizard.path.cloudflared': 'Cloudflare Tunnel 内网穿透',
3379
+ 'wizard.path.cloudflaredDesc': '无需公网 IP,也不必配置路由器端口映射。通过 Cloudflare 隧道直连本机,需域名已接入 Cloudflare',
3380
+ 'wizard.path.lan': '局域网 / Tailscale 直连',
3381
+ 'wizard.path.lanDesc': '在同一局域网或虚拟局域网内连接手机与另一台电脑,无需域名与公网证书',
3382
+ 'wizard.next': '下一步',
3383
+ 'wizard.back': '上一步',
3384
+ 'wizard.hostName': '服务端显示名称',
3385
+ 'wizard.hostNamePlaceholder': '例如:mac-mini',
3386
+ 'wizard.hostNameHint': '其他电脑或手机连入后在侧边栏看到的主机名称(可留空使用默认名称)',
3387
+ 'wizard.autoShare': '新建会话自动共享给已配对的桌面客户端',
3388
+ 'wizard.lanUrl': '局域网 / Tailscale 访问地址(可选)',
3389
+ 'wizard.lanUrlPlaceholder': 'http://192.168.1.10:3088',
3390
+ 'wizard.lanUrlHint': '填写本机局域网或 Tailscale 地址(如 http://192.168.1.10:3088),方便下一步直接生成手机扫码二维码与客户端连接信息',
3391
+ 'wizard.domain': '访问域名',
3392
+ 'wizard.domainHint': '填写完整域名,如 dsh.example.com(不带 https:// 和路径)',
3393
+ 'wizard.httpsPort': 'HTTPS 端口',
3394
+ 'wizard.cfToken': 'Cloudflare API 令牌',
3395
+ 'wizard.cfTokenHint': '用于自动申请证书或配置隧道,以 0600 权限保存在本机,不写入配置文件',
3396
+ 'wizard.cfTokenHowTo': '如何获取 API 令牌',
3397
+ 'wizard.serverUrl': '反代后的公网地址',
3398
+ 'wizard.serverUrlHint': '填写完整 HTTPS 根地址,如 https://dsh.example.com(不支持子目录)',
3399
+ 'wizard.proxySnippet': '查看 Nginx / Caddy / Lucky 配置参考',
3400
+ 'wizard.badDomain': '域名格式无效,请填写如 dsh.example.com 的域名',
3401
+ 'wizard.badPort': '端口号需为 1–65535 之间的整数',
3402
+ 'wizard.missingToken': '请填写 Cloudflare API 令牌',
3403
+ 'wizard.badServerUrl': '请填写有效的根路径地址(公网需 https://,局域网可用 http://,不含子目录)',
3404
+ 'wizard.verify.gateway': '本地网关就绪',
3405
+ 'wizard.verify.public': '访问地址连通',
3406
+ 'wizard.verify.pairing': '设备配对状态',
3407
+ 'wizard.light.pass': '正常',
3408
+ 'wizard.light.pending': '待验证',
3409
+ 'wizard.light.fail': '异常',
3410
+ 'wizard.pairTarget.web': '配对手机 / 浏览器(扫码)',
3411
+ 'wizard.pairTarget.desktop': '配对另一台电脑(桌面客户端)',
3412
+ 'wizard.startVerify': '生成配对二维码',
3413
+ 'wizard.reverify': '重新生成二维码',
3414
+ 'wizard.pairingCode': '配对码',
3415
+ 'wizard.pairingSeconds': '有效期剩余 {seconds} 秒',
3416
+ 'wizard.qrHint': '用手机相机或浏览器扫码打开配对页,输入上方配对码完成连接',
3417
+ 'wizard.verifyPass': '配对成功,设备 {deviceId} 已连接',
3418
+ 'wizard.verifyCookieInsecure': '手机已完成配对,但反向代理未传递 X-Forwarded-Proto: https,导致安全 Cookie 未生效。请检查转发头后重试',
3419
+ 'wizard.verifyExpired': '配对码已过期,请点击下方按钮重新生成',
3420
+ 'wizard.verifyStale': '接入配置已变更,请重新生成二维码',
3421
+ 'wizard.verifyPending': '请用手机扫描二维码并在页面中输入配对码…',
3422
+ 'wizard.verifyNotStarted': '点击下方按钮验证访问地址并生成手机配对二维码',
3423
+ 'wizard.desktop.addressLabel': '服务端连接地址(供客户端填写)',
3424
+ 'wizard.desktop.addressFallback': 'http://<本机局域网IP>:{port}',
3425
+ 'wizard.desktop.generate': '生成桌面客户端配对码',
3426
+ 'wizard.desktop.regenerate': '重新生成配对码',
3427
+ 'wizard.desktop.copyInfo': '复制地址与配对码',
3428
+ 'wizard.desktop.guide': '在另一台电脑打开本插件详情页,第 1 步选「作为桌面客户端」,第 2 步填入上方服务端地址与 8 位配对码即可完成连接。',
3429
+ 'wizard.desktop.pairedTitle': '已连接的桌面客户端({count})',
3430
+ 'wizard.desktop.pairedEmpty': '暂未配对桌面客户端',
3431
+ 'wizard.client.deviceName': '本机设备名称',
3432
+ 'wizard.client.deviceNamePlaceholder': '例如:macbook-pro',
3433
+ 'wizard.client.deviceNameHint': '配对后在服务端设备列表中显示的名称',
3434
+ 'wizard.client.serverUrl': '服务端地址',
3435
+ 'wizard.client.serverUrlPlaceholder': 'https://dsh.example.com 或 http://192.168.1.10:3088',
3436
+ 'wizard.client.serverUrlHint': '填写服务端的公网 HTTPS 地址,或局域网 / Tailscale 下的 http://<服务端IP>:3088',
3437
+ 'wizard.client.pairingCode': '8 位桌面配对码',
3438
+ 'wizard.client.pairingCodePlaceholder': '在服务端向导第 3 步切到「配对另一台电脑」生成',
3439
+ 'wizard.client.pairingCodeHint': '输入服务端生成的 8 位桌面客户端专用配对码(自动转大写,自动过滤 0/O/1/I/L 及符号)',
3440
+ 'wizard.client.pairAndConnect': '配对并连接服务端',
3441
+ 'wizard.client.pairingBusy': '正在配对并连接…',
3442
+ 'wizard.client.skipToStatus': '已配对,查看连接状态',
3443
+ 'wizard.client.lightCredential': '本地配对凭证',
3444
+ 'wizard.client.lightConnection': '服务端连接',
3445
+ 'wizard.client.lightSync': '会话与插件同步',
3446
+ 'wizard.client.reconfigure': '更换服务端 / 重新配对',
3447
+ 'wizard.startFailed': '验证未通过:{reason}',
3448
+ 'wizard.startFailedProbe': '无法访问该连接地址({detail})',
3449
+ 'wizard.startFailedRuntime': '入口服务尚未运行,请先在上方启动',
3450
+ 'wizard.startFailedGateway': '本地网关未响应,请检查网关运行状态',
3451
+ 'wizard.startFailedPair': '无法生成配对码({detail})',
3452
+ 'wizard.startFailedUrl': '公网地址格式无效',
3453
+ 'wizard.remoteOnly': '安全限制:公网接入向导仅限在服务端本机操作',
3454
+ 'wizard.notWired': '当前服务端版本不支持此功能,请升级后重试',
3455
+ 'wizard.card.proxyTitle': '反向代理配置参考',
3456
+ 'wizard.card.tokenTitle': '获取 Cloudflare API 令牌',
3457
+ 'wizard.card.copy': '复制配置',
3458
+ 'wizard.card.copied': '已复制',
3459
+ 'wizard.card.close': '关闭',
3460
+ 'wizard.card.tokenStep1': '登录 Cloudflare 控制台(dash.cloudflare.com),进入右上角「我的个人资料 → API 令牌」(用户令牌,以 cfut_ 开头),或左侧「管理账户 → 账户 API 令牌」(账户令牌,以 cfat_ 开头)',
3461
+ 'wizard.card.tokenStep2': '点击「创建令牌」(Create Token),选择「编辑区域 DNS」(Edit zone DNS)模板',
3462
+ 'wizard.card.tokenStep3': '在「权限」中保留默认的「区域(Zone)· DNS · 编辑(Edit)」权限',
3463
+ 'wizard.card.tokenStep4': '在「区域资源」(Zone Resources)中选择「包括(Include)· 特定区域(Specific zone)· 你的域名」',
3464
+ 'wizard.card.tokenStep5': '若使用 Cloudflare Tunnel,点击「+ 添加更多权限」补充下方所列的隧道、账户设置与区域读取权限',
3465
+ 'wizard.card.tokenStep6': '点击「继续以显示摘要」(Continue to summary),核对无误后点击「创建令牌」(Create Token)',
3466
+ 'wizard.card.tokenStep7': '复制生成的 API 令牌密钥(仅在创建完成页显示一次,关闭后无法再次查看)',
3467
+ 'wizard.card.tokenStep8': '粘贴到下方输入框并点击「保存令牌」',
3468
+ 'wizard.card.tokenPerms': '所需权限对照',
3469
+ 'wizard.card.tokenPermDns': '区域 · DNS · 编辑',
3470
+ 'wizard.card.tokenPermDnsWhy': '必选,用于 DNS-01 证书签发与解析记录配置',
3471
+ 'wizard.card.tokenPermTunnel': '账户 · Cloudflare Tunnel · 编辑',
3472
+ 'wizard.card.tokenPermTunnelWhy': '仅 Cloudflare Tunnel 方式需要',
3473
+ 'wizard.card.tokenPermAccount': '账户 · 账户设置 · 读取',
3474
+ 'wizard.card.tokenPermAccountWhy': '仅 Cloudflare Tunnel 方式需要(读取账户 ID)',
3475
+ 'wizard.card.tokenPermZoneRead': '区域 · 区域 · 读取',
3476
+ 'wizard.card.tokenPermZoneReadWhy': '仅 Cloudflare Tunnel 方式需要(定位域名所属区域)',
3477
+ 'wizard.card.tokenPaste': '粘贴 API 令牌',
3478
+ 'wizard.card.tokenUse': '保存令牌',
3479
+ 'wizard.card.migrateTitle': '将域名接入 Cloudflare',
3480
+ 'wizard.card.migrateStep1': '在域名注册商后台关闭 DNSSEC(若已开启),并确认域名未锁定 NS(名称服务器)修改',
3481
+ 'wizard.card.migrateStep2': '登录 Cloudflare 控制台,在左侧导航进入「域(Domains)→ 概览」,点击「接入域」(Onboard a domain)',
3482
+ 'wizard.card.migrateStep3': '输入你的根域名(如 example.com),选择快速扫描 DNS 记录,点击继续并选择 Free 计划',
3483
+ 'wizard.card.migrateStep4': '核对扫描出的 DNS 解析记录后继续,复制 Cloudflare 分配的两个名称服务器(Nameservers)地址',
3484
+ 'wizard.card.migrateStep5': '前往域名注册商后台,将原有 NS 替换为 Cloudflare 分配的两个名称服务器地址',
3485
+ 'wizard.card.migrateStep6': '等待状态变为「活动(Active)」(通常数分钟至数小时),返回此处点击「重新检测域名状态」',
3486
+ 'wizard.card.migrateRecheck': '重新检测域名状态',
3487
+ 'wizard.card.proxyCheck': '已配置完成,去验证',
3488
+ 'wizard.card.portForwardTitle': '路由器端口映射说明',
3489
+ 'wizard.card.portForwardStep1': '登录路由器管理后台(常见地址为 192.168.1.1 或 192.168.0.1)',
3490
+ 'wizard.card.portForwardStep2': '进入「端口转发 / 虚拟服务器 / NAT」设置页面',
3491
+ 'wizard.card.portForwardStep3': '新增 TCP 转发规则:外部端口 {port} 指向本机内网 IP 的 {port} 端口',
3492
+ 'wizard.card.portForwardStep4': '保存规则后,使用手机移动网络(关闭 Wi-Fi)测试公网连接',
3493
+ 'wizard.portForwardEntry': '非 443 端口:查看路由器端口映射说明',
3494
+ 'wizard.migrateEntry': '域名尚未接入 Cloudflare?',
3495
+ 'wizard.ingress': '入口服务',
3496
+ 'wizard.caddy.start': '启动 Caddy',
3497
+ 'wizard.caddy.starting': '正在下载并启动…',
3498
+ 'wizard.caddy.stop': '停止',
3499
+ 'wizard.caddy.running': 'Caddy 运行中',
3500
+ 'wizard.caddy.stopped': 'Caddy 已停止',
3501
+ 'wizard.caddy.neverStarted': 'Caddy 未启动',
3502
+ 'wizard.caddy.startOk': 'Caddy 已启动',
3503
+ 'wizard.caddy.startFailed': 'Caddy 启动失败:{reason}',
3504
+ 'wizard.caddy.failDownload': '自动下载失败,请手动安装后重试',
3505
+ 'wizard.caddy.failValidate': '生成的配置文件校验未通过',
3506
+ 'wizard.caddy.failAlreadyRunning': '服务已在运行中',
3507
+ 'wizard.caddy.failMissingToken': '请先在上一步保存 Cloudflare API 令牌',
3508
+ 'wizard.caddy.failNoDomain': '请先在上一步填写有效的访问域名',
3509
+ 'wizard.caddy.failBadConfig': '配置参数有误:{detail}',
3510
+ 'wizard.caddy.failSpawn': '无法拉起 Caddy 进程',
3511
+ 'wizard.token.saved': '已保存在本机',
3512
+ 'wizard.token.saveFailed': '令牌保存失败,请重试',
3513
+ 'wizard.tunnel.setup': '创建并启动隧道',
3514
+ 'wizard.tunnel.stop': '停止',
3515
+ 'wizard.tunnel.running': '隧道运行中',
3516
+ 'wizard.tunnel.stopped': '隧道已停止',
3517
+ 'wizard.tunnel.neverStarted': '隧道未启动',
3518
+ 'wizard.tunnel.ok': 'Cloudflare 隧道已就绪并开始运行',
3519
+ 'wizard.tunnel.conflict': '检测到同名隧道,请选择要复用的隧道:',
3520
+ 'wizard.tunnel.useExisting': '复用隧道',
3521
+ 'wizard.tunnel.zoneNotFound': '当前令牌下未找到该域名所属的 Cloudflare 区域,请确认域名已接入该账号',
3522
+ 'wizard.tunnel.dnsConflict': '该域名已存在指向 {detail} 的 CNAME 记录,请先在 Cloudflare DNS 中清理后再试',
3523
+ 'wizard.tunnel.auth': 'Cloudflare 认证失败,请检查令牌权限是否正确',
3524
+ 'wizard.tunnel.network': '无法连接 Cloudflare API,请检查网络',
3525
+ 'wizard.tunnel.api': 'Cloudflare 接口报错:{detail}',
3526
+ 'wizard.tunnel.spawnFailed': 'cloudflared 启动失败:{detail}',
3527
+ 'wizard.tunnel.noToken': '请先在上一步保存 Cloudflare API 令牌',
3528
+ 'wizard.tunnel.badDomain': '域名格式无效',
3529
+ 'wizard.tunnel.checking': '正在配置隧道并启动(首次自动下载 cloudflared)…',
3530
+ 'wizard.check.okOnCf': '已确认域名托管在 Cloudflare',
3531
+ 'wizard.check.okNotOnCf': '未在 Cloudflare 中找到该域名,请先完成域名接入',
3532
+ 'wizard.check.failed': '域名检测失败:{reason}',
3533
+ 'wizard.overview.roleLabel': '当前角色',
3534
+ 'wizard.overview.gatewayLabel': '本地网关',
3535
+ 'wizard.overview.gatewayReady': '运行正常 · 端口 {port}',
3536
+ 'wizard.overview.gatewayDown': '未响应',
3537
+ 'wizard.overview.ingressLabel': '接入方式与地址',
3538
+ 'wizard.overview.devicesLabel': '已配对设备',
3539
+ 'wizard.overview.devicesSummary': '手机/网页 {web} 台 · 桌面客户端 {desktop} 台',
3540
+ 'wizard.overview.devicesNone': '暂未配对设备',
3541
+ 'wizard.overview.clientCredLabel': '本地配对凭证',
3542
+ 'wizard.overview.clientConnLabel': '服务端连接',
3543
+ 'wizard.overview.clientServerLabel': '远端服务端地址',
3544
+ 'wizard.overview.startSetup': '开始配置向导',
3545
+ 'wizard.overview.reconfigure': '修改接入配置',
3546
+ 'wizard.overview.pairPhone': '配对手机 / 扫码',
3547
+ 'wizard.overview.pairDesktop': '配对另一台电脑',
3548
+ 'wizard.overview.clientDetail': '连接详情与配对向导',
3549
+ 'wizard.overview.emptyIntro': '尚未完成远程访问配置。点击右侧按钮启动向导,按步骤配置本机作为服务端(开放手机与另一台电脑连入)或作为桌面客户端连接远端主机。',
3550
+ 'wizard.modal.title': '远程访问与配对向导',
3551
+ 'wizard.modal.stepTag': '第 {num} 步',
3552
+ 'wizard.modal.step1Desc': '选择本机作为服务端还是桌面客户端;若作为服务端,选择外部设备的连入方式',
3553
+ 'wizard.modal.step2Desc': '根据上一步选择的方案,填写对应的连接地址、域名或配对凭证',
3554
+ 'wizard.modal.step3Desc': '检测连通状态,并扫码配对手机或生成桌面客户端配对码',
3555
+ 'wizard.modal.selectedPlan': '已选方案:{plan}',
3556
+ 'wizard.modal.cancel': '取消',
3557
+ 'wizard.modal.saveAndNext': '保存并进入验证',
3558
+ 'wizard.modal.finish': '完成',
3559
+ 'wizard.light.probing': '检测中…',
3560
+ 'wizard.overview.manageDevices': '管理设备',
3561
+ 'wizard.devicesModal.title': '已配对设备管理',
3562
+ 'wizard.devicesModal.subtitle': '查看与管理已配对的手机/网页设备及桌面客户端,支持改名、切换布局或吊销凭证',
3563
+ 'wizard.devicesModal.retest': '重新测试连通性',
3352
3564
  };
3353
3565
  /** English dictionary, key-identical to the Chinese source of truth. */
3354
3566
  exports.en = {
@@ -3543,7 +3755,7 @@ exports.en = {
3543
3755
  // T60: the pairing button's disabled reason while the device-name draft is invalid.
3544
3756
  'settings.client.fixDeviceName': 'Fix the device name first',
3545
3757
  'settings.client.pairingCode': 'Pairing code',
3546
- 'settings.client.pairingCodeHint': 'Generate a desktop-client pairing code on the server\u2019s settings page and enter it here (8 characters, uppercased automatically).',
3758
+ 'settings.client.pairingCodeHint': 'Generate a desktop-client pairing code on the server\u2019s settings page and enter it here (8 characters; auto-uppercased and filters out 0/O/1/I/L and symbols).',
3547
3759
  'settings.client.pair': 'Pair',
3548
3760
  'settings.client.pairing': 'Pairing…',
3549
3761
  'settings.client.failInvalid': 'The server address is not valid',
@@ -3654,6 +3866,210 @@ exports.en = {
3654
3866
  'downloadToLocal': 'Download to this computer',
3655
3867
  'downloadBusy': 'Downloading…',
3656
3868
  'downloadFailed': 'Download failed',
3869
+ // --- GW-10: remote access & device pairing wizard ---
3870
+ 'wizard.title': 'Remote Access & Device Pairing',
3871
+ 'wizard.subtitle': 'Set up this machine as a server (for phones and other computers) or as a client connected to a remote host',
3872
+ 'wizard.off': 'LAN mode / Not configured',
3873
+ 'wizard.step.pick': 'Role & Access',
3874
+ 'wizard.step.configure': 'Connection Settings',
3875
+ 'wizard.step.verify': 'Verify & Pair',
3876
+ 'wizard.role.sectionTitle': 'Machine Role',
3877
+ 'wizard.role.host': 'Server (Host)',
3878
+ 'wizard.role.hostDesc': 'Runs main sessions and the gateway on this machine so phones or other DSH computers can connect',
3879
+ 'wizard.role.client': 'Desktop Client',
3880
+ 'wizard.role.clientDesc': 'Connects to another DSH server so shared remote sessions appear directly in this sidebar',
3881
+ 'wizard.role.clientPickNote': 'In Client mode, this machine connects to a remote server gateway over HTTPS or LAN / Tailscale. Click "Next" to enter the server address and pairing code.',
3882
+ 'wizard.ingressSectionTitle': 'Server Access Method',
3883
+ 'wizard.path.caddy': 'Managed Caddy (Recommended)',
3884
+ 'wizard.path.caddyDesc': 'Best when you have a public IP. Downloads Caddy and issues TLS certificates via Cloudflare DNS automatically',
3885
+ 'wizard.path.external': 'Existing Reverse Proxy',
3886
+ 'wizard.path.externalDesc': 'Already running Nginx, Caddy, or Lucky? Copy a ready-made proxy rule into your existing setup',
3887
+ 'wizard.path.cloudflared': 'Cloudflare Tunnel',
3888
+ 'wizard.path.cloudflaredDesc': 'No public IP or router port forwarding needed. Connects directly through a Cloudflare tunnel',
3889
+ 'wizard.path.lan': 'LAN / Tailscale Direct',
3890
+ 'wizard.path.lanDesc': 'Connect phones and computers on the same LAN or Tailscale network without a domain or TLS certificate',
3891
+ 'wizard.next': 'Next',
3892
+ 'wizard.back': 'Back',
3893
+ 'wizard.hostName': 'Server Display Name',
3894
+ 'wizard.hostNamePlaceholder': 'e.g. mac-mini',
3895
+ 'wizard.hostNameHint': 'Name shown in the sidebar of connected computers and phones (leave blank for default)',
3896
+ 'wizard.autoShare': 'Automatically share new sessions with paired desktop clients',
3897
+ 'wizard.lanUrl': 'LAN / Tailscale URL (optional)',
3898
+ 'wizard.lanUrlPlaceholder': 'http://192.168.1.10:3088',
3899
+ 'wizard.lanUrlHint': 'Enter this machine\u2019s LAN or Tailscale URL (e.g. http://192.168.1.10:3088) to generate a phone QR code and copyable client address in the next step',
3900
+ 'wizard.domain': 'Domain',
3901
+ 'wizard.domainHint': 'Full domain name, e.g. dsh.example.com (without https:// or path)',
3902
+ 'wizard.httpsPort': 'HTTPS Port',
3903
+ 'wizard.cfToken': 'Cloudflare API Token',
3904
+ 'wizard.cfTokenHint': 'Used for certificate issuance or tunnel setup. Stored locally (0600) and never written to config files',
3905
+ 'wizard.cfTokenHowTo': 'How to create an API token',
3906
+ 'wizard.serverUrl': 'Public HTTPS URL',
3907
+ 'wizard.serverUrlHint': 'Root HTTPS URL, e.g. https://dsh.example.com (subdirectories are not supported)',
3908
+ 'wizard.proxySnippet': 'View Nginx / Caddy / Lucky config snippets',
3909
+ 'wizard.badDomain': 'Invalid domain format (e.g. dsh.example.com)',
3910
+ 'wizard.badPort': 'Port must be an integer between 1 and 65535',
3911
+ 'wizard.missingToken': 'Enter your Cloudflare API token',
3912
+ 'wizard.badServerUrl': 'Enter a valid root URL (https:// for public, http:// for LAN, no path)',
3913
+ 'wizard.verify.gateway': 'Local gateway',
3914
+ 'wizard.verify.public': 'Access URL reachable',
3915
+ 'wizard.verify.pairing': 'Device paired',
3916
+ 'wizard.light.pass': 'Ready',
3917
+ 'wizard.light.pending': 'Waiting',
3918
+ 'wizard.light.fail': 'Failed',
3919
+ 'wizard.pairTarget.web': 'Pair Phone / Browser (QR)',
3920
+ 'wizard.pairTarget.desktop': 'Pair Another Computer (Desktop Client)',
3921
+ 'wizard.startVerify': 'Generate Pairing QR Code',
3922
+ 'wizard.reverify': 'Regenerate QR Code',
3923
+ 'wizard.pairingCode': 'Pairing code',
3924
+ 'wizard.pairingSeconds': 'Expires in {seconds}s',
3925
+ 'wizard.qrHint': 'Scan with your phone to open the pairing page, then enter the code above',
3926
+ 'wizard.verifyPass': 'Paired — device {deviceId} connected',
3927
+ 'wizard.verifyCookieInsecure': 'Paired, but the reverse proxy did not pass X-Forwarded-Proto: https. Add the header so secure cookies work, then verify again',
3928
+ 'wizard.verifyExpired': 'Pairing code expired — generate a new one below',
3929
+ 'wizard.verifyStale': 'Configuration changed — generate a new QR code',
3930
+ 'wizard.verifyPending': 'Scan the QR code on your phone and enter the pairing code…',
3931
+ 'wizard.verifyNotStarted': 'Click the button below to verify the access URL and generate a phone pairing QR code',
3932
+ 'wizard.desktop.addressLabel': 'Server Address (for client machine)',
3933
+ 'wizard.desktop.addressFallback': 'http://<LAN-IP>:{port}',
3934
+ 'wizard.desktop.generate': 'Generate Desktop Pairing Code',
3935
+ 'wizard.desktop.regenerate': 'Regenerate Code',
3936
+ 'wizard.desktop.copyInfo': 'Copy Address & Code',
3937
+ '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.',
3938
+ 'wizard.desktop.pairedTitle': 'Paired Desktop Clients ({count})',
3939
+ 'wizard.desktop.pairedEmpty': 'No desktop clients paired yet',
3940
+ 'wizard.client.deviceName': 'This Computer\u2019s Name',
3941
+ 'wizard.client.deviceNamePlaceholder': 'e.g. macbook-pro',
3942
+ 'wizard.client.deviceNameHint': 'Shown in the server\u2019s paired device list',
3943
+ 'wizard.client.serverUrl': 'Server Address',
3944
+ 'wizard.client.serverUrlPlaceholder': 'https://dsh.example.com or http://192.168.1.10:3088',
3945
+ 'wizard.client.serverUrlHint': 'Enter the server\u2019s public HTTPS URL or LAN / Tailscale http://<IP>:3088 address',
3946
+ 'wizard.client.pairingCode': '8-Character Desktop Pairing Code',
3947
+ 'wizard.client.pairingCodePlaceholder': 'Generated under "Pair Another Computer" in Step 3 on the server',
3948
+ 'wizard.client.pairingCodeHint': 'Enter the 8-character desktop client code generated on the server (auto-uppercased; 0/O/1/I/L and symbols are filtered out)',
3949
+ 'wizard.client.pairAndConnect': 'Pair & Connect',
3950
+ 'wizard.client.pairingBusy': 'Pairing & connecting…',
3951
+ 'wizard.client.skipToStatus': 'Already Paired — View Status',
3952
+ 'wizard.client.lightCredential': 'Pairing token',
3953
+ 'wizard.client.lightConnection': 'Server connection',
3954
+ 'wizard.client.lightSync': 'Session & plugin sync',
3955
+ 'wizard.client.reconfigure': 'Change Server / Re-pair',
3956
+ 'wizard.startFailed': 'Verification failed: {reason}',
3957
+ 'wizard.startFailedProbe': 'access URL could not reach this gateway ({detail})',
3958
+ 'wizard.startFailedRuntime': 'ingress service is not running — start it above first',
3959
+ 'wizard.startFailedGateway': 'local gateway did not respond',
3960
+ 'wizard.startFailedPair': 'failed to generate pairing code ({detail})',
3961
+ 'wizard.startFailedUrl': 'invalid public URL',
3962
+ 'wizard.remoteOnly': 'Security restriction: public access setup is only available directly on the host machine',
3963
+ 'wizard.notWired': 'This feature requires a newer server version',
3964
+ 'wizard.card.proxyTitle': 'Reverse Proxy Configuration',
3965
+ 'wizard.card.tokenTitle': 'Create a Cloudflare API Token',
3966
+ 'wizard.card.copy': 'Copy',
3967
+ 'wizard.card.copied': 'Copied',
3968
+ 'wizard.card.close': 'Close',
3969
+ 'wizard.card.tokenStep1': 'In the Cloudflare dashboard (dash.cloudflare.com), open "My Profile → API Tokens" (user token, cfut_) or "Manage Account → Account API tokens" (account token, cfat_)',
3970
+ 'wizard.card.tokenStep2': 'Select "Create Token" and choose the "Edit zone DNS" template',
3971
+ 'wizard.card.tokenStep3': 'Under Permissions, keep the default "Zone · DNS · Edit" permission',
3972
+ 'wizard.card.tokenStep4': 'Under "Zone Resources", select "Include · Specific zone · your domain"',
3973
+ 'wizard.card.tokenStep5': 'If using Cloudflare Tunnel, click "+ Add more" to add the tunnel, account settings, and zone read permissions listed below',
3974
+ 'wizard.card.tokenStep6': 'Select "Continue to summary", review the details, and select "Create Token"',
3975
+ 'wizard.card.tokenStep7': 'Copy the generated token secret (shown only once)',
3976
+ 'wizard.card.tokenStep8': 'Paste it below and click "Save Token"',
3977
+ 'wizard.card.tokenPerms': 'Required permissions',
3978
+ 'wizard.card.tokenPermDns': 'Zone · DNS · Edit',
3979
+ 'wizard.card.tokenPermDnsWhy': 'Required for DNS-01 certificate issuance and DNS records',
3980
+ 'wizard.card.tokenPermTunnel': 'Account · Cloudflare Tunnel · Edit',
3981
+ 'wizard.card.tokenPermTunnelWhy': 'Cloudflare Tunnel only',
3982
+ 'wizard.card.tokenPermAccount': 'Account · Account Settings · Read',
3983
+ 'wizard.card.tokenPermAccountWhy': 'Cloudflare Tunnel only (reads account ID)',
3984
+ 'wizard.card.tokenPermZoneRead': 'Zone · Zone · Read',
3985
+ 'wizard.card.tokenPermZoneReadWhy': 'Cloudflare Tunnel only (locates domain zone)',
3986
+ 'wizard.card.tokenPaste': 'Paste API token',
3987
+ 'wizard.card.tokenUse': 'Save Token',
3988
+ 'wizard.card.migrateTitle': 'Add Your Domain to Cloudflare',
3989
+ 'wizard.card.migrateStep1': 'Turn off DNSSEC at your domain registrar (if active) and ensure nameserver changes are unlocked',
3990
+ 'wizard.card.migrateStep2': 'In the Cloudflare dashboard, go to "Domains → Overview" and select "Onboard a domain"',
3991
+ 'wizard.card.migrateStep3': 'Enter your apex domain (e.g. example.com), scan DNS records, click Continue, and select the Free plan',
3992
+ 'wizard.card.migrateStep4': 'Review the scanned DNS records, continue, and copy the two assigned Cloudflare nameservers',
3993
+ 'wizard.card.migrateStep5': 'At your domain registrar, replace the existing nameservers with the two Cloudflare nameservers',
3994
+ 'wizard.card.migrateStep6': 'Once the domain status becomes "Active" on the Domains page, return here and click "Recheck Domain"',
3995
+ 'wizard.card.migrateRecheck': 'Recheck Domain',
3996
+ 'wizard.card.proxyCheck': 'Configured — Verify Now',
3997
+ 'wizard.card.portForwardTitle': 'Router Port Forwarding',
3998
+ 'wizard.card.portForwardStep1': 'Sign in to your router admin page (typically 192.168.1.1 or 192.168.0.1)',
3999
+ 'wizard.card.portForwardStep2': 'Open "Port Forwarding / Virtual Server / NAT"',
4000
+ 'wizard.card.portForwardStep3': 'Add a TCP rule: external port {port} → this machine\u2019s LAN IP on port {port}',
4001
+ 'wizard.card.portForwardStep4': 'Save the rule and test the public URL over mobile data (with Wi-Fi turned off)',
4002
+ 'wizard.portForwardEntry': 'Non-443 port: router port forwarding guide',
4003
+ 'wizard.migrateEntry': 'Domain not on Cloudflare yet?',
4004
+ 'wizard.ingress': 'Ingress service',
4005
+ 'wizard.caddy.start': 'Start Caddy',
4006
+ 'wizard.caddy.starting': 'Downloading & starting…',
4007
+ 'wizard.caddy.stop': 'Stop',
4008
+ 'wizard.caddy.running': 'Caddy running',
4009
+ 'wizard.caddy.stopped': 'Caddy stopped',
4010
+ 'wizard.caddy.neverStarted': 'Caddy not started',
4011
+ 'wizard.caddy.startOk': 'Caddy started',
4012
+ 'wizard.caddy.startFailed': 'Caddy failed to start: {reason}',
4013
+ 'wizard.caddy.failDownload': 'auto-download failed — install Caddy manually and retry',
4014
+ 'wizard.caddy.failValidate': 'generated config failed validation',
4015
+ 'wizard.caddy.failAlreadyRunning': 'already running',
4016
+ 'wizard.caddy.failMissingToken': 'save your Cloudflare API token in the previous step first',
4017
+ 'wizard.caddy.failNoDomain': 'save a valid domain in the previous step first',
4018
+ 'wizard.caddy.failBadConfig': 'invalid configuration: {detail}',
4019
+ 'wizard.caddy.failSpawn': 'failed to start Caddy process',
4020
+ 'wizard.token.saved': 'Saved locally',
4021
+ 'wizard.token.saveFailed': 'Failed to save token — please retry',
4022
+ 'wizard.tunnel.setup': 'Create & Start Tunnel',
4023
+ 'wizard.tunnel.stop': 'Stop',
4024
+ 'wizard.tunnel.running': 'Tunnel running',
4025
+ 'wizard.tunnel.stopped': 'Tunnel stopped',
4026
+ 'wizard.tunnel.neverStarted': 'Tunnel not started',
4027
+ 'wizard.tunnel.ok': 'Cloudflare tunnel is active and running',
4028
+ 'wizard.tunnel.conflict': 'A tunnel with this name already exists — select one to reuse:',
4029
+ 'wizard.tunnel.useExisting': 'Reuse tunnel',
4030
+ 'wizard.tunnel.zoneNotFound': 'Domain zone not found under this token — make sure the domain is active on this Cloudflare account',
4031
+ 'wizard.tunnel.dnsConflict': 'A CNAME record for this domain already points to {detail} — remove it in Cloudflare DNS first',
4032
+ 'wizard.tunnel.auth': 'Cloudflare authentication failed — check token permissions',
4033
+ 'wizard.tunnel.network': 'Could not reach Cloudflare API',
4034
+ 'wizard.tunnel.api': 'Cloudflare API error: {detail}',
4035
+ 'wizard.tunnel.spawnFailed': 'cloudflared failed to start: {detail}',
4036
+ 'wizard.tunnel.noToken': 'Save your Cloudflare API token in the previous step first',
4037
+ 'wizard.tunnel.badDomain': 'Invalid domain format',
4038
+ 'wizard.tunnel.checking': 'Configuring tunnel & starting (downloading cloudflared on first run)…',
4039
+ 'wizard.check.okOnCf': 'Domain is active on Cloudflare',
4040
+ 'wizard.check.okNotOnCf': 'Domain not found on Cloudflare — add it to Cloudflare first',
4041
+ 'wizard.check.failed': 'Domain check failed: {reason}',
4042
+ 'wizard.overview.roleLabel': 'Current Role',
4043
+ 'wizard.overview.gatewayLabel': 'Local Gateway',
4044
+ 'wizard.overview.gatewayReady': 'Running · port {port}',
4045
+ 'wizard.overview.gatewayDown': 'Unreachable',
4046
+ 'wizard.overview.ingressLabel': 'Access Method & URL',
4047
+ 'wizard.overview.devicesLabel': 'Paired Devices',
4048
+ 'wizard.overview.devicesSummary': 'Phone/Web {web} · Desktop {desktop}',
4049
+ 'wizard.overview.devicesNone': 'No paired devices yet',
4050
+ 'wizard.overview.clientCredLabel': 'Local Credential',
4051
+ 'wizard.overview.clientConnLabel': 'Server Connection',
4052
+ 'wizard.overview.clientServerLabel': 'Remote Server URL',
4053
+ 'wizard.overview.startSetup': 'Start Setup Wizard',
4054
+ 'wizard.overview.reconfigure': 'Reconfigure Wizard',
4055
+ 'wizard.overview.pairPhone': 'Pair Phone (QR)',
4056
+ 'wizard.overview.pairDesktop': 'Pair Desktop Client',
4057
+ 'wizard.overview.clientDetail': 'Connection & Pairing Wizard',
4058
+ 'wizard.overview.emptyIntro': 'Remote access is not configured yet. Click the button on the right to launch the step-by-step wizard for server access or desktop client pairing.',
4059
+ 'wizard.modal.title': 'Remote Access & Pairing Wizard',
4060
+ 'wizard.modal.stepTag': 'Step {num}',
4061
+ 'wizard.modal.step1Desc': 'Choose whether this machine acts as the server or a desktop client, and select how devices connect',
4062
+ 'wizard.modal.step2Desc': 'Fill in the connection URL, domain, or pairing code for your selected plan',
4063
+ 'wizard.modal.step3Desc': 'Verify connectivity and pair your phone or desktop client',
4064
+ 'wizard.modal.selectedPlan': 'Selected plan: {plan}',
4065
+ 'wizard.modal.cancel': 'Cancel',
4066
+ 'wizard.modal.saveAndNext': 'Save & Verify',
4067
+ 'wizard.modal.finish': 'Done',
4068
+ 'wizard.light.probing': 'Checking…',
4069
+ 'wizard.overview.manageDevices': 'Manage Devices',
4070
+ 'wizard.devicesModal.title': 'Paired Devices',
4071
+ 'wizard.devicesModal.subtitle': 'View and manage paired phone/web and desktop client devices — rename, switch layout, or revoke access',
4072
+ 'wizard.devicesModal.retest': 'Retest Connectivity',
3657
4073
  };
3658
4074
  };
3659
4075
  __modules["client-data/shares.js"] = function (require, module, exports) {
@@ -17197,6 +17613,2865 @@ function registerSettingsPage(ctx, section) {
17197
17613
  });
17198
17614
  }
17199
17615
  };
17616
+ __modules["client-data/qr.js"] = function (require, module, exports) {
17617
+ "use strict";
17618
+ /**
17619
+ * Minimal QR encoder (GW-10): byte mode, versions 1–5, ECC level L, fixed
17620
+ * mask 0 — enough for any sane origin URL (106 bytes at v5) while keeping
17621
+ * the whole implementation single-block (no interleave) and reviewable.
17622
+ * Returns the module matrix (true = dark) or null when the text does not
17623
+ * fit. Pure: the check script verifies it at decoder level (RS syndromes
17624
+ * all-zero + data recovery + format-info BCH), which is stronger than any
17625
+ * memorized reference vector.
17626
+ *
17627
+ * Standard QR constants only: sizes 21/25/29/33/37, data codewords
17628
+ * 19/34/55/80/108, ECC codewords 7/10/15/20/26, alignment centers
17629
+ * [6,18]/[6,22]/[6,26]/[6,30], Galois field 0x11d, format-info generator
17630
+ * 0x537 XOR-mask 0x5412.
17631
+ */
17632
+ Object.defineProperty(exports, "__esModule", { value: true });
17633
+ exports.qrMatrix = qrMatrix;
17634
+ const VERSIONS = [
17635
+ { size: 21, dataCodewords: 19, eccCodewords: 7, alignment: null, capacity: 17 },
17636
+ { size: 25, dataCodewords: 34, eccCodewords: 10, alignment: [6, 18], capacity: 32 },
17637
+ { size: 29, dataCodewords: 55, eccCodewords: 15, alignment: [6, 22], capacity: 53 },
17638
+ { size: 33, dataCodewords: 80, eccCodewords: 20, alignment: [6, 26], capacity: 78 },
17639
+ { size: 37, dataCodewords: 108, eccCodewords: 26, alignment: [6, 30], capacity: 106 },
17640
+ ];
17641
+ // --- Galois field GF(256), polynomial 0x11d -----------------------------------
17642
+ const EXP = new Uint8Array(512);
17643
+ const LOG = new Uint8Array(256);
17644
+ {
17645
+ let value = 1;
17646
+ for (let exponent = 0; exponent < 255; exponent++) {
17647
+ EXP[exponent] = value;
17648
+ LOG[value] = exponent;
17649
+ value <<= 1;
17650
+ if (value & 0x100)
17651
+ value ^= 0x11d;
17652
+ }
17653
+ for (let exponent = 255; exponent < 512; exponent++)
17654
+ EXP[exponent] = EXP[exponent - 255] ?? 0;
17655
+ }
17656
+ function gfMul(a, b) {
17657
+ if (a === 0 || b === 0)
17658
+ return 0;
17659
+ const la = LOG[a];
17660
+ const lb = LOG[b];
17661
+ if (la === undefined || lb === undefined)
17662
+ return 0;
17663
+ return EXP[la + lb] ?? 0;
17664
+ }
17665
+ /** Reed–Solomon remainder of `data` under the generator for `eccLen` check
17666
+ * codewords (LFSR division). The generator is ∏(x − α^i), i = 0…eccLen−1,
17667
+ * coefficients high-order first; g[j]·x lands at index j, g[j]·α^i at
17668
+ * index j+1. */
17669
+ function rsRemainder(data, eccLen) {
17670
+ let generator = [1];
17671
+ for (let i = 0; i < eccLen; i++) {
17672
+ const next = new Array(generator.length + 1).fill(0);
17673
+ for (let j = 0; j < generator.length; j++) {
17674
+ const coefficient = generator[j];
17675
+ if (coefficient === undefined)
17676
+ continue;
17677
+ const low = next[j] ?? 0;
17678
+ const high = next[j + 1] ?? 0;
17679
+ next[j] = low ^ coefficient;
17680
+ next[j + 1] = high ^ gfMul(coefficient, EXP[i] ?? 0);
17681
+ }
17682
+ generator = next;
17683
+ }
17684
+ const remainder = new Uint8Array(eccLen);
17685
+ for (const byte of data) {
17686
+ const factor = byte ^ (remainder[0] ?? 0);
17687
+ remainder.copyWithin(0, 1);
17688
+ remainder[eccLen - 1] = 0;
17689
+ if (factor !== 0) {
17690
+ for (let i = 0; i < eccLen; i++) {
17691
+ remainder[i] = (remainder[i] ?? 0) ^ gfMul(generator[i + 1] ?? 0, factor);
17692
+ }
17693
+ }
17694
+ }
17695
+ return remainder;
17696
+ }
17697
+ // --- bit assembly --------------------------------------------------------------
17698
+ class BitBuffer {
17699
+ bits = [];
17700
+ push(value, length) {
17701
+ for (let shift = length - 1; shift >= 0; shift--)
17702
+ this.bits.push((value >>> shift) & 1);
17703
+ }
17704
+ }
17705
+ function buildCodewords(text, version) {
17706
+ const buffer = new BitBuffer();
17707
+ buffer.push(0b0100, 4); // byte mode
17708
+ buffer.push(text.length, 8);
17709
+ for (const byte of text)
17710
+ buffer.push(byte, 8);
17711
+ // Terminator (up to 4 zero bits), then pad to a byte boundary.
17712
+ buffer.push(0, Math.min(4, version.dataCodewords * 8 - buffer.bits.length));
17713
+ while (buffer.bits.length % 8 !== 0)
17714
+ buffer.push(0, 1);
17715
+ const bytes = [];
17716
+ for (let i = 0; i < buffer.bits.length; i += 8) {
17717
+ let byte = 0;
17718
+ for (let j = 0; j < 8; j++)
17719
+ byte = (byte << 1) | (buffer.bits[i + j] ?? 0);
17720
+ bytes.push(byte);
17721
+ }
17722
+ // Pad alternately 0xEC / 0x11 to the data capacity.
17723
+ for (let pad = 0; bytes.length < version.dataCodewords; pad++)
17724
+ bytes.push(pad % 2 === 0 ? 0xec : 0x11);
17725
+ const remainder = rsRemainder(Uint8Array.from(bytes), version.eccCodewords);
17726
+ return Uint8Array.from([...bytes, ...remainder]);
17727
+ }
17728
+ function blankGrid(size) {
17729
+ return Array.from({ length: size }, () => new Array(size).fill(null));
17730
+ }
17731
+ function placeFinder(grid, row, col) {
17732
+ const n = grid.length;
17733
+ for (let dr = -1; dr <= 7; dr++) {
17734
+ for (let dc = -1; dc <= 7; dc++) {
17735
+ const r = row + dr;
17736
+ const c = col + dc;
17737
+ if (r < 0 || r >= n || c < 0 || c >= n)
17738
+ continue;
17739
+ const inRing = dr >= 0 && dr <= 6 && (dc === 0 || dc === 6);
17740
+ const inRing2 = dc >= 0 && dc <= 6 && (dr === 0 || dr === 6);
17741
+ const inCore = dr >= 2 && dr <= 4 && dc >= 2 && dc <= 4;
17742
+ const target = grid[r];
17743
+ if (target !== undefined)
17744
+ target[c] = inRing || inRing2 || inCore;
17745
+ }
17746
+ }
17747
+ }
17748
+ function placeAlignment(grid, centers) {
17749
+ for (const row of centers) {
17750
+ for (const col of centers) {
17751
+ // Skip the centers that sit inside a finder's 7×7 (only possible at
17752
+ // the three corners; versions 2–5 have none).
17753
+ let overlapsFinder = false;
17754
+ for (const [fr, fc] of [[0, 0], [0, grid.length - 7], [grid.length - 7, 0]]) {
17755
+ if (row >= fr - 1 && row <= fr + 7 && col >= fc - 1 && col <= fc + 7)
17756
+ overlapsFinder = true;
17757
+ }
17758
+ if (overlapsFinder)
17759
+ continue;
17760
+ for (let dr = -2; dr <= 2; dr++) {
17761
+ for (let dc = -2; dc <= 2; dc++) {
17762
+ const target = grid[row + dr];
17763
+ if (target !== undefined)
17764
+ target[col + dc] = Math.max(Math.abs(dr), Math.abs(dc)) !== 1;
17765
+ }
17766
+ }
17767
+ }
17768
+ }
17769
+ }
17770
+ function reserveFormatAreas(grid) {
17771
+ const n = grid.length;
17772
+ // `false` marks RESERVED (kept from the data placer, overwritten by the
17773
+ // real format bits later); null stays "free for data".
17774
+ const put = (row, col, value) => {
17775
+ const target = grid[row];
17776
+ if (target !== undefined)
17777
+ target[col] = value;
17778
+ };
17779
+ for (let i = 0; i <= 8; i++) {
17780
+ if (i !== 6)
17781
+ put(8, i, false);
17782
+ if (i !== 6)
17783
+ put(i, 8, false);
17784
+ }
17785
+ for (let i = 0; i < 8; i++) {
17786
+ put(n - 1 - i, 8, false);
17787
+ // NOTE: unlike the first copy there is NO skip here — the second copy
17788
+ // crosses no timing pattern, and leaving one cell unreserved (the
17789
+ // first-copy rule pasted over) would let a data bit land inside it and
17790
+ // desynchronize the whole stream (v2+).
17791
+ put(8, n - 1 - i, false);
17792
+ }
17793
+ put(n - 8, 8, true); // the fixed dark module
17794
+ }
17795
+ function formatBits(mask) {
17796
+ const data = (0b01 << 3) | mask; // ECC level L
17797
+ let rem = data << 10;
17798
+ for (let i = 4; i >= 0; i--) {
17799
+ if ((rem >>> (i + 10)) & 1)
17800
+ rem ^= 0x537 << i;
17801
+ }
17802
+ return ((data << 10) | rem) ^ 0x5412;
17803
+ }
17804
+ function placeFormat(grid, mask) {
17805
+ const n = grid.length;
17806
+ const bits = formatBits(mask);
17807
+ // ISO/IEC 18004 Figure 25: bit 14 (MSB) sits at (8, 0) / (n - 1, 8) and
17808
+ // bit 0 (LSB) sits at (0, 8) / (8, n - 1). Placing LSB-first reverses the
17809
+ // 15-bit BCH codeword and makes every real scanner reject the symbol.
17810
+ const bit = (index) => ((bits >>> index) & 1) === 1;
17811
+ const put = (row, col, value) => {
17812
+ const target = grid[row];
17813
+ if (target !== undefined)
17814
+ target[col] = value;
17815
+ };
17816
+ for (let i = 0; i <= 5; i++)
17817
+ put(8, i, bit(14 - i));
17818
+ put(8, 7, bit(8));
17819
+ put(8, 8, bit(7));
17820
+ put(7, 8, bit(6));
17821
+ for (let i = 9; i <= 14; i++)
17822
+ put(14 - i, 8, bit(14 - i));
17823
+ for (let i = 0; i <= 6; i++)
17824
+ put(n - 1 - i, 8, bit(14 - i));
17825
+ for (let i = 7; i <= 14; i++)
17826
+ put(8, n - 15 + i, bit(14 - i));
17827
+ }
17828
+ function placeData(grid, codewords) {
17829
+ const n = grid.length;
17830
+ // Fixed mask 0: (row + col) even flips. Any mask decodes; the penalty
17831
+ // optimization is optional and this keeps the implementation small.
17832
+ const maskFlips = (row, col) => (row + col) % 2 === 0;
17833
+ let bitIndex = 7;
17834
+ let byteIndex = 0;
17835
+ let increment = -1;
17836
+ let row = n - 1;
17837
+ // The standard two-column zigzag: from the bottom-right corner upward,
17838
+ // column PAIR by column PAIR, skipping the timing column pair (6).
17839
+ for (let right = n - 1; right >= 0; right -= 2) {
17840
+ if (right === 6)
17841
+ right = 5;
17842
+ // Fill the pair top-to-bottom or bottom-to-top until the edge.
17843
+ while (true) {
17844
+ const cells = grid[row];
17845
+ // `row` is always in range here (the while loop flips before that can
17846
+ // change); the guard only keeps a future invariant break non-fatal.
17847
+ if (cells === undefined)
17848
+ break;
17849
+ for (let col = right; col >= right - 1; col--) {
17850
+ const current = cells[col];
17851
+ if (current !== true && current !== false) {
17852
+ let dark = false;
17853
+ const byte = codewords[byteIndex];
17854
+ if (byte !== undefined && byteIndex < codewords.length)
17855
+ dark = ((byte >>> bitIndex) & 1) === 1;
17856
+ if (maskFlips(row, col))
17857
+ dark = !dark;
17858
+ cells[col] = dark;
17859
+ bitIndex -= 1;
17860
+ if (bitIndex === -1) {
17861
+ byteIndex += 1;
17862
+ bitIndex = 7;
17863
+ }
17864
+ }
17865
+ }
17866
+ row += increment;
17867
+ if (row < 0 || row >= n) {
17868
+ row -= increment;
17869
+ increment = -increment;
17870
+ break;
17871
+ }
17872
+ }
17873
+ }
17874
+ }
17875
+ /**
17876
+ * The QR matrix for `text` (UTF-8, byte mode), or null when it does not fit
17877
+ * into a version-5-L symbol (106 bytes).
17878
+ */
17879
+ function qrMatrix(text) {
17880
+ const bytes = new TextEncoder().encode(text);
17881
+ const version = VERSIONS.find((candidate) => candidate.capacity >= bytes.length);
17882
+ if (version === undefined)
17883
+ return null;
17884
+ const grid = blankGrid(version.size);
17885
+ placeFinder(grid, 0, 0);
17886
+ placeFinder(grid, 0, version.size - 7);
17887
+ placeFinder(grid, version.size - 7, 0);
17888
+ if (version.alignment !== null)
17889
+ placeAlignment(grid, version.alignment);
17890
+ // Timing patterns.
17891
+ for (let i = 8; i < version.size - 8; i++) {
17892
+ const timingRow = grid[6];
17893
+ const column = grid[i];
17894
+ if (timingRow !== undefined && timingRow[i] === null)
17895
+ timingRow[i] = i % 2 === 0;
17896
+ if (column !== undefined && column[6] === null)
17897
+ column[6] = i % 2 === 0;
17898
+ }
17899
+ reserveFormatAreas(grid);
17900
+ const codewords = buildCodewords(bytes, version);
17901
+ placeData(grid, codewords);
17902
+ placeFormat(grid, 0);
17903
+ return grid.map((row) => row.map((cell) => cell === true));
17904
+ }
17905
+ };
17906
+ __modules["client-data/wizard.js"] = function (require, module, exports) {
17907
+ "use strict";
17908
+ /**
17909
+ * Wizard UI pure logic (GW-10): route constants, wire types, the step
17910
+ * state machine, form validation and the derived lights — everything the
17911
+ * React components need that a test can drive without a DOM. Mirrors of
17912
+ * the host half's shapes (src/wizard/routes.ts / verify.ts /
17913
+ * proxy-snippets.ts) live HERE because the browser bundle cannot import
17914
+ * those modules (they transitively import node builtins through
17915
+ * config.ts); the check script cross-checks the snippet templates against
17916
+ * the host's so the two cannot drift.
17917
+ *
17918
+ * Validation mirrors src/config.ts's `hostOf` for the same reason — the
17919
+ * row layer applies the same rule when the fields land there, so a client
17920
+ * that accepts something the resolver would silently drop would only set
17921
+ * the user up for a mystery.
17922
+ */
17923
+ Object.defineProperty(exports, "__esModule", { value: true });
17924
+ 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;
17925
+ exports.tunnelFailureCopy = tunnelFailureCopy;
17926
+ exports.tunnelCheckCopy = tunnelCheckCopy;
17927
+ exports.initialStep = initialStep;
17928
+ exports.isWizardConfigured = isWizardConfigured;
17929
+ exports.formSeedOf = formSeedOf;
17930
+ exports.upstreamPortOf = upstreamPortOf;
17931
+ exports.stepAfterPick = stepAfterPick;
17932
+ exports.isValidHostname = isValidHostname;
17933
+ exports.isValidPort = isValidPort;
17934
+ exports.publicUrlFrom = publicUrlFrom;
17935
+ exports.configureReadiness = configureReadiness;
17936
+ exports.deriveWizardLights = deriveWizardLights;
17937
+ exports.deriveClientWizardLights = deriveClientWizardLights;
17938
+ exports.pairingSecondsLeft = pairingSecondsLeft;
17939
+ exports.caddyFailureCopy = caddyFailureCopy;
17940
+ exports.nginxSnippetText = nginxSnippetText;
17941
+ exports.caddySnippetText = caddySnippetText;
17942
+ exports.luckyStepTexts = luckyStepTexts;
17943
+ /** Wizard route constants — mirror of src/wizard/routes.ts. */
17944
+ exports.WIZARD_STATUS_ROUTE = '/_dsh/zen-remote/admin/wizard/status';
17945
+ exports.WIZARD_VERIFY_ROUTE = '/_dsh/zen-remote/admin/wizard/verify';
17946
+ exports.WIZARD_CF_TOKEN_ROUTE = '/_dsh/zen-remote/admin/wizard/cf-token';
17947
+ exports.WIZARD_TUNNEL_TOKEN_ROUTE = '/_dsh/zen-remote/admin/wizard/tunnel-token';
17948
+ exports.WIZARD_CADDY_START_ROUTE = '/_dsh/zen-remote/admin/wizard/caddy/start';
17949
+ exports.WIZARD_CADDY_STOP_ROUTE = '/_dsh/zen-remote/admin/wizard/caddy/stop';
17950
+ exports.WIZARD_CADDY_STATUS_ROUTE = '/_dsh/zen-remote/admin/wizard/caddy/status';
17951
+ exports.WIZARD_TUNNEL_CHECK_ROUTE = '/_dsh/zen-remote/admin/wizard/tunnel/check-domain';
17952
+ exports.WIZARD_TUNNEL_SETUP_ROUTE = '/_dsh/zen-remote/admin/wizard/tunnel/setup';
17953
+ exports.WIZARD_TUNNEL_STOP_ROUTE = '/_dsh/zen-remote/admin/wizard/tunnel/stop';
17954
+ exports.WIZARD_TUNNEL_STATUS_ROUTE = '/_dsh/zen-remote/admin/wizard/tunnel/status';
17955
+ /** The tunnel setup failure's locale key (plus params). Pure. */
17956
+ function tunnelFailureCopy(reason, detail) {
17957
+ switch (reason) {
17958
+ case 'tunnel-conflict':
17959
+ return { key: 'wizard.tunnel.conflict', params: {} };
17960
+ case 'zone-not-found':
17961
+ return { key: 'wizard.tunnel.zoneNotFound', params: {} };
17962
+ case 'dns-conflict':
17963
+ return { key: 'wizard.tunnel.dnsConflict', params: { detail: detail ?? '' } };
17964
+ case 'auth':
17965
+ return { key: 'wizard.tunnel.auth', params: {} };
17966
+ case 'network':
17967
+ return { key: 'wizard.tunnel.network', params: {} };
17968
+ case 'no-token':
17969
+ return { key: 'wizard.tunnel.noToken', params: {} };
17970
+ case 'bad-domain':
17971
+ return { key: 'wizard.tunnel.badDomain', params: {} };
17972
+ case 'spawn-failed':
17973
+ return { key: 'wizard.tunnel.spawnFailed', params: { detail: detail ?? '' } };
17974
+ default:
17975
+ return { key: 'wizard.tunnel.api', params: { detail: detail ?? reason } };
17976
+ }
17977
+ }
17978
+ /** The domain check's copy (the migration card's re-check). Pure. */
17979
+ function tunnelCheckCopy(result) {
17980
+ if (result.ok) {
17981
+ return result.onCloudflare
17982
+ ? { key: 'wizard.check.okOnCf', tone: 'ok' }
17983
+ : { key: 'wizard.check.okNotOnCf', tone: 'error' };
17984
+ }
17985
+ return { key: 'wizard.check.failed', tone: 'error' };
17986
+ }
17987
+ /** Where the wizard opens: a configured ingress jumps straight to verify. */
17988
+ function initialStep(status) {
17989
+ return status !== undefined && status.mode !== 'off' ? 'verify' : 'pick';
17990
+ }
17991
+ /**
17992
+ * Whether the inline overview card should render the configured status
17993
+ * dashboard instead of the first-time setup prompt.
17994
+ */
17995
+ function isWizardConfigured(input) {
17996
+ if (input.effectiveClient) {
17997
+ return input.tokenConfigured || input.clientServerUrl.trim() !== '' || input.clientState === 'connected';
17998
+ }
17999
+ if (input.wizardStatus !== undefined && (input.wizardStatus.mode !== 'off' || input.wizardStatus.publicUrl !== '')) {
18000
+ return true;
18001
+ }
18002
+ return input.pairedDeviceCount > 0 || input.hasSavedHostConfig;
18003
+ }
18004
+ /**
18005
+ * The configure-form seed from one wizard status projection (GW-fix-3/M1):
18006
+ * the projection (wizard/status) is the ONLY surface that carries the
18007
+ * ingress fields — admin/status strips them by design, so the form must
18008
+ * seed from here. Empty values leave the form untouched.
18009
+ */
18010
+ function formSeedOf(status) {
18011
+ if (status === undefined || status.mode === 'off') {
18012
+ return { domain: '', httpsPort: '', serverUrl: '', path: undefined };
18013
+ }
18014
+ return {
18015
+ domain: status.domain,
18016
+ httpsPort: String(status.httpsPort),
18017
+ serverUrl: status.publicUrl,
18018
+ path: status.mode,
18019
+ };
18020
+ }
18021
+ /**
18022
+ * The reverse-proxy snippet's upstream port: the gateway's ACTUAL port as
18023
+ * the wizard runtime reports it, falling back to the configured default
18024
+ * 3088 when no status has landed yet (GW-fix-3/M2).
18025
+ */
18026
+ function upstreamPortOf(status) {
18027
+ return typeof status?.gatewayActualPort === 'number' && status.gatewayActualPort >= 1 && status.gatewayActualPort <= 65535
18028
+ ? status.gatewayActualPort
18029
+ : 3088;
18030
+ }
18031
+ /** The step AFTER pick, given the user's path choice. */
18032
+ function stepAfterPick() {
18033
+ return 'configure';
18034
+ }
18035
+ /** Hostname shape (mirror of src/config.ts hostOf): bare letters/digits/
18036
+ * dots/inner hyphens, labels ≤ 63, total ≤ 253, no scheme/path/port/
18037
+ * whitespace. IP literals pass. */
18038
+ const HOSTNAME_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/;
18039
+ function isValidHostname(value) {
18040
+ return value.length <= 253 && HOSTNAME_PATTERN.test(value);
18041
+ }
18042
+ /** Port strings: [1, 65535]. */
18043
+ function isValidPort(value) {
18044
+ if (!/^\d+$/.test(value))
18045
+ return false;
18046
+ const parsed = Number(value);
18047
+ return Number.isInteger(parsed) && parsed >= 1 && parsed <= 65535;
18048
+ }
18049
+ /** The public origin a verified domain+port answers on (443 hides the port). */
18050
+ function publicUrlFrom(domain, httpsPort) {
18051
+ return httpsPort === 443 ? `https://${domain}` : `https://${domain}:${httpsPort}`;
18052
+ }
18053
+ /** Which configure form is complete enough to advance. */
18054
+ function configureReadiness(path, form) {
18055
+ if (path === 'lan') {
18056
+ const trimmed = form.serverUrl.trim().replace(/\/+$/u, '');
18057
+ if (trimmed === '')
18058
+ return { ok: true, publicUrl: '', httpsPort: 443 };
18059
+ let parsed;
18060
+ try {
18061
+ parsed = new URL(trimmed);
18062
+ }
18063
+ catch {
18064
+ return { ok: false, error: 'bad-server-url' };
18065
+ }
18066
+ if ((parsed.protocol !== 'http:' && parsed.protocol !== 'https:') || (parsed.pathname !== '/' && parsed.pathname !== '') || parsed.search !== '' || parsed.hash !== '') {
18067
+ return { ok: false, error: 'bad-server-url' };
18068
+ }
18069
+ return { ok: true, publicUrl: parsed.origin, httpsPort: 443 };
18070
+ }
18071
+ if (path === 'external') {
18072
+ // The user's own reverse proxy: the public origin is the one thing to
18073
+ // get right (same admission the probe applies — https, root path).
18074
+ const trimmed = form.serverUrl.trim().replace(/\/+$/u, '');
18075
+ let parsed;
18076
+ try {
18077
+ parsed = new URL(trimmed);
18078
+ }
18079
+ catch {
18080
+ return { ok: false, error: 'bad-server-url' };
18081
+ }
18082
+ if (parsed.protocol !== 'https:' || (parsed.pathname !== '/' && parsed.pathname !== '') || parsed.search !== '' || parsed.hash !== '') {
18083
+ return { ok: false, error: 'bad-server-url' };
18084
+ }
18085
+ return { ok: true, publicUrl: parsed.origin, httpsPort: 443 };
18086
+ }
18087
+ if (!isValidHostname(form.domain.trim()))
18088
+ return { ok: false, error: 'bad-domain' };
18089
+ if (!isValidPort(form.httpsPort.trim()))
18090
+ return { ok: false, error: 'bad-port' };
18091
+ const port = Number(form.httpsPort.trim());
18092
+ if ((path === 'caddy' || path === 'cloudflared') && form.cfToken.trim() === '')
18093
+ return { ok: false, error: 'missing-token' };
18094
+ return { ok: true, publicUrl: publicUrlFrom(form.domain.trim(), port), httpsPort: port };
18095
+ }
18096
+ /** Pending-but-started states the lights stay amber on. */
18097
+ const VERIFY_PENDING_RESULTS = new Set(['not-started', 'pending', 'stale', 'code-expired']);
18098
+ /**
18099
+ * The three progress lights, derived from the admin/status reachability
18100
+ * plus the verification poll result. Before a session is started the poll
18101
+ * answers `not-started`, which is a pending state — the public and pairing
18102
+ * lights stay amber (nothing has been tried, so red would just shout);
18103
+ * once started, an amber poll keeps them amber, a wrong-shape answer
18104
+ * (redirect, someone else's server) turns the public light red, and an
18105
+ * insecure cookie turns the pairing light red without ever passing.
18106
+ */
18107
+ function deriveWizardLights(input) {
18108
+ const { poll, startResult } = input;
18109
+ const startProbeFailed = startResult !== undefined && !startResult.ok
18110
+ && (startResult.reason === 'probe-failed' || startResult.reason === 'bad-public-url' || startResult.reason === 'no-public-url');
18111
+ const startProbePassed = startResult?.ok === true && poll?.result !== 'stale' && poll?.result !== 'cookie-insecure';
18112
+ const publicFail = startProbeFailed || (poll !== undefined && poll.result !== 'pass' && !VERIFY_PENDING_RESULTS.has(poll.result));
18113
+ return {
18114
+ gateway: input.gatewayReachable ? 'pass' : 'fail',
18115
+ publicUrl: (poll?.result === 'pass' || startProbePassed) ? 'pass' : publicFail ? 'fail' : 'pending',
18116
+ pairing: poll?.result === 'pass' ? 'pass' : poll?.result === 'cookie-insecure' ? 'fail' : 'pending',
18117
+ };
18118
+ }
18119
+ const CLIENT_FAIL_STATES = new Set([
18120
+ 'unreachable',
18121
+ 'revoked',
18122
+ 'incompatible',
18123
+ 'invalid-url',
18124
+ 'unexpected',
18125
+ ]);
18126
+ /**
18127
+ * Derive the three status lights for the desktop sub-client verification
18128
+ * step (`role === 'client'`).
18129
+ */
18130
+ function deriveClientWizardLights(input) {
18131
+ const connected = input.clientState === 'connected';
18132
+ const failed = input.clientState !== undefined && CLIENT_FAIL_STATES.has(input.clientState);
18133
+ return {
18134
+ credential: (input.tokenConfigured || connected) ? 'pass' : input.clientState === 'revoked' ? 'fail' : 'pending',
18135
+ connection: connected ? 'pass' : failed ? 'fail' : 'pending',
18136
+ sync: connected ? (input.interceptInstalled === false ? 'fail' : 'pass') : 'pending',
18137
+ };
18138
+ }
18139
+ /** Pairing-code countdown, whole seconds, never negative. */
18140
+ function pairingSecondsLeft(expiresAt, now) {
18141
+ return Math.max(0, Math.ceil((expiresAt - now) / 1000));
18142
+ }
18143
+ /**
18144
+ * The managed-Caddy start-failure copy: maps the wire error code to a
18145
+ * locale key (+params / manual-install link). Pure — the check script pins
18146
+ * every mapping.
18147
+ */
18148
+ function caddyFailureCopy(error, detail, installUrl) {
18149
+ switch (error) {
18150
+ case 'missing-token':
18151
+ return { key: 'wizard.caddy.failMissingToken', params: {} };
18152
+ case 'already-running':
18153
+ return { key: 'wizard.caddy.failAlreadyRunning', params: {} };
18154
+ case 'bad-config':
18155
+ return { key: 'wizard.caddy.failBadConfig', params: { detail: detail ?? '' } };
18156
+ case 'download-failed':
18157
+ return { key: 'wizard.caddy.failDownload', params: {}, ...(installUrl !== undefined ? { installUrl } : {}) };
18158
+ case 'validate-failed':
18159
+ return { key: 'wizard.caddy.failValidate', params: {} };
18160
+ default:
18161
+ return { key: 'wizard.caddy.failSpawn', params: {} };
18162
+ }
18163
+ }
18164
+ /** Same grammar the row layer and the host half enforce. */
18165
+ function valid(input) {
18166
+ if (!isValidHostname(input.domain))
18167
+ return null;
18168
+ if (!isValidHostname(input.upstreamHost))
18169
+ return null;
18170
+ if (!Number.isInteger(input.upstreamPort) || input.upstreamPort < 1 || input.upstreamPort > 65535)
18171
+ return null;
18172
+ return { domain: input.domain, upstream: input.upstreamHost, port: input.upstreamPort };
18173
+ }
18174
+ function nginxSnippetText(input) {
18175
+ const parts = valid(input);
18176
+ if (parts === null)
18177
+ return null;
18178
+ return [
18179
+ '# 这段放在 http {} 层(若已有同名 map 则跳过)',
18180
+ 'map $http_upgrade $connection_upgrade {',
18181
+ ' default upgrade;',
18182
+ " '' close;",
18183
+ '}',
18184
+ '',
18185
+ 'server {',
18186
+ ' listen 443 ssl;',
18187
+ ` server_name ${parts.domain};`,
18188
+ '',
18189
+ ' location / {',
18190
+ ` proxy_pass http://${parts.upstream}:${parts.port};`,
18191
+ ' proxy_http_version 1.1;',
18192
+ ' proxy_set_header Upgrade $http_upgrade;',
18193
+ ' proxy_set_header Connection $connection_upgrade;',
18194
+ ' proxy_set_header Host $host;',
18195
+ ' proxy_set_header X-Real-IP $remote_addr;',
18196
+ ' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;',
18197
+ ' proxy_set_header X-Forwarded-Proto $scheme;',
18198
+ ' proxy_buffering off;',
18199
+ ' client_max_body_size 128m;',
18200
+ ' proxy_read_timeout 3600s;',
18201
+ ' proxy_send_timeout 3600s;',
18202
+ ' }',
18203
+ '}',
18204
+ '',
18205
+ ].join('\n');
18206
+ }
18207
+ function caddySnippetText(input) {
18208
+ const parts = valid(input);
18209
+ if (parts === null)
18210
+ return null;
18211
+ return `${parts.domain} {\n reverse_proxy ${parts.upstream}:${parts.port}\n}\n`;
18212
+ }
18213
+ /** The Lucky steps (mirror of proxy-snippets.ts luckySteps). */
18214
+ function luckyStepTexts(input) {
18215
+ const parts = valid(input);
18216
+ if (parts === null)
18217
+ return null;
18218
+ return [
18219
+ '在 Lucky 的「Web 服务」里新建一条规则,监听 443 端口,绑定你的域名。',
18220
+ `后端(反代目标)填 http://${parts.upstream}:${parts.port},开启 WebSocket 支持。`,
18221
+ '打开「万事大吉」开关(放行全部路径),保存即可生效。',
18222
+ ];
18223
+ }
18224
+ /** The snippet kinds the reverse-proxy card tabs over, in tab order. */
18225
+ exports.SNIPPET_KINDS = ['nginx', 'caddy', 'lucky'];
18226
+ };
18227
+ __modules["client/wizard/WizardCard.js"] = function (require, module, exports) {
18228
+ "use strict";
18229
+ Object.defineProperty(exports, "__esModule", { value: true });
18230
+ exports.WizardCard = WizardCard;
18231
+ const jsx_runtime_1 = require("react/jsx-runtime");
18232
+ /**
18233
+ * WizardCard (GW-10): the modal popup for steps the user must perform
18234
+ * ELSEWHERE — creating the Cloudflare token, configuring their own
18235
+ * reverse proxy. Numbered steps in the page locale, copy-to-clipboard for
18236
+ * the value or snippet at hand, Esc / backdrop / button to close. Scoped
18237
+ * under `[data-zen-remote="wizard-card"]`.
18238
+ *
18239
+ * Deliberately NO react-dom portal: `react-dom` is not a seed word of the
18240
+ * host browser module table (GW-fix-4 — `check-client-externals` gates the
18241
+ * allowlist), so the overlay renders IN PLACE as a `position: fixed`
18242
+ * layer. That keeps the plugin version-agnostic; the z-index floor of
18243
+ * 1000 clears the Plugins page chrome, and no ancestor of the detail page
18244
+ * establishes a containing block that could clip it.
18245
+ */
18246
+ const react_1 = require("react");
18247
+ function WizardCard({ title, children, copyText, copyLabel, copiedLabel, closeLabel, primaryAction, onClose }) {
18248
+ const [copied, setCopied] = (0, react_1.useState)(false);
18249
+ const copiedTimer = (0, react_1.useRef)();
18250
+ (0, react_1.useEffect)(() => {
18251
+ const onKey = (event) => {
18252
+ if (event.key === 'Escape')
18253
+ onClose();
18254
+ };
18255
+ document.addEventListener('keydown', onKey);
18256
+ return () => {
18257
+ document.removeEventListener('keydown', onKey);
18258
+ if (copiedTimer.current !== undefined)
18259
+ clearTimeout(copiedTimer.current);
18260
+ };
18261
+ }, [onClose]);
18262
+ const copy = async () => {
18263
+ if (copyText === undefined || copyText === null || copyText === '')
18264
+ return;
18265
+ try {
18266
+ await navigator.clipboard.writeText(copyText);
18267
+ setCopied(true);
18268
+ if (copiedTimer.current !== undefined)
18269
+ clearTimeout(copiedTimer.current);
18270
+ copiedTimer.current = setTimeout(() => setCopied(false), 2000);
18271
+ }
18272
+ catch {
18273
+ // Clipboard denial (permissions, non-secure context): the snippet is
18274
+ // selectable text anyway.
18275
+ }
18276
+ };
18277
+ return ((0, jsx_runtime_1.jsx)("div", { "data-zen-remote": "wizard-card", onClick: onClose, children: (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-cardPanel", onClick: (event) => event.stopPropagation(), children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-cardHeader", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-cardTitle", children: title }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-cardCloseIcon", "aria-label": closeLabel, onClick: onClose, children: "\u00D7" })] }), (0, jsx_runtime_1.jsx)("div", { className: "zr-wizard-cardBody", children: children }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-cardActions", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", onClick: onClose, children: closeLabel }), copyText !== undefined && copyText !== null && copyText !== '' ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", onClick: () => { void copy(); }, children: copied ? copiedLabel : copyLabel })) : null, primaryAction] })] }) }));
18278
+ }
18279
+ };
18280
+ __modules["client/wizard/cards/MigrateCard.js"] = function (require, module, exports) {
18281
+ "use strict";
18282
+ Object.defineProperty(exports, "__esModule", { value: true });
18283
+ exports.MigrateCard = MigrateCard;
18284
+ const jsx_runtime_1 = require("react/jsx-runtime");
18285
+ const WizardCard_tsx_1 = require("./client/wizard/WizardCard.js");
18286
+ const STEPS = ['migrateStep1', 'migrateStep2', 'migrateStep3', 'migrateStep4', 'migrateStep5', 'migrateStep6'];
18287
+ function MigrateCard({ t, onRecheck, onClose }) {
18288
+ return ((0, jsx_runtime_1.jsx)(WizardCard_tsx_1.WizardCard, { title: t('wizard.card.migrateTitle'), copyText: null, copyLabel: t('wizard.card.copy'), copiedLabel: t('wizard.card.copied'), closeLabel: t('wizard.card.close'), primaryAction: ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", onClick: onRecheck, children: t('wizard.card.migrateRecheck') })), onClose: onClose, children: (0, jsx_runtime_1.jsx)("div", { className: "zr-wizard-cardSteps", children: STEPS.map((key, index) => ((0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-cardStep", children: `${index + 1}. ${t(`wizard.card.${key}`)}` }, key))) }) }));
18289
+ }
18290
+ };
18291
+ __modules["client/wizard/cards/PortForwardCard.js"] = function (require, module, exports) {
18292
+ "use strict";
18293
+ Object.defineProperty(exports, "__esModule", { value: true });
18294
+ exports.PortForwardCard = PortForwardCard;
18295
+ const jsx_runtime_1 = require("react/jsx-runtime");
18296
+ const WizardCard_tsx_1 = require("./client/wizard/WizardCard.js");
18297
+ const STEPS = ['portForwardStep1', 'portForwardStep2', 'portForwardStep3', 'portForwardStep4'];
18298
+ function PortForwardCard({ t, port, onClose }) {
18299
+ return ((0, jsx_runtime_1.jsx)(WizardCard_tsx_1.WizardCard, { title: t('wizard.card.portForwardTitle'), copyText: null, copyLabel: t('wizard.card.copy'), copiedLabel: t('wizard.card.copied'), closeLabel: t('wizard.card.close'), onClose: onClose, children: (0, jsx_runtime_1.jsx)("div", { className: "zr-wizard-cardSteps", children: STEPS.map((key, index) => ((0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-cardStep", children: `${index + 1}. ${t(`wizard.card.${key}`, { port: String(port) })}` }, key))) }) }));
18300
+ }
18301
+ };
18302
+ __modules["client/wizard/cards/ProxyCard.js"] = function (require, module, exports) {
18303
+ "use strict";
18304
+ Object.defineProperty(exports, "__esModule", { value: true });
18305
+ exports.ProxyCard = ProxyCard;
18306
+ const jsx_runtime_1 = require("react/jsx-runtime");
18307
+ /**
18308
+ * ProxyCard (GW-11): the reverse-proxy snippets for the user's OWN proxy,
18309
+ * tabbed nginx / Caddy / Lucky, with the snippet copy button and the
18310
+ * 「我配好了,检查」 button that hands control back to the wizard's
18311
+ * verification step.
18312
+ */
18313
+ const react_1 = require("react");
18314
+ const wizard_ts_1 = require("./client-data/wizard.js");
18315
+ const WizardCard_tsx_1 = require("./client/wizard/WizardCard.js");
18316
+ function ProxyCard({ t, domain, upstreamPort, onCheck, onClose }) {
18317
+ const [tab, setTab] = (0, react_1.useState)('nginx');
18318
+ const input = { domain, upstreamHost: '127.0.0.1', upstreamPort };
18319
+ const text = tab === 'nginx'
18320
+ ? (0, wizard_ts_1.nginxSnippetText)(input)
18321
+ : tab === 'caddy' ? (0, wizard_ts_1.caddySnippetText)(input) : ((0, wizard_ts_1.luckyStepTexts)(input) ?? []).join('\n');
18322
+ return ((0, jsx_runtime_1.jsxs)(WizardCard_tsx_1.WizardCard, { title: t('wizard.card.proxyTitle'), copyText: text, copyLabel: t('wizard.card.copy'), copiedLabel: t('wizard.card.copied'), closeLabel: t('wizard.card.close'), primaryAction: ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", onClick: () => { onCheck(); onClose(); }, children: t('wizard.card.proxyCheck') })), onClose: onClose, children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-wizard-cardTabs", children: wizard_ts_1.SNIPPET_KINDS.map((kind) => ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-cardTab", "data-selected": tab === kind, onClick: () => setTab(kind), children: kind }, kind))) }), text === null
18323
+ ? (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-cardStep", children: t('wizard.badDomain') })
18324
+ : (0, jsx_runtime_1.jsx)("pre", { className: "zr-wizard-cardCode", children: text })] }));
18325
+ }
18326
+ };
18327
+ __modules["client/wizard/cards/TokenCard.js"] = function (require, module, exports) {
18328
+ "use strict";
18329
+ Object.defineProperty(exports, "__esModule", { value: true });
18330
+ exports.TokenCard = TokenCard;
18331
+ const jsx_runtime_1 = require("react/jsx-runtime");
18332
+ /**
18333
+ * TokenCard (GW-11): the Cloudflare API-token creation walkthrough — eight
18334
+ * numbered steps, the permission checklist as a small table, and a
18335
+ * paste-in box that hands the token back to the wizard's form (`onUse`).
18336
+ * The token never persists in component state beyond the box and never
18337
+ * leaves this machine except inside the wizard's own POST.
18338
+ */
18339
+ const react_1 = require("react");
18340
+ const WizardCard_tsx_1 = require("./client/wizard/WizardCard.js");
18341
+ const STEPS = ['tokenStep1', 'tokenStep2', 'tokenStep3', 'tokenStep4', 'tokenStep5', 'tokenStep6', 'tokenStep7', 'tokenStep8'];
18342
+ function TokenCard({ t, onUse, onClose }) {
18343
+ const [pasted, setPasted] = (0, react_1.useState)('');
18344
+ return ((0, jsx_runtime_1.jsxs)(WizardCard_tsx_1.WizardCard, { title: t('wizard.card.tokenTitle'), copyText: null, copyLabel: t('wizard.card.copy'), copiedLabel: t('wizard.card.copied'), closeLabel: t('wizard.card.close'), primaryAction: ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", disabled: pasted.trim() === '', onClick: () => onUse(pasted.trim()), children: t('wizard.card.tokenUse') })), onClose: onClose, children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-wizard-cardSteps", children: STEPS.map((key, index) => ((0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-cardStep", children: `${index + 1}. ${t(`wizard.card.${key}`)}` }, key))) }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-cardSubhead", children: t('wizard.card.tokenPerms') }), (0, jsx_runtime_1.jsx)("table", { className: "zr-wizard-cardTable", children: (0, jsx_runtime_1.jsxs)("tbody", { children: [(0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("td", { className: "zr-wizard-cardPerm", children: t('wizard.card.tokenPermDns') }), (0, jsx_runtime_1.jsx)("td", { className: "zr-wizard-cardWhy", children: t('wizard.card.tokenPermDnsWhy') })] }), (0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("td", { className: "zr-wizard-cardPerm", children: t('wizard.card.tokenPermTunnel') }), (0, jsx_runtime_1.jsx)("td", { className: "zr-wizard-cardWhy", children: t('wizard.card.tokenPermTunnelWhy') })] }), (0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("td", { className: "zr-wizard-cardPerm", children: t('wizard.card.tokenPermAccount') }), (0, jsx_runtime_1.jsx)("td", { className: "zr-wizard-cardWhy", children: t('wizard.card.tokenPermAccountWhy') })] }), (0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("td", { className: "zr-wizard-cardPerm", children: t('wizard.card.tokenPermZoneRead') }), (0, jsx_runtime_1.jsx)("td", { className: "zr-wizard-cardWhy", children: t('wizard.card.tokenPermZoneReadWhy') })] })] }) }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-field", children: [(0, jsx_runtime_1.jsx)("label", { className: "zr-wizard-label", htmlFor: "zr-wizard-token-paste", children: t('wizard.card.tokenPaste') }), (0, jsx_runtime_1.jsx)("input", { id: "zr-wizard-token-paste", className: "zr-wizard-input", type: "password", value: pasted, onChange: (event) => setPasted(event.target.value) })] })] }));
18345
+ }
18346
+ };
18347
+ __modules["client/wizard/GatewayWizard.js"] = function (require, module, exports) {
18348
+ "use strict";
18349
+ Object.defineProperty(exports, "__esModule", { value: true });
18350
+ exports.GatewayWizard = GatewayWizard;
18351
+ const jsx_runtime_1 = require("react/jsx-runtime");
18352
+ /**
18353
+ * GatewayWizard (GW-10): the unified three-step remote-access & device-pairing
18354
+ * guide at the top of the plugin detail page (`plugins.bundle.config`,
18355
+ * `key: 'dsh-zen-remote'`).
18356
+ *
18357
+ * Covers both deployment roles in one entry point:
18358
+ * - Server (`host`): choose the ingress method (Caddy / Reverse Proxy /
18359
+ * Cloudflare Tunnel / LAN & Tailscale), configure parameters + server
18360
+ * display name & auto-share, verify connectivity, and pair either a
18361
+ * Phone/Browser (QR code) or another Desktop DSH Client (8-character
18362
+ * desktop pairing code + live paired-client list).
18363
+ * - Desktop Client (`client`): enter the server gateway URL + 8-character
18364
+ * desktop pairing code to claim a device token, and verify live
18365
+ * connection & session-sync status.
18366
+ */
18367
+ const react_1 = require("react");
18368
+ const settings_form_ts_1 = require("./client-data/settings-form.js");
18369
+ const qr_ts_1 = require("./client-data/qr.js");
18370
+ const wizard_ts_1 = require("./client-data/wizard.js");
18371
+ const MigrateCard_tsx_1 = require("./client/wizard/cards/MigrateCard.js");
18372
+ const PortForwardCard_tsx_1 = require("./client/wizard/cards/PortForwardCard.js");
18373
+ const ProxyCard_tsx_1 = require("./client/wizard/cards/ProxyCard.js");
18374
+ const TokenCard_tsx_1 = require("./client/wizard/cards/TokenCard.js");
18375
+ /** The start failure's locale key + params, from the wire reason. */
18376
+ function startFailureCopy(result) {
18377
+ switch (result.reason) {
18378
+ case 'bad-public-url':
18379
+ case 'no-public-url':
18380
+ return { key: 'wizard.startFailedUrl', params: {} };
18381
+ case 'runtime-not-ready':
18382
+ return { key: 'wizard.startFailedRuntime', params: {} };
18383
+ case 'gateway-unreachable':
18384
+ return { key: 'wizard.startFailedGateway', params: {} };
18385
+ case 'pair-refused':
18386
+ return { key: 'wizard.startFailedPair', params: { detail: String(result.detail ?? '') } };
18387
+ case 'probe-failed': {
18388
+ const detail = result.detail;
18389
+ const shape = detail === undefined || detail.kind !== 'reached'
18390
+ ? String(detail?.reason ?? 'network')
18391
+ : `root=${detail.root} admin=${detail.admin}`;
18392
+ return { key: 'wizard.startFailedProbe', params: { detail: shape } };
18393
+ }
18394
+ }
18395
+ }
18396
+ /** The QR code's SVG path: one rect per dark module, pixel-crisp. */
18397
+ function qrSvgPath(matrix, scale) {
18398
+ let path = '';
18399
+ matrix.forEach((cells, row) => {
18400
+ cells.forEach((dark, col) => {
18401
+ if (dark)
18402
+ path += `M${col * scale} ${row * scale}h${scale}v${scale}h-${scale}z`;
18403
+ });
18404
+ });
18405
+ return path;
18406
+ }
18407
+ const LIGHT_ORDER = [
18408
+ { key: 'wizard.verify.gateway', light: 'gateway' },
18409
+ { key: 'wizard.verify.public', light: 'publicUrl' },
18410
+ { key: 'wizard.verify.pairing', light: 'pairing' },
18411
+ ];
18412
+ const CLIENT_LIGHT_ORDER = [
18413
+ { key: 'wizard.client.lightCredential', light: 'credential' },
18414
+ { key: 'wizard.client.lightConnection', light: 'connection' },
18415
+ { key: 'wizard.client.lightSync', light: 'sync' },
18416
+ ];
18417
+ const LIGHT_LABEL = {
18418
+ pass: 'wizard.light.pass',
18419
+ pending: 'wizard.light.pending',
18420
+ fail: 'wizard.light.fail',
18421
+ };
18422
+ const STEP_ORDER = ['pick', 'configure', 'verify'];
18423
+ const ROLE_OPTIONS = [
18424
+ ['host', 'wizard.role.host', 'wizard.role.hostDesc'],
18425
+ ['client', 'wizard.role.client', 'wizard.role.clientDesc'],
18426
+ ];
18427
+ const PATH_OPTIONS = [
18428
+ ['caddy', 'wizard.path.caddy', 'wizard.path.caddyDesc'],
18429
+ ['external', 'wizard.path.external', 'wizard.path.externalDesc'],
18430
+ ['cloudflared', 'wizard.path.cloudflared', 'wizard.path.cloudflaredDesc'],
18431
+ ['lan', 'wizard.path.lan', 'wizard.path.lanDesc'],
18432
+ ];
18433
+ function clientStatusCopy(view, now, t) {
18434
+ const line = (0, settings_form_ts_1.clientStatusLineOf)(view, now);
18435
+ switch (line.kind) {
18436
+ case 'connectedName': return t('settings.client.statusConnectedName', { serverName: line.serverName });
18437
+ case 'connected': return t('settings.client.statusConnected', { serverUrl: line.serverUrl });
18438
+ case 'offlineRetry': return t('settings.client.statusOfflineRetry', { seconds: line.seconds });
18439
+ case 'offlineRetrySoon': return t('settings.client.statusOfflineRetrySoon');
18440
+ case 'unreachable': return t('settings.client.statusUnreachable');
18441
+ case 'revoked': return t('settings.client.statusRevoked');
18442
+ case 'incompatible': return t('settings.client.statusIncompatible');
18443
+ case 'unexpected': return t('settings.client.statusUnexpected');
18444
+ case 'invalidUrl': return t('settings.client.statusInvalidUrl');
18445
+ default: return t('settings.client.statusUnpaired');
18446
+ }
18447
+ }
18448
+ function claimFailCopy(code, retryAfterMs, t) {
18449
+ switch (code) {
18450
+ case 'invalid': return t('settings.client.failInvalid');
18451
+ case 'insecure-http': return t('settings.client.failInsecureHttp');
18452
+ case 'bad-code': return t('settings.client.failBadCode');
18453
+ case 'locked': {
18454
+ const minutes = Math.max(1, Math.ceil((retryAfterMs ?? 0) / 60000));
18455
+ return t('settings.client.failLocked', { minutes });
18456
+ }
18457
+ case 'unreachable': return t('settings.client.failUnreachable');
18458
+ case 'role-mismatch': return t('settings.client.failRoleMismatch');
18459
+ default: return t('settings.client.failUnexpected');
18460
+ }
18461
+ }
18462
+ function GatewayWizard(props) {
18463
+ const { t, config } = props;
18464
+ const [modalOpen, setModalOpen] = (0, react_1.useState)(false);
18465
+ const [devicesModalOpen, setDevicesModalOpen] = (0, react_1.useState)(false);
18466
+ const [step, setStep] = (0, react_1.useState)('pick');
18467
+ const [wizardRole, setWizardRole] = (0, react_1.useState)('host');
18468
+ const [path, setPath] = (0, react_1.useState)('caddy');
18469
+ const [domain, setDomain] = (0, react_1.useState)('');
18470
+ const [httpsPort, setHttpsPort] = (0, react_1.useState)('443');
18471
+ const [cfToken, setCfToken] = (0, react_1.useState)('');
18472
+ const [serverUrl, setServerUrl] = (0, react_1.useState)('');
18473
+ const [hostName, setHostName] = (0, react_1.useState)('');
18474
+ const [autoShare, setAutoShare] = (0, react_1.useState)(true);
18475
+ const [formError, setFormError] = (0, react_1.useState)(null);
18476
+ const [savingConfig, setSavingConfig] = (0, react_1.useState)(false);
18477
+ const [hostSavedInSession, setHostSavedInSession] = (0, react_1.useState)(false);
18478
+ const [wizardStatus, setWizardStatus] = (0, react_1.useState)();
18479
+ const [gatewayReachable, setGatewayReachable] = (0, react_1.useState)(false);
18480
+ const [viaGateway, setViaGateway] = (0, react_1.useState)(false); // default local; server wall is the real gate
18481
+ const [statusLoaded, setStatusLoaded] = (0, react_1.useState)(false);
18482
+ const [allDevices, setAllDevices] = (0, react_1.useState)([]);
18483
+ const [deviceBusy, setDeviceBusy] = (0, react_1.useState)(false);
18484
+ const [deviceError, setDeviceError] = (0, react_1.useState)(false);
18485
+ const [renamingDevice, setRenamingDevice] = (0, react_1.useState)(null);
18486
+ const [pairTarget, setPairTarget] = (0, react_1.useState)('web');
18487
+ const [desktopPairing, setDesktopPairing] = (0, react_1.useState)();
18488
+ const [desktopPairBusy, setDesktopPairBusy] = (0, react_1.useState)(false);
18489
+ const [desktopCopied, setDesktopCopied] = (0, react_1.useState)(false);
18490
+ const [startResult, setStartResult] = (0, react_1.useState)();
18491
+ const [probingUrl, setProbingUrl] = (0, react_1.useState)(false);
18492
+ const [webPairRequested, setWebPairRequested] = (0, react_1.useState)(false);
18493
+ const [poll, setPoll] = (0, react_1.useState)();
18494
+ const [notWired, setNotWired] = (0, react_1.useState)(false);
18495
+ const [now, setNow] = (0, react_1.useState)(() => Date.now());
18496
+ const [card, setCard] = (0, react_1.useState)();
18497
+ const [tokenNotice, setTokenNotice] = (0, react_1.useState)();
18498
+ const [caddyState, setCaddyState] = (0, react_1.useState)();
18499
+ const [caddyBusy, setCaddyBusy] = (0, react_1.useState)(false);
18500
+ const [caddyStartError, setCaddyStartError] = (0, react_1.useState)();
18501
+ const [tunnelState, setTunnelState] = (0, react_1.useState)();
18502
+ const [tunnelBusy, setTunnelBusy] = (0, react_1.useState)(false);
18503
+ const [tunnelResult, setTunnelResult] = (0, react_1.useState)();
18504
+ const [tunnelCheck, setTunnelCheck] = (0, react_1.useState)();
18505
+ const [adoptTunnelId, setAdoptTunnelId] = (0, react_1.useState)();
18506
+ const [probedClient, setProbedClient] = (0, react_1.useState)(null);
18507
+ const selectPath = (0, react_1.useCallback)((nextPath) => {
18508
+ setFormError(null);
18509
+ setPath((prev) => {
18510
+ if (prev !== nextPath) {
18511
+ setCaddyStartError(undefined);
18512
+ setTunnelResult(undefined);
18513
+ setTunnelCheck(undefined);
18514
+ setAdoptTunnelId(undefined);
18515
+ setStartResult(undefined);
18516
+ setPoll(undefined);
18517
+ setWebPairRequested(false);
18518
+ }
18519
+ return nextPath;
18520
+ });
18521
+ }, []);
18522
+ const webDeviceCount = (0, react_1.useMemo)(() => allDevices.filter((d) => d.role === 'web').length, [allDevices]);
18523
+ const desktopDevices = (0, react_1.useMemo)(() => allDevices.filter((d) => d.role === 'desktop-client'), [allDevices]);
18524
+ // Client-mode states
18525
+ const [clientDeviceName, setClientDeviceName] = (0, react_1.useState)('');
18526
+ const [clientServerUrl, setClientServerUrl] = (0, react_1.useState)('');
18527
+ const [clientPairCode, setClientPairCode] = (0, react_1.useState)('');
18528
+ const [clientClaimBusy, setClientClaimBusy] = (0, react_1.useState)(false);
18529
+ const [clientClaimFail, setClientClaimFail] = (0, react_1.useState)(null);
18530
+ const [clientView, setClientView] = (0, react_1.useState)();
18531
+ const [clientLoading, setClientLoading] = (0, react_1.useState)(false);
18532
+ const [clientReconnectBusy, setClientReconnectBusy] = (0, react_1.useState)(false);
18533
+ const [clientUnpairBusy, setClientUnpairBusy] = (0, react_1.useState)(false);
18534
+ // The saved role, live.
18535
+ const roleSnapshot = (0, react_1.useSyncExternalStore)((0, react_1.useCallback)((cb) => config.subscribe(cb), [config]), () => config.getSnapshot());
18536
+ const savedClient = config.savedRoleIsClient();
18537
+ const effectiveClient = savedClient || (probedClient === true && !config.rowRoleKnown());
18538
+ // When the row document carries no role at all, probe the lightweight
18539
+ // client-config route (both roles register it).
18540
+ (0, react_1.useEffect)(() => {
18541
+ if (config.rowRoleKnown())
18542
+ return;
18543
+ let cancelled = false;
18544
+ fetch(settings_form_ts_1.CLIENT_CONFIG_ROUTE)
18545
+ .then((response) => (response.ok ? response.json() : undefined))
18546
+ .then((body) => {
18547
+ if (!cancelled)
18548
+ setProbedClient(body?.role === 'client');
18549
+ })
18550
+ .catch(() => {
18551
+ if (!cancelled)
18552
+ setProbedClient(false);
18553
+ });
18554
+ return () => { cancelled = true; };
18555
+ }, [config, roleSnapshot]);
18556
+ // Seed role & row-stored fields once when the form snapshot settles.
18557
+ const rowSeededRef = (0, react_1.useRef)(false);
18558
+ (0, react_1.useEffect)(() => {
18559
+ if (rowSeededRef.current || roleSnapshot.scopeStatus === 'loading')
18560
+ return;
18561
+ if (!config.rowRoleKnown() && probedClient === null)
18562
+ return;
18563
+ rowSeededRef.current = true;
18564
+ const rowName = typeof config.rowValue('serverName') === 'string' ? config.rowValue('serverName') : '';
18565
+ const rowAutoShare = config.rowValue('autoShareNewSessions');
18566
+ const rowClientUrl = typeof config.rowValue('serverUrl') === 'string' ? config.rowValue('serverUrl') : '';
18567
+ if (rowName !== '') {
18568
+ setHostName(rowName);
18569
+ setClientDeviceName(rowName);
18570
+ }
18571
+ if (typeof rowAutoShare === 'boolean')
18572
+ setAutoShare(rowAutoShare);
18573
+ if (rowClientUrl !== '')
18574
+ setClientServerUrl(rowClientUrl);
18575
+ if (effectiveClient) {
18576
+ setWizardRole('client');
18577
+ setStep(roleSnapshot.deviceToken.configured || rowClientUrl !== '' ? 'verify' : 'configure');
18578
+ }
18579
+ else {
18580
+ setWizardRole('host');
18581
+ }
18582
+ }, [roleSnapshot, probedClient, effectiveClient, config]);
18583
+ const loadClientStatus = (0, react_1.useCallback)(async () => {
18584
+ setClientLoading(true);
18585
+ try {
18586
+ const response = await fetch(settings_form_ts_1.CLIENT_STATUS_ROUTE);
18587
+ if (response.ok) {
18588
+ const body = (await response.json());
18589
+ setClientView((0, settings_form_ts_1.deriveClientStatusView)(body));
18590
+ }
18591
+ }
18592
+ catch {
18593
+ // keep last client view
18594
+ }
18595
+ finally {
18596
+ setClientLoading(false);
18597
+ }
18598
+ }, []);
18599
+ const hostTicketRef = (0, react_1.useRef)(0);
18600
+ const loadHostStatus = (0, react_1.useCallback)(async () => {
18601
+ const mine = ++hostTicketRef.current;
18602
+ try {
18603
+ const [wizardResponse, adminResponse] = await Promise.all([fetch(wizard_ts_1.WIZARD_STATUS_ROUTE), fetch(settings_form_ts_1.ADMIN_STATUS_ROUTE)]);
18604
+ if (wizardResponse.ok && mine === hostTicketRef.current) {
18605
+ const body = (await wizardResponse.json());
18606
+ if (body?.ok === true)
18607
+ setWizardStatus(body);
18608
+ }
18609
+ if (adminResponse.ok && mine === hostTicketRef.current) {
18610
+ const body = (await adminResponse.json());
18611
+ if (body?.ok === true) {
18612
+ setGatewayReachable(body.gatewayReachable === true);
18613
+ setViaGateway(body.viaGateway === true);
18614
+ const derived = (0, settings_form_ts_1.deriveSettingsView)(body, { now: Date.now() });
18615
+ setAllDevices(derived.devices);
18616
+ if (derived.pairing?.role === 'desktop-client' && typeof body.gateway?.pairing?.expiresAt === 'number') {
18617
+ setDesktopPairing({ code: derived.pairing.code, expiresAt: body.gateway.pairing.expiresAt });
18618
+ }
18619
+ }
18620
+ }
18621
+ if (mine === hostTicketRef.current)
18622
+ setStatusLoaded(true);
18623
+ }
18624
+ catch {
18625
+ // keep the last ready data
18626
+ }
18627
+ }, []);
18628
+ // Poll host routes when on host role, or client status when on client role.
18629
+ (0, react_1.useEffect)(() => {
18630
+ if (effectiveClient) {
18631
+ void loadClientStatus();
18632
+ const handle = setInterval(() => { void loadClientStatus(); }, 5000);
18633
+ return () => clearInterval(handle);
18634
+ }
18635
+ void loadHostStatus();
18636
+ const handle = setInterval(() => { void loadHostStatus(); }, 5000);
18637
+ return () => clearInterval(handle);
18638
+ }, [effectiveClient, loadClientStatus, loadHostStatus]);
18639
+ const runDeviceAction = async (payload) => {
18640
+ setDeviceBusy(true);
18641
+ setDeviceError(false);
18642
+ try {
18643
+ const response = await fetch(settings_form_ts_1.ADMIN_ACTION_ROUTE, {
18644
+ method: 'POST',
18645
+ headers: { 'Content-Type': 'application/json' },
18646
+ body: JSON.stringify(payload),
18647
+ });
18648
+ const body = (await response.json().catch(() => ({})));
18649
+ if (response.ok && body.ok === true) {
18650
+ await loadHostStatus();
18651
+ }
18652
+ else {
18653
+ setDeviceError(true);
18654
+ }
18655
+ }
18656
+ catch {
18657
+ setDeviceError(true);
18658
+ }
18659
+ finally {
18660
+ setDeviceBusy(false);
18661
+ }
18662
+ };
18663
+ // Close the wizard modal or devices modal on Escape when no sub-card is open and not mid-save.
18664
+ (0, react_1.useEffect)(() => {
18665
+ if ((!modalOpen && !devicesModalOpen) || card !== undefined)
18666
+ return;
18667
+ const onKey = (event) => {
18668
+ if (event.key === 'Escape' && !savingConfig && !clientClaimBusy && !deviceBusy) {
18669
+ if (renamingDevice !== null) {
18670
+ setRenamingDevice(null);
18671
+ return;
18672
+ }
18673
+ if (devicesModalOpen) {
18674
+ setDevicesModalOpen(false);
18675
+ return;
18676
+ }
18677
+ setModalOpen(false);
18678
+ }
18679
+ };
18680
+ document.addEventListener('keydown', onKey);
18681
+ return () => document.removeEventListener('keydown', onKey);
18682
+ }, [modalOpen, devicesModalOpen, card, savingConfig, clientClaimBusy, deviceBusy, renamingDevice]);
18683
+ // Seed the entry step AND the configure forms from the FIRST ready
18684
+ // wizard status on a host deployment.
18685
+ const seededRef = (0, react_1.useRef)(false);
18686
+ (0, react_1.useEffect)(() => {
18687
+ if (seededRef.current || wizardStatus === undefined || effectiveClient)
18688
+ return;
18689
+ seededRef.current = true;
18690
+ setStep((0, wizard_ts_1.initialStep)(wizardStatus));
18691
+ const seed = (0, wizard_ts_1.formSeedOf)(wizardStatus);
18692
+ if (seed.path !== undefined)
18693
+ setPath(seed.path);
18694
+ else if (wizardStatus.publicUrl !== '')
18695
+ setPath('lan');
18696
+ if (seed.domain !== '')
18697
+ setDomain(seed.domain);
18698
+ if (seed.httpsPort !== '')
18699
+ setHttpsPort(seed.httpsPort);
18700
+ if (seed.serverUrl !== '') {
18701
+ setServerUrl(seed.serverUrl);
18702
+ }
18703
+ else if (wizardStatus.mode === 'off' && wizardStatus.lanAddress !== undefined && wizardStatus.lanAddress !== '') {
18704
+ setServerUrl(`http://${wizardStatus.lanAddress}:${(0, wizard_ts_1.upstreamPortOf)(wizardStatus)}`);
18705
+ }
18706
+ }, [wizardStatus, effectiveClient]);
18707
+ // Verification polling: only while a web pairing session is active and could still move.
18708
+ const pollVerdict = poll?.result;
18709
+ (0, react_1.useEffect)(() => {
18710
+ const active = webPairRequested
18711
+ && startResult?.ok === true
18712
+ && startResult.pairingCode !== ''
18713
+ && (pollVerdict === undefined || pollVerdict === 'not-started' || pollVerdict === 'pending');
18714
+ if (!active)
18715
+ return;
18716
+ let cancelled = false;
18717
+ const beat = async () => {
18718
+ try {
18719
+ const response = await fetch(wizard_ts_1.WIZARD_VERIFY_ROUTE);
18720
+ if (!response.ok || cancelled)
18721
+ return;
18722
+ const body = (await response.json());
18723
+ if (body?.result !== undefined && !cancelled)
18724
+ setPoll(body);
18725
+ }
18726
+ catch {
18727
+ // keep the last verdict
18728
+ }
18729
+ };
18730
+ const handle = setInterval(() => { void beat(); }, 3000);
18731
+ void beat();
18732
+ return () => {
18733
+ cancelled = true;
18734
+ clearInterval(handle);
18735
+ };
18736
+ }, [webPairRequested, startResult, pollVerdict]);
18737
+ // Countdown ticker while a web or desktop pairing code (or client retry) is active.
18738
+ const hasActiveCountdown = (webPairRequested && startResult?.ok === true && startResult.pairingCode !== '')
18739
+ || (desktopPairing !== undefined && desktopPairing.expiresAt > now)
18740
+ || clientView?.nextRetryAt !== undefined;
18741
+ (0, react_1.useEffect)(() => {
18742
+ if (!hasActiveCountdown)
18743
+ return;
18744
+ const handle = setInterval(() => setNow(Date.now()), 1000);
18745
+ return () => clearInterval(handle);
18746
+ }, [hasActiveCountdown]);
18747
+ const disabled = !effectiveClient && viaGateway;
18748
+ const tokenSaved = wizardStatus?.cfTokenConfigured === true || tokenNotice === 'saved';
18749
+ const effectiveCfToken = cfToken.trim() !== '' ? cfToken : (tokenSaved ? '__SAVED__' : '');
18750
+ const readiness = (0, react_1.useMemo)(() => (0, wizard_ts_1.configureReadiness)(path, { domain, httpsPort, cfToken: effectiveCfToken, serverUrl }), [path, domain, httpsPort, effectiveCfToken, serverUrl]);
18751
+ const resolvedVerifyUrl = (0, react_1.useMemo)(() => {
18752
+ if (path === 'external' || path === 'lan') {
18753
+ const trimmed = serverUrl.trim();
18754
+ if (trimmed !== '')
18755
+ return trimmed;
18756
+ }
18757
+ if (readiness.ok && readiness.publicUrl !== '')
18758
+ return readiness.publicUrl;
18759
+ if (wizardStatus?.publicUrl !== undefined && wizardStatus.publicUrl !== '')
18760
+ return wizardStatus.publicUrl;
18761
+ if (path === 'lan') {
18762
+ const lanHost = wizardStatus?.lanAddress !== undefined && wizardStatus.lanAddress !== ''
18763
+ ? wizardStatus.lanAddress
18764
+ : '127.0.0.1';
18765
+ return `http://${lanHost}:${(0, wizard_ts_1.upstreamPortOf)(wizardStatus)}`;
18766
+ }
18767
+ return '';
18768
+ }, [path, serverUrl, readiness, wizardStatus]);
18769
+ const probeAccessUrl = (0, react_1.useCallback)(async (urlToProbe) => {
18770
+ if (urlToProbe === '')
18771
+ return;
18772
+ setNotWired(false);
18773
+ setProbingUrl(true);
18774
+ try {
18775
+ const sendProbe = () => fetch(wizard_ts_1.WIZARD_VERIFY_ROUTE, {
18776
+ method: 'POST',
18777
+ headers: { 'Content-Type': 'application/json' },
18778
+ body: JSON.stringify({ publicUrl: urlToProbe, probeOnly: true }),
18779
+ });
18780
+ let response = await sendProbe();
18781
+ // When switching from client to host role, the plugin row reloads and the
18782
+ // gateway child process takes a brief moment to bind its port. Retry once
18783
+ // or twice before surfacing a transient 404 or gateway-unreachable.
18784
+ for (let attempt = 0; attempt < 2; attempt++) {
18785
+ if (response.status === 404) {
18786
+ await new Promise((resolve) => setTimeout(resolve, 900));
18787
+ response = await sendProbe();
18788
+ continue;
18789
+ }
18790
+ const clone = (await response.clone().json().catch(() => undefined));
18791
+ if (clone !== undefined && !clone.ok && clone.reason === 'gateway-unreachable') {
18792
+ await new Promise((resolve) => setTimeout(resolve, 900));
18793
+ response = await sendProbe();
18794
+ continue;
18795
+ }
18796
+ break;
18797
+ }
18798
+ if (response.status === 501) {
18799
+ setNotWired(true);
18800
+ return;
18801
+ }
18802
+ const body = (await response.json());
18803
+ if (body.ok)
18804
+ setGatewayReachable(true);
18805
+ setStartResult((prev) => {
18806
+ // Keep an active web pairing code on screen if one was already requested and probe passed.
18807
+ if (body.ok && prev?.ok === true && prev.pairingCode !== '' && body.pairingCode === '') {
18808
+ return prev;
18809
+ }
18810
+ return body;
18811
+ });
18812
+ }
18813
+ catch {
18814
+ setStartResult({ ok: false, reason: 'gateway-unreachable' });
18815
+ }
18816
+ finally {
18817
+ setProbingUrl(false);
18818
+ }
18819
+ }, []);
18820
+ // Automatically test access URL connectivity when the verify step opens (or when
18821
+ // managed Caddy / Tunnel transitions to running while on the verify step).
18822
+ (0, react_1.useEffect)(() => {
18823
+ if (!modalOpen || step !== 'verify' || wizardRole !== 'host' || disabled)
18824
+ return;
18825
+ if (resolvedVerifyUrl === '')
18826
+ return;
18827
+ void probeAccessUrl(resolvedVerifyUrl);
18828
+ }, [modalOpen, step, wizardRole, disabled, resolvedVerifyUrl, caddyState, tunnelState, probeAccessUrl]);
18829
+ const saveTokenValue = async (rawToken) => {
18830
+ try {
18831
+ const response = await fetch(wizard_ts_1.WIZARD_CF_TOKEN_ROUTE, {
18832
+ method: 'POST',
18833
+ headers: { 'Content-Type': 'application/json' },
18834
+ body: JSON.stringify({ token: rawToken }),
18835
+ });
18836
+ if (response.status === 404 && effectiveClient) {
18837
+ await props.saveRowFields({
18838
+ role: 'host',
18839
+ autoShareNewSessions: autoShare,
18840
+ ...(hostName.trim() !== '' ? { serverName: hostName.trim() } : {}),
18841
+ });
18842
+ setProbedClient(false);
18843
+ for (let i = 0; i < 6; i++) {
18844
+ await new Promise((resolve) => setTimeout(resolve, 650));
18845
+ const retry = await fetch(wizard_ts_1.WIZARD_CF_TOKEN_ROUTE, {
18846
+ method: 'POST',
18847
+ headers: { 'Content-Type': 'application/json' },
18848
+ body: JSON.stringify({ token: rawToken }),
18849
+ });
18850
+ if (retry.status !== 404) {
18851
+ const ok = retry.ok;
18852
+ setTokenNotice(ok ? 'saved' : 'failed');
18853
+ if (ok)
18854
+ setCfToken('');
18855
+ return ok;
18856
+ }
18857
+ }
18858
+ }
18859
+ const ok = response.ok;
18860
+ setTokenNotice(ok ? 'saved' : 'failed');
18861
+ if (ok)
18862
+ setCfToken('');
18863
+ return ok;
18864
+ }
18865
+ catch {
18866
+ setTokenNotice('failed');
18867
+ return false;
18868
+ }
18869
+ };
18870
+ const advanceFromConfigure = async () => {
18871
+ setFormError(null);
18872
+ if (!readiness.ok) {
18873
+ setFormError(readiness.error);
18874
+ return;
18875
+ }
18876
+ setSavingConfig(true);
18877
+ const wasClient = effectiveClient;
18878
+ const currentHostSetting = typeof config.rowValue('host') === 'string' ? config.rowValue('host').trim() : '';
18879
+ const needsLanHostBind = path === 'lan' && currentHostSetting !== '0.0.0.0' && currentHostSetting !== '::';
18880
+ try {
18881
+ try {
18882
+ const baseFields = {
18883
+ role: 'host',
18884
+ autoShareNewSessions: autoShare,
18885
+ ...(hostName.trim() !== '' ? { serverName: hostName.trim() } : {}),
18886
+ };
18887
+ if (path === 'lan') {
18888
+ // LAN / Tailscale direct connections arrive on the machine's LAN or
18889
+ // Tailscale interface, so the gateway child must bind to `0.0.0.0`
18890
+ // rather than loopback-only `127.0.0.1`.
18891
+ await props.saveRowFields({
18892
+ ...baseFields,
18893
+ host: '0.0.0.0',
18894
+ ingressMode: 'off',
18895
+ ...(readiness.publicUrl !== '' ? { ingressPublicUrl: readiness.publicUrl } : {}),
18896
+ }, readiness.publicUrl === '' ? { unset: ['ingressPublicUrl'] } : undefined);
18897
+ }
18898
+ else if (path === 'external') {
18899
+ await props.saveRowFields({
18900
+ ...baseFields,
18901
+ ingressMode: 'external',
18902
+ ingressPublicUrl: readiness.publicUrl,
18903
+ });
18904
+ }
18905
+ else {
18906
+ await props.saveRowFields({
18907
+ ...baseFields,
18908
+ ingressMode: path,
18909
+ ingressDomain: domain.trim(),
18910
+ ingressHttpsPort: readiness.httpsPort,
18911
+ });
18912
+ }
18913
+ // If this machine was previously in client mode or just rebound
18914
+ // `host` to `0.0.0.0` for LAN access, wait for the restart-watcher to
18915
+ // reload the plugin row and respawn the local gateway child process
18916
+ // before saving the token or probing.
18917
+ if (wasClient || needsLanHostBind) {
18918
+ setProbedClient(false);
18919
+ if (needsLanHostBind && !wasClient) {
18920
+ // Give the restart-watcher time to observe the `host` change and
18921
+ // restart the gateway child on 0.0.0.0.
18922
+ await new Promise((resolve) => setTimeout(resolve, 900));
18923
+ }
18924
+ for (let i = 0; i < 6; i++) {
18925
+ await new Promise((resolve) => setTimeout(resolve, 650));
18926
+ try {
18927
+ const res = await fetch(settings_form_ts_1.ADMIN_STATUS_ROUTE);
18928
+ if (res.ok) {
18929
+ const body = (await res.json());
18930
+ if (body?.ok === true && body.gatewayReachable === true)
18931
+ break;
18932
+ }
18933
+ }
18934
+ catch {
18935
+ // keep waiting for gateway startup
18936
+ }
18937
+ }
18938
+ await loadHostStatus();
18939
+ }
18940
+ }
18941
+ catch {
18942
+ // Even if the row mutation is pending mirror sync, proceed to token save / verify.
18943
+ }
18944
+ if ((path === 'caddy' || path === 'cloudflared') && cfToken.trim() !== '') {
18945
+ const saved = await saveTokenValue(cfToken.trim());
18946
+ if (!saved)
18947
+ return;
18948
+ }
18949
+ // Mode-switch process synchronization: stop any managed ingress process
18950
+ // that does not belong to the newly saved path so an old tunnel or
18951
+ // Caddy child never keeps running across a mode change.
18952
+ if (path !== 'caddy') {
18953
+ fetch(wizard_ts_1.WIZARD_CADDY_STOP_ROUTE, { method: 'POST' }).catch(() => { });
18954
+ setCaddyState('stopped');
18955
+ setCaddyStartError(undefined);
18956
+ }
18957
+ if (path !== 'cloudflared') {
18958
+ fetch(wizard_ts_1.WIZARD_TUNNEL_STOP_ROUTE, { method: 'POST' }).catch(() => { });
18959
+ setTunnelState('stopped');
18960
+ setTunnelResult(undefined);
18961
+ setTunnelCheck(undefined);
18962
+ setAdoptTunnelId(undefined);
18963
+ }
18964
+ await loadHostStatus();
18965
+ setHostSavedInSession(true);
18966
+ setStep('verify');
18967
+ }
18968
+ finally {
18969
+ setSavingConfig(false);
18970
+ }
18971
+ };
18972
+ const startVerification = async () => {
18973
+ const publicUrl = resolvedVerifyUrl;
18974
+ setNotWired(false);
18975
+ setWebPairRequested(true);
18976
+ setProbingUrl(true);
18977
+ try {
18978
+ const response = await fetch(wizard_ts_1.WIZARD_VERIFY_ROUTE, {
18979
+ method: 'POST',
18980
+ headers: { 'Content-Type': 'application/json' },
18981
+ body: JSON.stringify({ publicUrl }),
18982
+ });
18983
+ if (response.status === 501) {
18984
+ setNotWired(true);
18985
+ return;
18986
+ }
18987
+ const body = (await response.json());
18988
+ if (body.ok)
18989
+ setGatewayReachable(true);
18990
+ setStartResult(body);
18991
+ setPoll(body.ok ? { result: 'not-started' } : undefined);
18992
+ }
18993
+ catch {
18994
+ setStartResult({ ok: false, reason: 'gateway-unreachable' });
18995
+ setPoll(undefined);
18996
+ }
18997
+ finally {
18998
+ setProbingUrl(false);
18999
+ }
19000
+ };
19001
+ const runDesktopPair = async () => {
19002
+ setDesktopPairBusy(true);
19003
+ setDesktopCopied(false);
19004
+ try {
19005
+ const response = await fetch(settings_form_ts_1.ADMIN_PAIR_ROUTE, {
19006
+ method: 'POST',
19007
+ headers: { 'Content-Type': 'application/json' },
19008
+ body: JSON.stringify({ role: 'desktop-client' }),
19009
+ });
19010
+ if (response.ok) {
19011
+ const body = (await response.json());
19012
+ if (body?.ok === true && typeof body.code === 'string' && typeof body.expiresAt === 'number') {
19013
+ setDesktopPairing({ code: body.code, expiresAt: body.expiresAt });
19014
+ }
19015
+ }
19016
+ }
19017
+ catch {
19018
+ // ignore network error; user can retry
19019
+ }
19020
+ finally {
19021
+ setDesktopPairBusy(false);
19022
+ }
19023
+ };
19024
+ const resolvedHostEndpoint = (0, react_1.useMemo)(() => {
19025
+ if (path === 'external' || path === 'lan') {
19026
+ const trimmed = serverUrl.trim();
19027
+ if (trimmed !== '')
19028
+ return trimmed;
19029
+ }
19030
+ if (readiness.ok && readiness.publicUrl !== '')
19031
+ return readiness.publicUrl;
19032
+ if (wizardStatus?.publicUrl !== undefined && wizardStatus.publicUrl !== '')
19033
+ return wizardStatus.publicUrl;
19034
+ const port = (0, wizard_ts_1.upstreamPortOf)(wizardStatus);
19035
+ if (wizardStatus?.lanAddress !== undefined && wizardStatus.lanAddress !== '') {
19036
+ return `http://${wizardStatus.lanAddress}:${port}`;
19037
+ }
19038
+ return `http://<服务端局域网IP>:${port}`;
19039
+ }, [path, serverUrl, readiness, wizardStatus]);
19040
+ const copyDesktopInfo = async () => {
19041
+ if (desktopPairing === undefined)
19042
+ return;
19043
+ const text = `服务端地址:${resolvedHostEndpoint}\n桌面客户端配对码:${desktopPairing.code}`;
19044
+ try {
19045
+ await navigator.clipboard?.writeText(text);
19046
+ setDesktopCopied(true);
19047
+ }
19048
+ catch {
19049
+ // ignore clipboard refusal
19050
+ }
19051
+ };
19052
+ const runClientClaim = async () => {
19053
+ setFormError(null);
19054
+ setClientClaimFail(null);
19055
+ const trimmedUrl = clientServerUrl.trim();
19056
+ const normalizedCode = (0, settings_form_ts_1.normalizePairingCode)(clientPairCode);
19057
+ if (trimmedUrl === '' || !/^https?:\/\//i.test(trimmedUrl)) {
19058
+ setFormError('bad-client-url');
19059
+ return;
19060
+ }
19061
+ if (normalizedCode === '') {
19062
+ setFormError('bad-client-code');
19063
+ return;
19064
+ }
19065
+ setClientClaimBusy(true);
19066
+ try {
19067
+ // If the deployment is currently running as 'host', switch role to 'client'
19068
+ // first so the backend restart-watcher reloads the plugin into client mode
19069
+ // and registers CLIENT_CLAIM_ROUTE.
19070
+ if (!effectiveClient) {
19071
+ try {
19072
+ await props.saveRowFields({
19073
+ role: 'client',
19074
+ ...(clientDeviceName.trim() !== '' ? { serverName: clientDeviceName.trim() } : {}),
19075
+ });
19076
+ await new Promise((resolve) => setTimeout(resolve, 1600));
19077
+ }
19078
+ catch {
19079
+ // proceed to claim attempt
19080
+ }
19081
+ }
19082
+ const deviceName = (0, settings_form_ts_1.claimDeviceNameOf)(clientDeviceName, t('settings.client.deviceNameDefault'));
19083
+ const response = await fetch(settings_form_ts_1.CLIENT_CLAIM_ROUTE, {
19084
+ method: 'POST',
19085
+ headers: { 'Content-Type': 'application/json' },
19086
+ body: JSON.stringify({
19087
+ serverUrl: trimmedUrl,
19088
+ code: normalizedCode,
19089
+ deviceName,
19090
+ }),
19091
+ });
19092
+ const body = (await response.json());
19093
+ if (body?.ok === true && typeof body.token === 'string' && body.token !== '') {
19094
+ const urlToWrite = typeof body.serverUrl === 'string' && body.serverUrl !== '' ? body.serverUrl : trimmedUrl;
19095
+ const echoed = typeof body.deviceName === 'string' && body.deviceName.trim() !== '' ? body.deviceName.trim() : deviceName;
19096
+ await config.writeClientPairing(urlToWrite, body.token, echoed);
19097
+ setClientPairCode('');
19098
+ await loadClientStatus();
19099
+ setStep('verify');
19100
+ }
19101
+ else {
19102
+ const failReason = body && typeof body.code === 'string' ? body.code : 'unexpected';
19103
+ const retryAfterMs = body && typeof body.retryAfterMs === 'number' ? body.retryAfterMs : undefined;
19104
+ setClientClaimFail(retryAfterMs === undefined ? { code: failReason } : { code: failReason, retryAfterMs });
19105
+ }
19106
+ }
19107
+ catch {
19108
+ setClientClaimFail({ code: 'unreachable' });
19109
+ }
19110
+ finally {
19111
+ setClientClaimBusy(false);
19112
+ }
19113
+ };
19114
+ const runClientReconnect = async () => {
19115
+ setClientReconnectBusy(true);
19116
+ try {
19117
+ await fetch(settings_form_ts_1.CLIENT_RECONNECT_ROUTE, { method: 'POST' });
19118
+ await loadClientStatus();
19119
+ }
19120
+ catch {
19121
+ // ignore
19122
+ }
19123
+ finally {
19124
+ setClientReconnectBusy(false);
19125
+ }
19126
+ };
19127
+ const runClientUnpair = async () => {
19128
+ setClientUnpairBusy(true);
19129
+ try {
19130
+ await config.clearDeviceToken();
19131
+ await loadClientStatus();
19132
+ setStep('configure');
19133
+ }
19134
+ catch {
19135
+ // ignore
19136
+ }
19137
+ finally {
19138
+ setClientUnpairBusy(false);
19139
+ }
19140
+ };
19141
+ // Managed-Caddy & Tunnel controls (GW-13a / GW-13b): keyed strictly off the
19142
+ // currently selected `path` so switching away from Tunnel or Caddy never
19143
+ // leaves the previous mode's status bar or notices on screen.
19144
+ const caddyPath = wizardRole === 'host' && path === 'caddy';
19145
+ const tunnelPath = wizardRole === 'host' && path === 'cloudflared';
19146
+ const pollIngressState = async (route, apply) => {
19147
+ try {
19148
+ const response = await fetch(route);
19149
+ if (!response.ok)
19150
+ return;
19151
+ const body = (await response.json());
19152
+ if (body?.state === 'running' || body?.state === 'stopped' || body?.state === 'never-started')
19153
+ apply(body.state);
19154
+ }
19155
+ catch {
19156
+ // keep the last state
19157
+ }
19158
+ };
19159
+ (0, react_1.useEffect)(() => {
19160
+ if (step !== 'verify' || !caddyPath)
19161
+ return;
19162
+ const beat = () => pollIngressState(wizard_ts_1.WIZARD_CADDY_STATUS_ROUTE, setCaddyState);
19163
+ void beat();
19164
+ const handle = setInterval(() => { void beat(); }, 3000);
19165
+ return () => clearInterval(handle);
19166
+ }, [step, caddyPath]);
19167
+ (0, react_1.useEffect)(() => {
19168
+ if (step !== 'verify' || !tunnelPath)
19169
+ return;
19170
+ const beat = () => pollIngressState(wizard_ts_1.WIZARD_TUNNEL_STATUS_ROUTE, setTunnelState);
19171
+ void beat();
19172
+ const handle = setInterval(() => { void beat(); }, 3000);
19173
+ return () => clearInterval(handle);
19174
+ }, [step, tunnelPath]);
19175
+ const startCaddy = async () => {
19176
+ setCaddyStartError(undefined);
19177
+ setCaddyBusy(true);
19178
+ try {
19179
+ const parsedPort = Number(httpsPort.trim());
19180
+ const response = await fetch(wizard_ts_1.WIZARD_CADDY_START_ROUTE, {
19181
+ method: 'POST',
19182
+ headers: { 'Content-Type': 'application/json' },
19183
+ body: JSON.stringify({
19184
+ domain: domain.trim(),
19185
+ ...(Number.isSafeInteger(parsedPort) && parsedPort >= 1 && parsedPort <= 65535 ? { httpsPort: parsedPort } : {}),
19186
+ }),
19187
+ });
19188
+ const body = (await response.json());
19189
+ if (body?.ok === true) {
19190
+ setCaddyState('running');
19191
+ setTunnelState('stopped');
19192
+ return;
19193
+ }
19194
+ if (response.status === 501) {
19195
+ setNotWired(true);
19196
+ return;
19197
+ }
19198
+ setCaddyStartError((0, wizard_ts_1.caddyFailureCopy)(String(body?.error ?? ''), typeof body?.detail === 'string' ? body.detail : undefined, typeof body?.installUrl === 'string' ? body.installUrl : undefined));
19199
+ }
19200
+ catch {
19201
+ setCaddyStartError({ key: 'wizard.caddy.failSpawn', params: {} });
19202
+ }
19203
+ finally {
19204
+ setCaddyBusy(false);
19205
+ }
19206
+ };
19207
+ const stopCaddy = async () => {
19208
+ try {
19209
+ await fetch(wizard_ts_1.WIZARD_CADDY_STOP_ROUTE, { method: 'POST' });
19210
+ setCaddyState('stopped');
19211
+ }
19212
+ catch {
19213
+ // the 3 s poll will correct the label
19214
+ }
19215
+ };
19216
+ // Tunnel path (GW-13b): the check-domain probe (the migration card's
19217
+ // re-check) and the full setup. A conflict carries the same-name list —
19218
+ // each entry offers adoption, which re-runs setup with its tunnelId.
19219
+ const checkDomain = async () => {
19220
+ const targetDomain = path === 'external' ? serverUrl.trim() : domain.trim();
19221
+ setTunnelCheck(undefined);
19222
+ try {
19223
+ const response = await fetch(wizard_ts_1.WIZARD_TUNNEL_CHECK_ROUTE, {
19224
+ method: 'POST',
19225
+ headers: { 'Content-Type': 'application/json' },
19226
+ body: JSON.stringify({ domain: targetDomain }),
19227
+ });
19228
+ const body = (await response.json());
19229
+ setTunnelCheck(body);
19230
+ }
19231
+ catch {
19232
+ setTunnelCheck({ ok: false, reason: 'network' });
19233
+ }
19234
+ };
19235
+ const setupTunnelNow = async (tunnelId) => {
19236
+ setTunnelBusy(true);
19237
+ try {
19238
+ const response = await fetch(wizard_ts_1.WIZARD_TUNNEL_SETUP_ROUTE, {
19239
+ method: 'POST',
19240
+ headers: { 'Content-Type': 'application/json' },
19241
+ body: JSON.stringify({ domain: domain.trim(), ...(tunnelId !== undefined ? { tunnelId } : {}) }),
19242
+ });
19243
+ const body = (await response.json());
19244
+ setTunnelResult(body);
19245
+ if (body.ok) {
19246
+ setTunnelState('running');
19247
+ setCaddyState('stopped');
19248
+ }
19249
+ }
19250
+ catch {
19251
+ setTunnelResult({ ok: false, reason: 'network' });
19252
+ }
19253
+ finally {
19254
+ setTunnelBusy(false);
19255
+ }
19256
+ };
19257
+ // Only show on the bundle detail page (never on a component row page).
19258
+ const subject = props.subject;
19259
+ if (subject !== undefined && (subject.kind === 'row' || (subject.pkg?.name !== undefined && subject.pkg.name !== 'dsh-zen-remote'))) {
19260
+ return null;
19261
+ }
19262
+ const baseHostLights = (0, wizard_ts_1.deriveWizardLights)({ gatewayReachable, poll, startResult });
19263
+ const lights = pairTarget === 'desktop-client' && desktopDevices.length > 0
19264
+ ? { ...baseHostLights, pairing: 'pass' }
19265
+ : baseHostLights;
19266
+ const clientLights = (0, wizard_ts_1.deriveClientWizardLights)({
19267
+ tokenConfigured: roleSnapshot.deviceToken.configured || clientView?.state === 'connected',
19268
+ clientState: clientView?.state,
19269
+ interceptInstalled: clientView?.intercept?.installed,
19270
+ });
19271
+ const verified = poll?.result === 'pass';
19272
+ const qr = startResult?.ok === true ? (0, qr_ts_1.qrMatrix)(startResult.publicUrl) : null;
19273
+ const stepLabel = (name) => t(name === 'pick' ? 'wizard.step.pick' : name === 'configure' ? 'wizard.step.configure' : 'wizard.step.verify');
19274
+ const currentStepIndex = STEP_ORDER.indexOf(step);
19275
+ const pathName = (value) => t(value === 'caddy'
19276
+ ? 'wizard.path.caddy'
19277
+ : value === 'external'
19278
+ ? 'wizard.path.external'
19279
+ : value === 'cloudflared'
19280
+ ? 'wizard.path.cloudflared'
19281
+ : 'wizard.path.lan');
19282
+ const hasSavedHostConfig = hostSavedInSession
19283
+ || config.rowValue('role') === 'host'
19284
+ || (typeof config.rowValue('ingressPublicUrl') === 'string' && config.rowValue('ingressPublicUrl').trim() !== '');
19285
+ const configured = (0, wizard_ts_1.isWizardConfigured)({
19286
+ effectiveClient,
19287
+ wizardStatus,
19288
+ pairedDeviceCount: webDeviceCount + desktopDevices.length,
19289
+ hasSavedHostConfig,
19290
+ tokenConfigured: roleSnapshot.deviceToken.configured,
19291
+ clientServerUrl,
19292
+ clientState: clientView?.state,
19293
+ });
19294
+ const statusSummary = (() => {
19295
+ if (effectiveClient) {
19296
+ if (clientView?.state === 'connected') {
19297
+ const label = clientView.serverName !== '' ? clientView.serverName : (clientView.serverUrl || t('wizard.role.client'));
19298
+ return { state: 'pass', text: `${t('wizard.role.client')} · ${label}` };
19299
+ }
19300
+ if (roleSnapshot.deviceToken.configured) {
19301
+ return { state: 'pending', text: t('wizard.role.client') };
19302
+ }
19303
+ return { state: 'idle', text: t('settings.client.statusUnpaired') };
19304
+ }
19305
+ if (wizardStatus === undefined || wizardStatus.mode === 'off') {
19306
+ if (configured && gatewayReachable) {
19307
+ return { state: 'pass', text: t('wizard.path.lan') };
19308
+ }
19309
+ return { state: 'idle', text: t('wizard.off') };
19310
+ }
19311
+ if (wizardStatus.mode === 'caddy') {
19312
+ const running = caddyState === 'running' || (caddyState === undefined && wizardStatus.caddyRunning);
19313
+ return { state: running ? 'pass' : 'pending', text: t(running ? 'wizard.caddy.running' : 'wizard.caddy.stopped') };
19314
+ }
19315
+ if (wizardStatus.mode === 'cloudflared') {
19316
+ const running = tunnelState === 'running' || (tunnelState === undefined && wizardStatus.cloudflaredRunning);
19317
+ return { state: running ? 'pass' : 'pending', text: t(running ? 'wizard.tunnel.running' : 'wizard.tunnel.stopped') };
19318
+ }
19319
+ return { state: 'pass', text: wizardStatus.publicUrl || t('wizard.path.external') };
19320
+ })();
19321
+ const openWizardAt = (targetStep, options) => {
19322
+ setFormError(null);
19323
+ if (options?.role !== undefined)
19324
+ setWizardRole(options.role);
19325
+ if (options?.target !== undefined)
19326
+ setPairTarget(options.target);
19327
+ setStep(targetStep);
19328
+ setModalOpen(true);
19329
+ };
19330
+ 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
19331
+ ? (clientLoading ? t('settings.client.statusRefreshing') : t('settings.client.statusUnpaired'))
19332
+ : 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
19333
+ ? t('wizard.overview.gatewayReady', { port: String((0, wizard_ts_1.upstreamPortOf)(wizardStatus)) })
19334
+ : 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: () => {
19335
+ setDeviceError(false);
19336
+ setRenamingDevice(null);
19337
+ setDevicesModalOpen(true);
19338
+ }, children: t('wizard.overview.manageDevices') })] }), (0, jsx_runtime_1.jsxs)("p", { className: "zr-wizard-overviewValue", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-dot", "data-state": allDevices.length > 0 ? 'pass' : 'pending' }), (0, jsx_runtime_1.jsx)("span", { children: allDevices.length > 0
19339
+ ? t('wizard.overview.devicesSummary', { web: String(webDeviceCount), desktop: String(desktopDevices.length) })
19340
+ : t('wizard.overview.devicesNone') })] }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-overviewSub", children: statusSummary.text })] })] }), (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: disabled, onClick: () => openWizardAt('pick', { role: 'host' }), children: t('wizard.overview.reconfigure') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: disabled, onClick: () => {
19341
+ setDeviceError(false);
19342
+ setRenamingDevice(null);
19343
+ setDevicesModalOpen(true);
19344
+ }, children: t('wizard.overview.manageDevices') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-row", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: disabled, onClick: () => openWizardAt('verify', { role: 'host', target: 'desktop-client' }), children: t('wizard.overview.pairDesktop') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", disabled: disabled, onClick: () => openWizardAt('verify', { role: 'host', target: 'web' }), children: t('wizard.overview.pairPhone') })] })] })] }))) : ((0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-emptyBanner", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-emptyText", children: t('wizard.overview.emptyIntro') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", disabled: disabled, onClick: () => openWizardAt('pick'), children: t('wizard.overview.startSetup') })] })), modalOpen ? ((0, jsx_runtime_1.jsx)("div", { "data-zen-remote": "wizard-modal", onClick: () => {
19345
+ if (!savingConfig && !clientClaimBusy)
19346
+ setModalOpen(false);
19347
+ }, children: (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-modalDialog", role: "dialog", "aria-modal": "true", "aria-label": t('wizard.modal.title'), onClick: (event) => event.stopPropagation(), children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-modalHeader", children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-modalTitle", children: t('wizard.modal.title') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-cardCloseIcon", "aria-label": t('wizard.card.close'), disabled: savingConfig || clientClaimBusy, onClick: () => setModalOpen(false), children: "\u00D7" })] }), (0, jsx_runtime_1.jsx)("ol", { className: "zr-wizard-stepper", "aria-label": t('wizard.modal.title'), children: STEP_ORDER.map((name, index) => {
19348
+ const state = currentStepIndex > index ? 'done' : currentStepIndex === index ? 'current' : 'upcoming';
19349
+ return ((0, jsx_runtime_1.jsxs)("li", { style: { display: 'contents' }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-stepItem", "data-state": state, "aria-current": state === 'current' ? 'step' : undefined, children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-stepCircle", "aria-hidden": "true", children: state === 'done' ? ((0, jsx_runtime_1.jsx)("svg", { width: "13", height: "13", viewBox: "0 0 16 16", fill: "none", children: (0, jsx_runtime_1.jsx)("path", { d: "M3.5 8.5L6.5 11.5L12.5 4.5", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) })) : (index + 1) }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-stepText", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-stepTag", children: t('wizard.modal.stepTag', { num: String(index + 1) }) }), (0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-stepName", children: stepLabel(name) })] })] }), index < STEP_ORDER.length - 1 ? ((0, jsx_runtime_1.jsx)("div", { className: "zr-wizard-stepConnector", "data-done": currentStepIndex > index, "aria-hidden": "true" })) : null] }, name));
19350
+ }) }), step === 'pick' ? ((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.step1Desc') }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-sectionTitle", children: t('wizard.role.sectionTitle') }), (0, jsx_runtime_1.jsx)("div", { className: "zr-wizard-roleGrid", role: "radiogroup", "aria-label": t('wizard.role.sectionTitle'), children: ROLE_OPTIONS.map(([roleValue, nameKey, descKey]) => {
19351
+ const selected = wizardRole === roleValue;
19352
+ return ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "radio", "aria-checked": selected, className: "zr-wizard-path", "data-selected": selected, disabled: disabled, onClick: () => {
19353
+ setFormError(null);
19354
+ setWizardRole(roleValue);
19355
+ }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-pathHead", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-pathRadio", "data-selected": selected, "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-pathName", children: t(nameKey) })] }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-pathDesc", children: t(descKey) })] }, roleValue));
19356
+ }) }), wizardRole === 'host' ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-sectionTitle", children: t('wizard.ingressSectionTitle') }), (0, jsx_runtime_1.jsx)("div", { className: "zr-wizard-paths", role: "radiogroup", "aria-label": t('wizard.ingressSectionTitle'), children: PATH_OPTIONS.map(([value, nameKey, descKey]) => {
19357
+ const selected = path === value;
19358
+ return ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "radio", "aria-checked": selected, className: "zr-wizard-path", "data-selected": selected, disabled: disabled, onClick: () => {
19359
+ selectPath(value);
19360
+ }, onDoubleClick: () => {
19361
+ if (disabled)
19362
+ return;
19363
+ selectPath(value);
19364
+ setStep((0, wizard_ts_1.stepAfterPick)());
19365
+ }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-pathHead", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-pathRadio", "data-selected": selected, "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-pathName", children: t(nameKey) })] }), (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-pathDesc", children: t(descKey) })] }, value));
19366
+ }) })] })) : ((0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", children: t('wizard.role.clientPickNote') }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-modalFooter", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", onClick: () => setModalOpen(false), children: t('wizard.modal.cancel') }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-row", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-footerHint", children: wizardRole === 'host'
19367
+ ? `${t('wizard.role.host')} · ${pathName(path)}`
19368
+ : t('wizard.role.client') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", disabled: disabled, onClick: () => {
19369
+ setFormError(null);
19370
+ setStep((0, wizard_ts_1.stepAfterPick)());
19371
+ }, children: t('wizard.next') })] })] })] })) : null, step === 'configure' ? ((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.step2Desc') }), (0, jsx_runtime_1.jsx)("div", { className: "zr-wizard-selectedBar", children: (0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-selectedLabel", children: t('wizard.modal.selectedPlan', {
19372
+ plan: wizardRole === 'host'
19373
+ ? `${t('wizard.role.host')} · ${pathName(path)}`
19374
+ : t('wizard.role.client'),
19375
+ }) }) }), 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: () => {
19376
+ setFormError(null);
19377
+ setStep('pick');
19378
+ }, 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
19379
+ ? t('wizard.caddy.starting')
19380
+ : 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: () => {
19381
+ void (async () => {
19382
+ try {
19383
+ await fetch(wizard_ts_1.WIZARD_TUNNEL_STOP_ROUTE, { method: 'POST' });
19384
+ setTunnelState('stopped');
19385
+ }
19386
+ catch { /* the poll corrects */ }
19387
+ })();
19388
+ }, children: t('wizard.tunnel.stop') })] })] })) : null, tunnelPath && tunnelCheck !== undefined ? (() => {
19389
+ const copy = (0, wizard_ts_1.tunnelCheckCopy)(tunnelCheck);
19390
+ return (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": copy.tone === 'error' ? 'error' : undefined, children: copy.key === 'wizard.check.failed'
19391
+ ? t('wizard.check.failed', { reason: tunnelCheck.ok ? '' : String(tunnelCheck.reason) })
19392
+ : t(copy.key) });
19393
+ })() : 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' ? (() => {
19394
+ const copy = (0, wizard_ts_1.tunnelFailureCopy)(tunnelResult.reason, tunnelResult.detail);
19395
+ return (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t(copy.key, copy.params) });
19396
+ })() : 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 }) => {
19397
+ const state = lights[light];
19398
+ const labelText = light === 'publicUrl' && probingUrl
19399
+ ? t('wizard.light.probing')
19400
+ : t(LIGHT_LABEL[state]);
19401
+ return ((0, jsx_runtime_1.jsxs)("li", { className: "zr-wizard-light", "data-state": state, children: [(0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-lightMain", children: [(0, jsx_runtime_1.jsx)("span", { className: "zr-wizard-dot", "data-state": state }), (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": state, children: labelText })] }, key));
19402
+ }) }), startResult !== undefined && !startResult.ok ? (() => {
19403
+ const copy = startFailureCopy(startResult);
19404
+ return (0, jsx_runtime_1.jsx)("p", { className: "zr-wizard-notice", "data-tone": "error", children: t('wizard.startFailed', { reason: t(copy.key, copy.params) }) });
19405
+ })() : 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
19406
+ ? (clientLoading ? t('settings.client.statusRefreshing') : t('settings.client.statusUnpaired'))
19407
+ : 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: () => {
19408
+ if (!deviceBusy)
19409
+ setDevicesModalOpen(false);
19410
+ }, 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) => {
19411
+ void runDeviceAction({ action: 'set-kind', id: device.id, kind: event.currentTarget.value });
19412
+ }, children: [(0, jsx_runtime_1.jsx)("option", { value: "auto", children: t('settings.kindAuto') }), (0, jsx_runtime_1.jsx)("option", { value: "phone", children: t('settings.kindPhone') }), (0, jsx_runtime_1.jsx)("option", { value: "desktop", children: t('settings.kindDesktop') })] })) : null, (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-linkBtn", disabled: disabled || deviceBusy, onClick: () => setRenamingDevice({ id: device.id, draft: device.name }), children: t('settings.deviceRename') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-linkBtn", disabled: disabled || deviceBusy, onClick: () => {
19413
+ if (window.confirm(t('settings.revokeConfirm'))) {
19414
+ void runDeviceAction({ action: 'revoke', id: device.id });
19415
+ }
19416
+ }, children: t('settings.deviceRevoke') })] }), renamingDevice?.id === device.id ? ((0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-row", children: [(0, jsx_runtime_1.jsx)("input", { className: "zr-wizard-input", style: { flex: 1, minWidth: 160 }, "aria-label": t('settings.renamePrompt'), maxLength: 40, value: renamingDevice.draft, autoFocus: true, onChange: (event) => setRenamingDevice({ id: device.id, draft: event.currentTarget.value }), onKeyDown: (event) => {
19417
+ if (event.key === 'Enter') {
19418
+ const trimmed = renamingDevice.draft.trim();
19419
+ setRenamingDevice(null);
19420
+ if (trimmed !== '' && trimmed !== device.name) {
19421
+ void runDeviceAction({ action: 'rename', id: device.id, name: trimmed });
19422
+ }
19423
+ }
19424
+ if (event.key === 'Escape')
19425
+ setRenamingDevice(null);
19426
+ } }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", disabled: deviceBusy, onClick: () => {
19427
+ const trimmed = renamingDevice.draft.trim();
19428
+ setRenamingDevice(null);
19429
+ if (trimmed !== '' && trimmed !== device.name) {
19430
+ void runDeviceAction({ action: 'rename', id: device.id, name: trimmed });
19431
+ }
19432
+ }, children: t('settings.renameSave') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: deviceBusy, onClick: () => setRenamingDevice(null), children: t('settings.renameCancel') })] })) : null] }, device.id))) }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-modalFooter", children: [(0, jsx_runtime_1.jsx)("div", { className: "zr-wizard-row", children: allDevices.length > 0 ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: disabled || deviceBusy, onClick: () => {
19433
+ if (window.confirm(t('settings.revokeAllConfirm'))) {
19434
+ void runDeviceAction({ action: 'revoke-all' });
19435
+ }
19436
+ }, children: t('settings.revokeAll') })) : null }), (0, jsx_runtime_1.jsxs)("div", { className: "zr-wizard-row", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn", disabled: disabled || deviceBusy, onClick: () => {
19437
+ setDevicesModalOpen(false);
19438
+ openWizardAt('verify', { role: 'host', target: 'desktop-client' });
19439
+ }, children: t('wizard.overview.pairDesktop') }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "zr-wizard-btn zr-wizard-btnPrimary", disabled: disabled || deviceBusy, onClick: () => {
19440
+ setDevicesModalOpen(false);
19441
+ openWizardAt('verify', { role: 'host', target: 'web' });
19442
+ }, 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: (() => {
19443
+ if (domain.trim() !== '')
19444
+ return domain.trim();
19445
+ try {
19446
+ const parsed = new URL(serverUrl.trim());
19447
+ if (parsed.hostname !== '')
19448
+ return parsed.hostname;
19449
+ }
19450
+ catch { /* fall back to placeholder */ }
19451
+ return 'dsh.example.com';
19452
+ })(), upstreamPort: (0, wizard_ts_1.upstreamPortOf)(wizardStatus), onCheck: () => {
19453
+ setStep('verify');
19454
+ void startVerification();
19455
+ }, onClose: () => setCard(undefined) })) : null, card === 'token' ? ((0, jsx_runtime_1.jsx)(TokenCard_tsx_1.TokenCard, { t: t, onUse: (token) => {
19456
+ void (async () => {
19457
+ await saveTokenValue(token);
19458
+ setCard(undefined);
19459
+ })();
19460
+ }, onClose: () => setCard(undefined) })) : null, card === 'migrate' ? ((0, jsx_runtime_1.jsx)(MigrateCard_tsx_1.MigrateCard, { t: t, onRecheck: () => {
19461
+ setCard(undefined);
19462
+ void checkDomain();
19463
+ }, onClose: () => setCard(undefined) })) : null, card === 'portforward' ? ((0, jsx_runtime_1.jsx)(PortForwardCard_tsx_1.PortForwardCard, { t: t, port: Number(httpsPort.trim()) || 8443, onClose: () => setCard(undefined) })) : null] }));
19464
+ }
19465
+ };
19466
+ __modules["client/wizard/wizard-css.js"] = function (require, module, exports) {
19467
+ "use strict";
19468
+ /**
19469
+ * Wizard-page CSS (GW-10), scoped end to end under the block's own
19470
+ * `data-zen-remote` attributes — the desktop no-op rule in its CSS form:
19471
+ * nothing here may reach a selector the wizard does not own, so the
19472
+ * Plugins detail page at ≥1024px renders byte-identically whether or not
19473
+ * the component mounts. Same transcription discipline as settings-css.ts
19474
+ * (host design tokens only, no literal colors).
19475
+ */
19476
+ Object.defineProperty(exports, "__esModule", { value: true });
19477
+ exports.WIZARD_CSS = void 0;
19478
+ exports.WIZARD_CSS = `
19479
+ [data-plugin-detail="dsh-zen-remote"] section[data-plugin-config]:empty {
19480
+ display: none;
19481
+ }
19482
+
19483
+ [data-zen-remote="wizard"] {
19484
+ display: flex;
19485
+ flex-direction: column;
19486
+ gap: 16px;
19487
+ width: 100%;
19488
+ box-sizing: border-box;
19489
+ margin-inline: auto;
19490
+ border: 1px solid var(--dsw-alias-border-l2);
19491
+ border-radius: 12px;
19492
+ background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-module-platform));
19493
+ padding: 18px 20px;
19494
+ }
19495
+
19496
+ [data-zen-remote="wizard"] .zr-wizard-header {
19497
+ display: flex;
19498
+ align-items: flex-start;
19499
+ justify-content: space-between;
19500
+ gap: 12px;
19501
+ flex-wrap: wrap;
19502
+ }
19503
+ [data-zen-remote="wizard"] .zr-wizard-headerText {
19504
+ display: flex;
19505
+ flex-direction: column;
19506
+ gap: 4px;
19507
+ min-width: 0;
19508
+ }
19509
+ [data-zen-remote="wizard"] .zr-wizard-title {
19510
+ font-size: 15px;
19511
+ font-weight: 600;
19512
+ color: var(--dsw-alias-label-primary);
19513
+ margin: 0;
19514
+ }
19515
+ [data-zen-remote="wizard"] .zr-wizard-subtitle {
19516
+ font-size: 12.5px;
19517
+ color: var(--dsw-alias-label-tertiary);
19518
+ margin: 0;
19519
+ line-height: 1.45;
19520
+ }
19521
+ [data-zen-remote="wizard"] .zr-wizard-statusBadge {
19522
+ display: inline-flex;
19523
+ align-items: center;
19524
+ gap: 6px;
19525
+ font-size: 12px;
19526
+ font-weight: 500;
19527
+ padding: 4px 10px;
19528
+ border-radius: 999px;
19529
+ border: 1px solid var(--dsw-alias-border-l2);
19530
+ background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-module-platform));
19531
+ color: var(--dsw-alias-label-secondary);
19532
+ white-space: nowrap;
19533
+ }
19534
+ [data-zen-remote="wizard"] .zr-wizard-statusBadge[data-state="pass"] {
19535
+ color: var(--dsw-alias-label-primary);
19536
+ }
19537
+
19538
+ [data-zen-remote="wizard"] .zr-wizard-notice {
19539
+ font-size: 12.5px;
19540
+ border: 1px solid var(--dsw-alias-border-l2);
19541
+ border-radius: 8px;
19542
+ padding: 9px 12px;
19543
+ background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-module-platform));
19544
+ color: var(--dsw-alias-label-secondary);
19545
+ margin: 0;
19546
+ line-height: 1.45;
19547
+ }
19548
+ [data-zen-remote="wizard"] .zr-wizard-notice[data-tone="error"] {
19549
+ border-color: var(--dsw-alias-state-error-primary, #d5304f);
19550
+ color: var(--dsw-alias-state-error-primary, #d5304f);
19551
+ }
19552
+ [data-zen-remote="wizard"] .zr-wizard-notice[data-tone="ok"] {
19553
+ border-color: var(--dsw-alias-state-success-primary, #16a34a);
19554
+ color: var(--dsw-alias-state-success-primary, #16a34a);
19555
+ }
19556
+
19557
+ /* Inline Status Overview Card (2x2 spacious layout, full text visibility) */
19558
+ [data-zen-remote="wizard"] .zr-wizard-overviewGrid {
19559
+ display: grid;
19560
+ grid-template-columns: repeat(2, minmax(0, 1fr));
19561
+ gap: 12px;
19562
+ }
19563
+ @media (max-width: 560px) {
19564
+ [data-zen-remote="wizard"] .zr-wizard-overviewGrid {
19565
+ grid-template-columns: 1fr;
19566
+ }
19567
+ }
19568
+ [data-zen-remote="wizard"] .zr-wizard-overviewItem {
19569
+ display: flex;
19570
+ flex-direction: column;
19571
+ gap: 6px;
19572
+ padding: 13px 15px;
19573
+ border-radius: 10px;
19574
+ border: 1px solid var(--dsw-alias-border-l2);
19575
+ background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-module-platform));
19576
+ min-width: 0;
19577
+ }
19578
+ [data-zen-remote="wizard"] .zr-wizard-overviewHead {
19579
+ display: flex;
19580
+ align-items: center;
19581
+ justify-content: space-between;
19582
+ gap: 8px;
19583
+ }
19584
+ [data-zen-remote="wizard"] .zr-wizard-overviewLabel {
19585
+ font-size: 12px;
19586
+ color: var(--dsw-alias-label-tertiary);
19587
+ margin: 0;
19588
+ }
19589
+ [data-zen-remote="wizard"] .zr-wizard-overviewValue {
19590
+ display: flex;
19591
+ align-items: center;
19592
+ gap: 7px;
19593
+ font-size: 13.5px;
19594
+ font-weight: 600;
19595
+ color: var(--dsw-alias-label-primary);
19596
+ margin: 0;
19597
+ line-height: 1.45;
19598
+ overflow-wrap: anywhere;
19599
+ word-break: break-word;
19600
+ }
19601
+ [data-zen-remote="wizard"] .zr-wizard-overviewSub {
19602
+ font-size: 12px;
19603
+ color: var(--dsw-alias-label-secondary);
19604
+ margin: 0;
19605
+ line-height: 1.45;
19606
+ overflow-wrap: anywhere;
19607
+ word-break: break-word;
19608
+ }
19609
+ [data-zen-remote="wizard"] .zr-wizard-emptyBanner {
19610
+ display: flex;
19611
+ align-items: center;
19612
+ justify-content: space-between;
19613
+ gap: 16px;
19614
+ flex-wrap: wrap;
19615
+ padding: 14px 16px;
19616
+ border-radius: 10px;
19617
+ border: 1px dashed var(--dsw-alias-border-l2);
19618
+ background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-module-platform));
19619
+ }
19620
+ [data-zen-remote="wizard"] .zr-wizard-emptyText {
19621
+ font-size: 12.5px;
19622
+ color: var(--dsw-alias-label-secondary);
19623
+ line-height: 1.5;
19624
+ margin: 0;
19625
+ flex: 1;
19626
+ min-width: 220px;
19627
+ }
19628
+
19629
+ /* Wizard Modal Dialog (above app chrome, below helper sub-cards) */
19630
+ [data-zen-remote="wizard-modal"] {
19631
+ position: fixed;
19632
+ inset: 0;
19633
+ z-index: 950;
19634
+ display: flex;
19635
+ align-items: center;
19636
+ justify-content: center;
19637
+ padding: 16px;
19638
+ background: rgba(0, 0, 0, 0.5);
19639
+ backdrop-filter: blur(2px);
19640
+ }
19641
+ [data-zen-remote="wizard-modal"] .zr-wizard-modalDialog {
19642
+ width: min(680px, calc(100vw - 32px));
19643
+ max-height: min(88vh, 760px);
19644
+ display: flex;
19645
+ flex-direction: column;
19646
+ border-radius: 14px;
19647
+ border: 1px solid var(--dsw-alias-border-l2);
19648
+ background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-module-platform));
19649
+ color: var(--dsw-alias-label-primary);
19650
+ box-shadow: 0 20px 48px rgba(0, 0, 0, 0.28);
19651
+ overflow: hidden;
19652
+ }
19653
+ [data-zen-remote="wizard-modal"] .zr-wizard-modalHeader {
19654
+ display: flex;
19655
+ align-items: center;
19656
+ justify-content: space-between;
19657
+ gap: 12px;
19658
+ padding: 16px 22px 14px;
19659
+ }
19660
+ [data-zen-remote="wizard-modal"] .zr-wizard-modalTitle {
19661
+ font-size: 15.5px;
19662
+ font-weight: 600;
19663
+ color: var(--dsw-alias-label-primary);
19664
+ margin: 0;
19665
+ }
19666
+ [data-zen-remote="wizard-modal"] .zr-wizard-modalBody {
19667
+ padding: 18px 22px;
19668
+ overflow-y: auto;
19669
+ display: flex;
19670
+ flex-direction: column;
19671
+ gap: 16px;
19672
+ flex: 1;
19673
+ }
19674
+ [data-zen-remote="wizard-modal"] .zr-wizard-modalFooter {
19675
+ padding: 14px 22px;
19676
+ border-top: 1px solid var(--dsw-alias-border-l2);
19677
+ background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-module-platform));
19678
+ display: flex;
19679
+ align-items: center;
19680
+ justify-content: space-between;
19681
+ gap: 12px;
19682
+ flex-wrap: wrap;
19683
+ }
19684
+
19685
+ /* Non-clickable 3-step visual progress indicator */
19686
+ [data-zen-remote="wizard"] .zr-wizard-stepper {
19687
+ display: flex;
19688
+ align-items: center;
19689
+ justify-content: space-between;
19690
+ gap: 10px;
19691
+ padding: 13px 22px;
19692
+ margin: 0;
19693
+ list-style: none;
19694
+ background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-module-platform));
19695
+ border-top: 1px solid var(--dsw-alias-border-l2);
19696
+ border-bottom: 1px solid var(--dsw-alias-border-l2);
19697
+ user-select: none;
19698
+ pointer-events: none;
19699
+ }
19700
+ [data-zen-remote="wizard"] .zr-wizard-stepItem {
19701
+ display: flex;
19702
+ align-items: center;
19703
+ gap: 10px;
19704
+ min-width: 0;
19705
+ }
19706
+ [data-zen-remote="wizard"] .zr-wizard-stepCircle {
19707
+ width: 26px;
19708
+ height: 26px;
19709
+ border-radius: 50%;
19710
+ display: inline-flex;
19711
+ align-items: center;
19712
+ justify-content: center;
19713
+ font-size: 12px;
19714
+ font-weight: 600;
19715
+ border: 1.5px solid var(--dsw-alias-border-l2);
19716
+ background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-module-platform));
19717
+ color: var(--dsw-alias-label-tertiary);
19718
+ flex: none;
19719
+ transition: background-color 160ms ease, border-color 160ms ease, color 160ms ease, box-shadow 160ms ease;
19720
+ }
19721
+ [data-zen-remote="wizard"] .zr-wizard-stepItem[data-state="current"] .zr-wizard-stepCircle {
19722
+ background: var(--dsw-alias-label-primary);
19723
+ border-color: var(--dsw-alias-label-primary);
19724
+ color: var(--dsw-alias-bg-layer-2, #fff);
19725
+ box-shadow: 0 0 0 3px var(--dsw-alias-border-l2);
19726
+ }
19727
+ [data-zen-remote="wizard"] .zr-wizard-stepItem[data-state="done"] .zr-wizard-stepCircle {
19728
+ background: var(--dsw-alias-state-success-primary, #16a34a);
19729
+ border-color: var(--dsw-alias-state-success-primary, #16a34a);
19730
+ color: #fff;
19731
+ }
19732
+ [data-zen-remote="wizard"] .zr-wizard-stepText {
19733
+ display: flex;
19734
+ flex-direction: column;
19735
+ gap: 1px;
19736
+ min-width: 0;
19737
+ }
19738
+ [data-zen-remote="wizard"] .zr-wizard-stepTag {
19739
+ font-size: 11px;
19740
+ color: var(--dsw-alias-label-tertiary);
19741
+ line-height: 1.2;
19742
+ }
19743
+ [data-zen-remote="wizard"] .zr-wizard-stepItem[data-state="current"] .zr-wizard-stepTag {
19744
+ color: var(--dsw-alias-label-secondary);
19745
+ font-weight: 500;
19746
+ }
19747
+ [data-zen-remote="wizard"] .zr-wizard-stepItem[data-state="done"] .zr-wizard-stepTag {
19748
+ color: var(--dsw-alias-state-success-primary, #16a34a);
19749
+ }
19750
+ [data-zen-remote="wizard"] .zr-wizard-stepName {
19751
+ font-size: 13px;
19752
+ font-weight: 500;
19753
+ color: var(--dsw-alias-label-tertiary);
19754
+ white-space: nowrap;
19755
+ overflow: hidden;
19756
+ text-overflow: ellipsis;
19757
+ }
19758
+ [data-zen-remote="wizard"] .zr-wizard-stepItem[data-state="current"] .zr-wizard-stepName {
19759
+ color: var(--dsw-alias-label-primary);
19760
+ font-weight: 600;
19761
+ }
19762
+ [data-zen-remote="wizard"] .zr-wizard-stepItem[data-state="done"] .zr-wizard-stepName {
19763
+ color: var(--dsw-alias-label-secondary);
19764
+ }
19765
+ [data-zen-remote="wizard"] .zr-wizard-stepConnector {
19766
+ flex: 1;
19767
+ height: 2px;
19768
+ min-width: 18px;
19769
+ border-radius: 2px;
19770
+ background: var(--dsw-alias-border-l2);
19771
+ transition: background-color 200ms ease;
19772
+ }
19773
+ [data-zen-remote="wizard"] .zr-wizard-stepConnector[data-done="true"] {
19774
+ background: var(--dsw-alias-state-success-primary, #16a34a);
19775
+ }
19776
+ [data-zen-remote="wizard"] .zr-wizard-stepIntro {
19777
+ font-size: 12.5px;
19778
+ color: var(--dsw-alias-label-secondary);
19779
+ margin: 0;
19780
+ line-height: 1.45;
19781
+ }
19782
+
19783
+ [data-zen-remote="wizard"] .zr-wizard-body {
19784
+ display: flex;
19785
+ flex-direction: column;
19786
+ gap: 14px;
19787
+ }
19788
+
19789
+ /* Step 1: 3-column path cards */
19790
+ [data-zen-remote="wizard"] .zr-wizard-paths {
19791
+ display: grid;
19792
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
19793
+ gap: 10px;
19794
+ margin: 0;
19795
+ padding: 0;
19796
+ border: none;
19797
+ }
19798
+ [data-zen-remote="wizard"] .zr-wizard-path {
19799
+ display: flex;
19800
+ flex-direction: column;
19801
+ gap: 6px;
19802
+ text-align: left;
19803
+ border: 1px solid var(--dsw-alias-border-l2);
19804
+ border-radius: 10px;
19805
+ padding: 12px 14px;
19806
+ background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-module-platform));
19807
+ cursor: pointer;
19808
+ font: inherit;
19809
+ color: inherit;
19810
+ transition: border-color 120ms ease, box-shadow 120ms ease;
19811
+ }
19812
+ [data-zen-remote="wizard"] .zr-wizard-path:hover:not(:disabled) {
19813
+ border-color: var(--dsw-alias-label-secondary);
19814
+ }
19815
+ [data-zen-remote="wizard"] .zr-wizard-path[data-selected="true"] {
19816
+ border-color: var(--dsw-alias-label-primary);
19817
+ box-shadow: 0 0 0 1px var(--dsw-alias-label-primary);
19818
+ background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-module-platform));
19819
+ }
19820
+ [data-zen-remote="wizard"] .zr-wizard-path:disabled {
19821
+ cursor: not-allowed;
19822
+ opacity: 0.55;
19823
+ }
19824
+ [data-zen-remote="wizard"] .zr-wizard-pathHead {
19825
+ display: flex;
19826
+ align-items: center;
19827
+ gap: 8px;
19828
+ }
19829
+ [data-zen-remote="wizard"] .zr-wizard-pathRadio {
19830
+ width: 14px;
19831
+ height: 14px;
19832
+ border-radius: 50%;
19833
+ border: 1.5px solid var(--dsw-alias-label-tertiary);
19834
+ display: inline-flex;
19835
+ align-items: center;
19836
+ justify-content: center;
19837
+ flex: none;
19838
+ }
19839
+ [data-zen-remote="wizard"] .zr-wizard-pathRadio[data-selected="true"] {
19840
+ border-color: var(--dsw-alias-label-primary);
19841
+ background: var(--dsw-alias-label-primary);
19842
+ box-shadow: inset 0 0 0 2.5px var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-module-platform));
19843
+ }
19844
+ [data-zen-remote="wizard"] .zr-wizard-pathName {
19845
+ font-size: 13px;
19846
+ font-weight: 600;
19847
+ color: var(--dsw-alias-label-primary);
19848
+ margin: 0;
19849
+ }
19850
+ [data-zen-remote="wizard"] .zr-wizard-pathDesc {
19851
+ font-size: 12px;
19852
+ color: var(--dsw-alias-label-tertiary);
19853
+ margin: 0;
19854
+ line-height: 1.45;
19855
+ }
19856
+ [data-zen-remote="wizard"] .zr-wizard-sectionTitle {
19857
+ font-size: 12px;
19858
+ font-weight: 600;
19859
+ color: var(--dsw-alias-label-secondary);
19860
+ margin: 0;
19861
+ }
19862
+ [data-zen-remote="wizard"] .zr-wizard-roleGrid {
19863
+ display: grid;
19864
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
19865
+ gap: 10px;
19866
+ }
19867
+ [data-zen-remote="wizard"] .zr-wizard-checkRow {
19868
+ display: inline-flex;
19869
+ align-items: center;
19870
+ gap: 8px;
19871
+ font-size: 12.5px;
19872
+ color: var(--dsw-alias-label-primary);
19873
+ cursor: pointer;
19874
+ user-select: none;
19875
+ }
19876
+ [data-zen-remote="wizard"] .zr-wizard-checkRow input[type="checkbox"] {
19877
+ margin: 0;
19878
+ cursor: pointer;
19879
+ }
19880
+
19881
+ /* Step 2: form controls */
19882
+ [data-zen-remote="wizard"] .zr-wizard-selectedBar {
19883
+ display: flex;
19884
+ align-items: center;
19885
+ justify-content: space-between;
19886
+ gap: 8px;
19887
+ padding: 8px 12px;
19888
+ border-radius: 8px;
19889
+ border: 1px solid var(--dsw-alias-border-l2);
19890
+ background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-module-platform));
19891
+ }
19892
+ [data-zen-remote="wizard"] .zr-wizard-selectedLabel {
19893
+ font-size: 12.5px;
19894
+ font-weight: 600;
19895
+ color: var(--dsw-alias-label-primary);
19896
+ }
19897
+ [data-zen-remote="wizard"] .zr-wizard-formStack {
19898
+ display: flex;
19899
+ flex-direction: column;
19900
+ gap: 12px;
19901
+ }
19902
+ [data-zen-remote="wizard"] .zr-wizard-formGrid {
19903
+ display: grid;
19904
+ grid-template-columns: 1fr 120px;
19905
+ gap: 12px;
19906
+ align-items: start;
19907
+ }
19908
+ [data-zen-remote="wizard"] .zr-wizard-field {
19909
+ display: flex;
19910
+ flex-direction: column;
19911
+ gap: 6px;
19912
+ }
19913
+ [data-zen-remote="wizard"] .zr-wizard-labelRow {
19914
+ display: flex;
19915
+ align-items: center;
19916
+ gap: 8px;
19917
+ }
19918
+ [data-zen-remote="wizard"] .zr-wizard-label {
19919
+ font-size: 12.5px;
19920
+ font-weight: 500;
19921
+ color: var(--dsw-alias-label-primary);
19922
+ margin: 0;
19923
+ }
19924
+ [data-zen-remote="wizard"] .zr-wizard-savedTag {
19925
+ font-size: 11px;
19926
+ padding: 1px 7px;
19927
+ border-radius: 999px;
19928
+ border: 1px solid var(--dsw-alias-border-l2);
19929
+ color: var(--dsw-alias-state-success-primary, #16a34a);
19930
+ background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-module-platform));
19931
+ }
19932
+ [data-zen-remote="wizard"] .zr-wizard-hint {
19933
+ font-size: 12px;
19934
+ color: var(--dsw-alias-label-tertiary);
19935
+ margin: 0;
19936
+ line-height: 1.4;
19937
+ }
19938
+ [data-zen-remote="wizard"] .zr-wizard-input {
19939
+ height: 34px;
19940
+ font: inherit;
19941
+ font-size: 13px;
19942
+ padding: 0 10px;
19943
+ border: 1px solid var(--dsw-alias-border-l2);
19944
+ border-radius: 8px;
19945
+ background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-module-platform));
19946
+ color: var(--dsw-alias-label-primary);
19947
+ width: 100%;
19948
+ box-sizing: border-box;
19949
+ }
19950
+ [data-zen-remote="wizard"] code.zr-wizard-input {
19951
+ display: flex;
19952
+ align-items: center;
19953
+ min-height: 36px;
19954
+ height: auto;
19955
+ padding: 6px 12px;
19956
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
19957
+ line-height: 1.4;
19958
+ overflow-wrap: anywhere;
19959
+ word-break: break-all;
19960
+ }
19961
+ [data-zen-remote="wizard"] .zr-wizard-input:focus {
19962
+ outline: none;
19963
+ border-color: var(--dsw-alias-label-primary);
19964
+ }
19965
+ [data-zen-remote="wizard"] .zr-wizard-row {
19966
+ display: flex;
19967
+ align-items: center;
19968
+ gap: 8px;
19969
+ flex-wrap: wrap;
19970
+ }
19971
+ [data-zen-remote="wizard"] .zr-wizard-link {
19972
+ background: none;
19973
+ border: none;
19974
+ padding: 0;
19975
+ font: inherit;
19976
+ font-size: 12px;
19977
+ color: var(--dsw-alias-label-secondary);
19978
+ cursor: pointer;
19979
+ text-decoration: underline;
19980
+ }
19981
+ [data-zen-remote="wizard"] .zr-wizard-link:hover:not(:disabled) {
19982
+ color: var(--dsw-alias-label-primary);
19983
+ }
19984
+ [data-zen-remote="wizard"] .zr-wizard-linkBtn {
19985
+ font: inherit;
19986
+ font-size: 12px;
19987
+ padding: 4px 10px;
19988
+ border-radius: 6px;
19989
+ border: 1px solid var(--dsw-alias-border-l2);
19990
+ background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-module-platform));
19991
+ color: var(--dsw-alias-label-secondary);
19992
+ cursor: pointer;
19993
+ }
19994
+ [data-zen-remote="wizard"] .zr-wizard-linkBtn:hover:not(:disabled) {
19995
+ color: var(--dsw-alias-label-primary);
19996
+ border-color: var(--dsw-alias-label-secondary);
19997
+ }
19998
+
19999
+ /* Primary & Secondary action buttons + Footer */
20000
+ [data-zen-remote="wizard"] .zr-wizard-footer {
20001
+ display: flex;
20002
+ align-items: center;
20003
+ justify-content: space-between;
20004
+ gap: 10px;
20005
+ padding-top: 8px;
20006
+ border-top: 1px solid var(--dsw-alias-border-l2);
20007
+ }
20008
+ [data-zen-remote="wizard"] .zr-wizard-footerHint {
20009
+ font-size: 12px;
20010
+ color: var(--dsw-alias-label-secondary);
20011
+ }
20012
+ [data-zen-remote="wizard"] .zr-wizard-btn,
20013
+ [data-zen-remote="wizard-card"] .zr-wizard-btn {
20014
+ font: inherit;
20015
+ font-size: 12.5px;
20016
+ font-weight: 500;
20017
+ height: 32px;
20018
+ padding: 0 14px;
20019
+ border-radius: 8px;
20020
+ border: 1px solid var(--dsw-alias-border-l2);
20021
+ background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-module-platform));
20022
+ color: var(--dsw-alias-label-primary);
20023
+ cursor: pointer;
20024
+ display: inline-flex;
20025
+ align-items: center;
20026
+ justify-content: center;
20027
+ }
20028
+ [data-zen-remote="wizard"] .zr-wizard-btn:hover:not(:disabled),
20029
+ [data-zen-remote="wizard-card"] .zr-wizard-btn:hover:not(:disabled) {
20030
+ border-color: var(--dsw-alias-label-secondary);
20031
+ }
20032
+ [data-zen-remote="wizard"] .zr-wizard-btnPrimary,
20033
+ [data-zen-remote="wizard-card"] .zr-wizard-btnPrimary {
20034
+ background: var(--dsw-alias-label-primary);
20035
+ border-color: var(--dsw-alias-label-primary);
20036
+ color: var(--dsw-alias-bg-layer-2, #fff);
20037
+ }
20038
+ [data-zen-remote="wizard"] .zr-wizard-btnPrimary:hover:not(:disabled),
20039
+ [data-zen-remote="wizard-card"] .zr-wizard-btnPrimary:hover:not(:disabled) {
20040
+ opacity: 0.9;
20041
+ }
20042
+ [data-zen-remote="wizard"] .zr-wizard-btn:disabled,
20043
+ [data-zen-remote="wizard-card"] .zr-wizard-btn:disabled {
20044
+ cursor: not-allowed;
20045
+ opacity: 0.5;
20046
+ }
20047
+
20048
+ /* Step 3: service control bar, 3-column lights, and QR pairing panel */
20049
+ [data-zen-remote="wizard"] .zr-wizard-serviceBar {
20050
+ display: flex;
20051
+ align-items: center;
20052
+ justify-content: space-between;
20053
+ gap: 12px;
20054
+ flex-wrap: wrap;
20055
+ padding: 10px 12px;
20056
+ border: 1px solid var(--dsw-alias-border-l2);
20057
+ border-radius: 10px;
20058
+ background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-module-platform));
20059
+ }
20060
+ [data-zen-remote="wizard"] .zr-wizard-serviceInfo {
20061
+ display: flex;
20062
+ align-items: center;
20063
+ gap: 8px;
20064
+ flex-wrap: wrap;
20065
+ }
20066
+ [data-zen-remote="wizard"] .zr-wizard-lights {
20067
+ display: grid;
20068
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
20069
+ gap: 8px;
20070
+ margin: 0;
20071
+ padding: 0;
20072
+ list-style: none;
20073
+ }
20074
+ [data-zen-remote="wizard"] .zr-wizard-light {
20075
+ display: flex;
20076
+ align-items: center;
20077
+ justify-content: space-between;
20078
+ gap: 8px;
20079
+ padding: 9px 12px;
20080
+ border-radius: 8px;
20081
+ border: 1px solid var(--dsw-alias-border-l2);
20082
+ background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-module-platform));
20083
+ font-size: 12.5px;
20084
+ }
20085
+ [data-zen-remote="wizard"] .zr-wizard-lightMain {
20086
+ display: flex;
20087
+ align-items: center;
20088
+ gap: 8px;
20089
+ min-width: 0;
20090
+ }
20091
+ [data-zen-remote="wizard"] .zr-wizard-lightName {
20092
+ color: var(--dsw-alias-label-primary);
20093
+ font-weight: 500;
20094
+ }
20095
+ [data-zen-remote="wizard"] .zr-wizard-lightState {
20096
+ font-size: 11.5px;
20097
+ color: var(--dsw-alias-label-tertiary);
20098
+ }
20099
+ [data-zen-remote="wizard"] .zr-wizard-lightState[data-state="pass"] {
20100
+ color: var(--dsw-alias-state-success-primary, #16a34a);
20101
+ }
20102
+ [data-zen-remote="wizard"] .zr-wizard-lightState[data-state="fail"] {
20103
+ color: var(--dsw-alias-state-error-primary, #d5304f);
20104
+ }
20105
+
20106
+ [data-zen-remote="wizard"] .zr-wizard-dot {
20107
+ width: 8px;
20108
+ height: 8px;
20109
+ border-radius: 50%;
20110
+ background: var(--dsw-alias-label-tertiary);
20111
+ flex: none;
20112
+ }
20113
+ [data-zen-remote="wizard"] .zr-wizard-dot[data-state="pass"] {
20114
+ background: var(--dsw-alias-state-success-primary, #16a34a);
20115
+ }
20116
+ [data-zen-remote="wizard"] .zr-wizard-dot[data-state="fail"] {
20117
+ background: var(--dsw-alias-state-error-primary, #d5304f);
20118
+ }
20119
+ [data-zen-remote="wizard"] .zr-wizard-dot[data-state="pending"] {
20120
+ background: var(--dsw-alias-state-warning-primary, #b8860b);
20121
+ }
20122
+
20123
+ [data-zen-remote="wizard"] .zr-wizard-pairPanel {
20124
+ display: flex;
20125
+ align-items: center;
20126
+ gap: 18px;
20127
+ flex-wrap: wrap;
20128
+ padding: 14px;
20129
+ border-radius: 10px;
20130
+ border: 1px solid var(--dsw-alias-border-l2);
20131
+ background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-module-platform));
20132
+ }
20133
+ [data-zen-remote="wizard"] .zr-wizard-pairMeta {
20134
+ display: flex;
20135
+ flex-direction: column;
20136
+ gap: 4px;
20137
+ flex: 1;
20138
+ min-width: 180px;
20139
+ }
20140
+ [data-zen-remote="wizard"] .zr-wizard-code {
20141
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
20142
+ font-size: 24px;
20143
+ letter-spacing: 4px;
20144
+ font-weight: 600;
20145
+ color: var(--dsw-alias-label-primary);
20146
+ margin: 2px 0;
20147
+ }
20148
+ [data-zen-remote="wizard"] .zr-wizard-countdown {
20149
+ font-size: 12px;
20150
+ color: var(--dsw-alias-label-secondary);
20151
+ margin: 0;
20152
+ }
20153
+ [data-zen-remote="wizard"] .zr-wizard-qr {
20154
+ width: 156px;
20155
+ height: 156px;
20156
+ image-rendering: pixelated;
20157
+ border: 1px solid var(--dsw-alias-border-l2);
20158
+ border-radius: 8px;
20159
+ background: #fff;
20160
+ display: block;
20161
+ flex: none;
20162
+ }
20163
+ [data-zen-remote="wizard"] .zr-wizard-qr[data-usable="false"] {
20164
+ display: none;
20165
+ }
20166
+ [data-zen-remote="wizard"] .zr-wizard-targetTabs {
20167
+ display: grid;
20168
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
20169
+ gap: 6px;
20170
+ padding: 4px;
20171
+ border-radius: 9px;
20172
+ border: 1px solid var(--dsw-alias-border-l2);
20173
+ background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-module-platform));
20174
+ }
20175
+ [data-zen-remote="wizard"] .zr-wizard-targetTab {
20176
+ font: inherit;
20177
+ font-size: 12.5px;
20178
+ font-weight: 500;
20179
+ padding: 7px 12px;
20180
+ border-radius: 6px;
20181
+ border: 1px solid transparent;
20182
+ background: transparent;
20183
+ color: var(--dsw-alias-label-secondary);
20184
+ cursor: pointer;
20185
+ text-align: center;
20186
+ }
20187
+ [data-zen-remote="wizard"] .zr-wizard-targetTab[data-selected="true"] {
20188
+ background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-module-platform));
20189
+ border-color: var(--dsw-alias-border-l2);
20190
+ color: var(--dsw-alias-label-primary);
20191
+ font-weight: 600;
20192
+ }
20193
+ [data-zen-remote="wizard"] .zr-wizard-desktopBox,
20194
+ [data-zen-remote="wizard"] .zr-wizard-clientBox {
20195
+ display: flex;
20196
+ flex-direction: column;
20197
+ gap: 10px;
20198
+ padding: 14px;
20199
+ border-radius: 10px;
20200
+ border: 1px solid var(--dsw-alias-border-l2);
20201
+ background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-module-platform));
20202
+ }
20203
+ [data-zen-remote="wizard"] .zr-wizard-deviceList {
20204
+ display: flex;
20205
+ flex-direction: column;
20206
+ gap: 10px;
20207
+ margin: 0;
20208
+ padding: 0;
20209
+ list-style: none;
20210
+ }
20211
+ [data-zen-remote="wizard"] .zr-wizard-deviceItem {
20212
+ display: flex;
20213
+ flex-direction: column;
20214
+ gap: 8px;
20215
+ padding: 12px 14px;
20216
+ border-radius: 10px;
20217
+ border: 1px solid var(--dsw-alias-border-l2);
20218
+ background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-module-platform));
20219
+ font-size: 12.5px;
20220
+ }
20221
+ [data-zen-remote="wizard"] .zr-wizard-deviceHead {
20222
+ display: flex;
20223
+ align-items: center;
20224
+ justify-content: space-between;
20225
+ gap: 10px;
20226
+ flex-wrap: wrap;
20227
+ }
20228
+ [data-zen-remote="wizard"] .zr-wizard-deviceTitleRow {
20229
+ display: flex;
20230
+ align-items: center;
20231
+ gap: 8px;
20232
+ flex-wrap: wrap;
20233
+ min-width: 0;
20234
+ }
20235
+ [data-zen-remote="wizard"] .zr-wizard-deviceBadge {
20236
+ font-size: 11px;
20237
+ font-weight: 500;
20238
+ padding: 2px 8px;
20239
+ border-radius: 999px;
20240
+ border: 1px solid var(--dsw-alias-border-l2);
20241
+ background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-module-platform));
20242
+ color: var(--dsw-alias-label-secondary);
20243
+ }
20244
+ [data-zen-remote="wizard"] .zr-wizard-deviceActions {
20245
+ display: flex;
20246
+ align-items: center;
20247
+ gap: 8px;
20248
+ flex-wrap: wrap;
20249
+ }
20250
+ [data-zen-remote="wizard"] .zr-wizard-select {
20251
+ height: 30px;
20252
+ font: inherit;
20253
+ font-size: 12px;
20254
+ padding: 0 8px;
20255
+ border-radius: 6px;
20256
+ border: 1px solid var(--dsw-alias-border-l2);
20257
+ background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-module-platform));
20258
+ color: var(--dsw-alias-label-primary);
20259
+ }
20260
+
20261
+ /* The modal card: a body-level overlay, above the app chrome. */
20262
+ [data-zen-remote="wizard-card"] {
20263
+ position: fixed;
20264
+ inset: 0;
20265
+ z-index: 1000;
20266
+ display: flex;
20267
+ align-items: center;
20268
+ justify-content: center;
20269
+ background: rgba(0, 0, 0, 0.45);
20270
+ }
20271
+ [data-zen-remote="wizard-card"] .zr-wizard-cardPanel {
20272
+ background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-module-platform));
20273
+ color: var(--dsw-alias-label-primary);
20274
+ border: 1px solid var(--dsw-alias-border-l2);
20275
+ border-radius: 12px;
20276
+ width: min(560px, calc(100vw - 32px));
20277
+ max-height: min(82vh, 660px);
20278
+ overflow: auto;
20279
+ padding: 18px 20px;
20280
+ display: flex;
20281
+ flex-direction: column;
20282
+ gap: 14px;
20283
+ }
20284
+ [data-zen-remote="wizard-card"] .zr-wizard-cardHeader {
20285
+ display: flex;
20286
+ align-items: center;
20287
+ justify-content: space-between;
20288
+ gap: 8px;
20289
+ }
20290
+ [data-zen-remote="wizard-card"] .zr-wizard-cardTitle {
20291
+ font-size: 15px;
20292
+ font-weight: 600;
20293
+ margin: 0;
20294
+ }
20295
+ [data-zen-remote="wizard-modal"] .zr-wizard-cardCloseIcon,
20296
+ [data-zen-remote="wizard-card"] .zr-wizard-cardCloseIcon {
20297
+ border: none;
20298
+ background: none;
20299
+ color: var(--dsw-alias-label-secondary);
20300
+ font-size: 18px;
20301
+ line-height: 1;
20302
+ padding: 2px 6px;
20303
+ border-radius: 6px;
20304
+ cursor: pointer;
20305
+ }
20306
+ [data-zen-remote="wizard-modal"] .zr-wizard-cardCloseIcon:hover,
20307
+ [data-zen-remote="wizard-card"] .zr-wizard-cardCloseIcon:hover {
20308
+ color: var(--dsw-alias-label-primary);
20309
+ }
20310
+ [data-zen-remote="wizard-card"] .zr-wizard-cardBody {
20311
+ display: flex;
20312
+ flex-direction: column;
20313
+ gap: 10px;
20314
+ }
20315
+ [data-zen-remote="wizard-card"] .zr-wizard-cardSteps {
20316
+ display: flex;
20317
+ flex-direction: column;
20318
+ gap: 6px;
20319
+ }
20320
+ [data-zen-remote="wizard-card"] .zr-wizard-cardSubhead {
20321
+ font-size: 12.5px;
20322
+ font-weight: 600;
20323
+ color: var(--dsw-alias-label-primary);
20324
+ margin: 4px 0 0;
20325
+ }
20326
+ [data-zen-remote="wizard-card"] .zr-wizard-cardTable {
20327
+ width: 100%;
20328
+ border-collapse: collapse;
20329
+ font-size: 12px;
20330
+ border: 1px solid var(--dsw-alias-border-l2);
20331
+ border-radius: 8px;
20332
+ overflow: hidden;
20333
+ }
20334
+ [data-zen-remote="wizard-card"] .zr-wizard-cardTable td {
20335
+ padding: 6px 10px;
20336
+ border-bottom: 1px solid var(--dsw-alias-border-l2);
20337
+ }
20338
+ [data-zen-remote="wizard-card"] .zr-wizard-cardTable tr:last-child td {
20339
+ border-bottom: none;
20340
+ }
20341
+ [data-zen-remote="wizard-card"] .zr-wizard-cardPerm {
20342
+ font-weight: 500;
20343
+ color: var(--dsw-alias-label-primary);
20344
+ white-space: nowrap;
20345
+ }
20346
+ [data-zen-remote="wizard-card"] .zr-wizard-cardWhy {
20347
+ color: var(--dsw-alias-label-tertiary);
20348
+ }
20349
+ [data-zen-remote="wizard-card"] .zr-wizard-cardTabs {
20350
+ display: flex;
20351
+ gap: 6px;
20352
+ }
20353
+ [data-zen-remote="wizard-card"] .zr-wizard-cardTab {
20354
+ font: inherit;
20355
+ font-size: 12px;
20356
+ font-weight: 500;
20357
+ padding: 4px 12px;
20358
+ border-radius: 999px;
20359
+ border: 1px solid var(--dsw-alias-border-l2);
20360
+ background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-module-platform));
20361
+ color: var(--dsw-alias-label-secondary);
20362
+ cursor: pointer;
20363
+ }
20364
+ [data-zen-remote="wizard-card"] .zr-wizard-cardTab[data-selected="true"] {
20365
+ color: var(--dsw-alias-bg-layer-2, #fff);
20366
+ background: var(--dsw-alias-label-primary);
20367
+ border-color: var(--dsw-alias-label-primary);
20368
+ }
20369
+ [data-zen-remote="wizard-card"] .zr-wizard-cardStep {
20370
+ font-size: 12.5px;
20371
+ color: var(--dsw-alias-label-secondary);
20372
+ margin: 0;
20373
+ line-height: 1.45;
20374
+ }
20375
+ [data-zen-remote="wizard-card"] .zr-wizard-cardCode {
20376
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
20377
+ font-size: 12px;
20378
+ white-space: pre;
20379
+ background: var(--dsw-alias-bg-layer-3, rgba(127, 127, 127, 0.12));
20380
+ border: 1px solid var(--dsw-alias-border-l2);
20381
+ border-radius: 8px;
20382
+ padding: 10px 12px;
20383
+ overflow: auto;
20384
+ max-height: 260px;
20385
+ margin: 0;
20386
+ }
20387
+ [data-zen-remote="wizard-card"] .zr-wizard-field {
20388
+ display: flex;
20389
+ flex-direction: column;
20390
+ gap: 6px;
20391
+ }
20392
+ [data-zen-remote="wizard-card"] .zr-wizard-label {
20393
+ font-size: 12.5px;
20394
+ font-weight: 500;
20395
+ color: var(--dsw-alias-label-primary);
20396
+ }
20397
+ [data-zen-remote="wizard-card"] .zr-wizard-input {
20398
+ height: 34px;
20399
+ font: inherit;
20400
+ font-size: 13px;
20401
+ padding: 0 10px;
20402
+ border: 1px solid var(--dsw-alias-border-l2);
20403
+ border-radius: 8px;
20404
+ background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-module-platform));
20405
+ color: var(--dsw-alias-label-primary);
20406
+ width: 100%;
20407
+ box-sizing: border-box;
20408
+ }
20409
+ [data-zen-remote="wizard-card"] .zr-wizard-cardActions {
20410
+ display: flex;
20411
+ justify-content: flex-end;
20412
+ gap: 8px;
20413
+ padding-top: 8px;
20414
+ border-top: 1px solid var(--dsw-alias-border-l2);
20415
+ }
20416
+ `;
20417
+ };
20418
+ __modules["client/wizard/register-wizard.js"] = function (require, module, exports) {
20419
+ "use strict";
20420
+ Object.defineProperty(exports, "__esModule", { value: true });
20421
+ exports.WIZARD_BUNDLE_KEY = void 0;
20422
+ exports.registerGatewayWizard = registerGatewayWizard;
20423
+ const settings_form_ts_1 = require("./client-data/settings-form.js");
20424
+ const locales_ts_1 = require("./client/locales.js");
20425
+ const register_settings_ts_1 = require("./client/settings/register-settings.js");
20426
+ const wizard_css_ts_1 = require("./client/wizard/wizard-css.js");
20427
+ /** `plugins.bundle.config` key: the bundle's package name. */
20428
+ exports.WIZARD_BUNDLE_KEY = 'dsh-zen-remote';
20429
+ /**
20430
+ * Register the wizard stylesheet and (lazily, only where the settings
20431
+ * service exists) the bundle detail-page top block.
20432
+ * @param ctx - client root context.
20433
+ * @param section - the block component; a parameter so this module stays
20434
+ * JSX-free and importable under Node's type stripping.
20435
+ */
20436
+ function registerGatewayWizard(ctx, section) {
20437
+ ctx.effect(() => {
20438
+ const tag = document.createElement('style');
20439
+ tag.dataset.plugin = 'dsh-zen-remote-wizard';
20440
+ tag.textContent = wizard_css_ts_1.WIZARD_CSS;
20441
+ document.head.appendChild(tag);
20442
+ return () => { tag.remove(); };
20443
+ }, 'dsh-zen-remote: wizard styles');
20444
+ ctx.inject(['configForms'], (formsCtx) => {
20445
+ const scope = formsCtx.configForms.get(register_settings_ts_1.SETTINGS_ENTRY_ID);
20446
+ // Follow the describe sidecar when available so client-mode pairing can
20447
+ // see whether `deviceToken` is configured (degrades to false in minimal
20448
+ // test harnesses that omit `describe`).
20449
+ const describe = typeof formsCtx.configForms.describe === 'function' ? formsCtx.configForms.describe() : undefined;
20450
+ const secretConfigured = () => {
20451
+ const namespaces = describe?.getSnapshot().view?.namespaces ?? [];
20452
+ const row = namespaces.find((candidate) => candidate.ns === register_settings_ts_1.SETTINGS_ENTRY_ID);
20453
+ return (row?.secrets ?? []).some((secret) => secret.path.length === 1 && secret.path[0] === settings_form_ts_1.DEVICE_TOKEN_FIELD && secret.set);
20454
+ };
20455
+ const config = new settings_form_ts_1.ZenRemoteSettingsForm(scope, secretConfigured);
20456
+ const offDescribe = describe !== undefined ? describe.subscribe(() => { config.refresh(); }) : () => { };
20457
+ formsCtx.effect(() => () => { offDescribe(); config.dispose(); }, 'dsh-zen-remote: wizard form');
20458
+ const saveRowFields = async (fields, options) => {
20459
+ const snapshot = scope.getSnapshot();
20460
+ const ops = [
20461
+ ...Object.entries(fields).map(([key, value]) => ({ op: 'set', path: [key], value })),
20462
+ ...(options?.unset ?? []).map((key) => ({ op: 'unset', path: [key] })),
20463
+ ];
20464
+ await scope.mutate(ops, snapshot.revision);
20465
+ };
20466
+ formsCtx.effect(() => formsCtx.slots.inject('plugins.bundle.config', () => formsCtx.slots.register({
20467
+ name: 'plugins.bundle.config',
20468
+ key: exports.WIZARD_BUNDLE_KEY,
20469
+ locale: locales_ts_1.NS,
20470
+ inject: () => ({ config, saveRowFields }),
20471
+ }, section)), 'dsh-zen-remote: wizard section');
20472
+ });
20473
+ }
20474
+ };
17200
20475
  __modules["client/RemoteHeaderIcon.js"] = function (require, module, exports) {
17201
20476
  "use strict";
17202
20477
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -19772,6 +23047,8 @@ const share_preview_tsx_1 = require("./client/share/share-preview.js");
19772
23047
  const locales_ts_1 = require("./client/locales.js");
19773
23048
  const SettingsSection_tsx_1 = require("./client/settings/SettingsSection.js");
19774
23049
  const register_settings_ts_1 = require("./client/settings/register-settings.js");
23050
+ const GatewayWizard_tsx_1 = require("./client/wizard/GatewayWizard.js");
23051
+ const register_wizard_ts_1 = require("./client/wizard/register-wizard.js");
19775
23052
  const RemoteHeaderIcon_tsx_1 = require("./client/RemoteHeaderIcon.js");
19776
23053
  const RemoteShareMenu_tsx_1 = require("./client/RemoteShareMenu.js");
19777
23054
  const shares_ts_1 = require("./client-data/shares.js");
@@ -19832,6 +23109,12 @@ function apply(ctx) {
19832
23109
  // 的按需注入都在 registerSettingsPage 里(那个服务名绝不进顶层的 inject
19833
23110
  // 数组,否则没有该服务的环境整个界面半边都加载不了)。
19834
23111
  (0, register_settings_ts_1.registerSettingsPage)(ctx, SettingsSection_tsx_1.SettingsSection);
23112
+ // GW-10 网关配置引导「让手机连上来」:同样在桌面门之前注册——桌面端 App
23113
+ // 正是主服务端,wizard 恰好是它最有用的入口。部件自身三重门控:subject
23114
+ // 过滤(kind === 'row' && pkg.name === 'dsh-zen-remote')、保存角色检查
23115
+ // (client 角色不渲染)、viaGateway 时全部操作禁用。样式全部作用在组件
23116
+ // 自身的 data 属性下,≥1024px 的其他页面逐像素不受影响。
23117
+ (0, register_wizard_ts_1.registerGatewayWizard)(ctx, GatewayWizard_tsx_1.GatewayWizard);
19835
23118
  // T33b 会话共享的三个入口里前两个(标题行远程图标、会话右键菜单项):
19836
23119
  // 同样必须在桌面门之前注册——桌面端 App 就是主服务端,图标和菜单恰好
19837
23120
  // 住在它的标题行与会话菜单里。部件自身按数据降级:shares 路由 404