dsh-mobile 0.3.2 → 0.3.4

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.
package/lib/client.js CHANGED
@@ -10,8 +10,15 @@ window.__ModuleLoader__.load({
10
10
  en: {
11
11
  mobileAccess: "Mobile access",
12
12
  collapseMobileAccess: "Collapse Mobile access",
13
- downloadAndroid: "Download Android app",
13
+ downloadAndroid: "Download latest Android app",
14
+ downloadAndroidVersion: "Download latest Android app · v{version}",
14
15
  downloadAndroidAria: "Download the latest Android app from GitHub Releases",
16
+ downloadAndroidVersionAria: "Download Android app v{version} from GitHub Releases",
17
+ updatePlugin: "Update plugin",
18
+ updatePluginAria: "Update DSH Mobile to v{version}",
19
+ updatingPlugin: "Updating…",
20
+ pluginUpdatedRestart: "Updated to v{version}. Restart DSH to apply it.",
21
+ pluginUpdateFailed: "Update failed: {error}",
15
22
  lan: "Local network",
16
23
  remote: "Remote",
17
24
  lanAccess: "Local network access",
@@ -27,6 +34,10 @@ window.__ModuleLoader__.load({
27
34
  pairingQr: "Pairing QR code",
28
35
  remoteIntro: "Choose the remote channel that suits you. Switching or disabling remote access does not affect the local network.",
29
36
  chooseProvider: "Choose connection method",
37
+ currentProvider: "Current connection",
38
+ remoteStateOff: "Off",
39
+ remoteStateConnecting: "Connecting",
40
+ remoteStateAttention: "Needs attention",
30
41
  providerInfoAria: "View remote connection security and network information",
31
42
  providerGroupAria: "Remote connection method",
32
43
  providerSafeTitle: "You remain protected",
@@ -145,8 +156,15 @@ window.__ModuleLoader__.load({
145
156
  it: {
146
157
  mobileAccess: "Accesso mobile",
147
158
  collapseMobileAccess: "Riduci Accesso mobile",
148
- downloadAndroid: "Scarica app Android",
159
+ downloadAndroid: "Scarica l’ultima app Android",
160
+ downloadAndroidVersion: "Scarica l’ultima app Android · v{version}",
149
161
  downloadAndroidAria: "Scarica l’ultima app Android da GitHub Releases",
162
+ downloadAndroidVersionAria: "Scarica l’app Android v{version} da GitHub Releases",
163
+ updatePlugin: "Aggiorna plugin",
164
+ updatePluginAria: "Aggiorna DSH Mobile alla v{version}",
165
+ updatingPlugin: "Aggiornamento…",
166
+ pluginUpdatedRestart: "Aggiornato alla v{version}. Riavvia DSH per applicarlo.",
167
+ pluginUpdateFailed: "Aggiornamento non riuscito: {error}",
150
168
  lan: "Rete locale",
151
169
  remote: "Remoto",
152
170
  lanAccess: "Accesso rete locale",
@@ -162,6 +180,10 @@ window.__ModuleLoader__.load({
162
180
  pairingQr: "Codice QR di abbinamento",
163
181
  remoteIntro: "Scegli il canale remoto più adatto. Cambiare o disattivare l’accesso remoto non influisce sulla rete locale.",
164
182
  chooseProvider: "Scegli metodo di connessione",
183
+ currentProvider: "Connessione attuale",
184
+ remoteStateOff: "Disattivata",
185
+ remoteStateConnecting: "Connessione",
186
+ remoteStateAttention: "Da controllare",
165
187
  providerInfoAria: "Informazioni su sicurezza e rete della connessione remota",
166
188
  providerGroupAria: "Metodo di connessione remota",
167
189
  providerSafeTitle: "La protezione resta attiva",
@@ -282,8 +304,15 @@ window.__ModuleLoader__.load({
282
304
  Object.assign(MOBILE_CONTROL_MESSAGES.zh, {
283
305
  mobileAccess: "移动访问",
284
306
  collapseMobileAccess: "收起移动访问",
285
- downloadAndroid: "下载 Android App",
286
- downloadAndroidAria: "前往 GitHub Releases 下载最新版 Android App",
307
+ downloadAndroid: "下载最新 Android App",
308
+ downloadAndroidVersion: "下载最新 Android App · v{version}",
309
+ downloadAndroidAria: "从 GitHub Releases 下载最新版 Android App",
310
+ downloadAndroidVersionAria: "从 GitHub Releases 下载 Android App v{version}",
311
+ updatePlugin: "更新插件",
312
+ updatePluginAria: "将 DSH Mobile 更新至 v{version}",
313
+ updatingPlugin: "正在更新…",
314
+ pluginUpdatedRestart: "插件已更新至 v{version},重启 DSH 后生效。",
315
+ pluginUpdateFailed: "更新失败:{error}",
287
316
  lan: "局域网",
288
317
  remote: "远程",
289
318
  lanAccess: "局域网访问",
@@ -299,6 +328,10 @@ window.__ModuleLoader__.load({
299
328
  pairingQr: "配对二维码",
300
329
  remoteIntro: "选择更适合你的远程通道。切换或关闭远程访问不会影响局域网。",
301
330
  chooseProvider: "选择连接方式",
331
+ currentProvider: "当前连接",
332
+ remoteStateOff: "未启用",
333
+ remoteStateConnecting: "连接中",
334
+ remoteStateAttention: "需处理",
302
335
  providerInfoAria: "查看远程连接安全与网络说明",
303
336
  providerGroupAria: "远程连接方式",
304
337
  providerSafeTitle: "你始终可以放心",
@@ -414,6 +447,192 @@ window.__ModuleLoader__.load({
414
447
  resetTailscaleConfirm: "退出电脑上的 Tailscale 登录并移除所有远程配对设备?局域网配置不会改变。",
415
448
  requestFailed: "请求失败:{error}"
416
449
  });
450
+ Object.assign(MOBILE_CONTROL_MESSAGES.en, {
451
+ selfHostedConnections: "Self-hosted connection",
452
+ selfHostedDescription: "For users with a VPS and domain. It does not depend on public tunnel bandwidth.",
453
+ advanced: "Advanced",
454
+ frpName: "Self-hosted FRP",
455
+ frpDescription: "A restricted FRP channel for this DSH gateway only. You manage the VPS and public domain.",
456
+ prepareFrp: "Configure self-hosted FRP",
457
+ frpStep1Title: "1 · Connection details",
458
+ frpStep1Text: "Enter the VPS address, frps control port, shared token, and public HTTPS origin.",
459
+ frpServerAddress: "VPS address",
460
+ frpServerAddressPlaceholder: "frp.example.com",
461
+ frpServerPort: "frps port",
462
+ frpToken: "Shared token",
463
+ frpTokenPlaceholder: "At least 16 characters",
464
+ frpPublicOrigin: "Public HTTPS origin",
465
+ frpPublicOriginPlaceholder: "https://dsh.example.com",
466
+ frpStep2Title: "2 · Prepare the VPS",
467
+ frpStep2Text: "Copy the restricted frps and Caddy template, then apply it on your VPS.",
468
+ copyServerTemplate: "Copy server template",
469
+ templateCopied: "Server template copied. Apply frps.toml and the Caddyfile on the VPS.",
470
+ templateCopyFailed: "Could not copy the template. Check the four fields above.",
471
+ frpStep3Title: "3 · Prepare this computer",
472
+ frpStep3Text: "Download and verify the pinned official frpc client only when requested.",
473
+ installFrpc: "Install official frpc",
474
+ frpInstallConfirm: "Download and verify official frpc 0.70.1 (about {size} MB), extracting only frpc to the DSH Mobile private directory? No service, PATH entry, or startup item is added.",
475
+ installingFrp: "Installing the official frpc component. Keep DSH running until it completes.",
476
+ frpNotInstalled: "frpc is not installed. Nothing is downloaded until you click Install.",
477
+ frpUnsupported: "This desktop platform is not supported by the managed frpc installer.",
478
+ frpComponentReady: "Official frpc {version} is verified.",
479
+ frpStep4Title: "4 · Verify and connect",
480
+ frpStep4Text: "The plugin starts only its DSH HTTP vhost and verifies the public endpoint belongs to this computer.",
481
+ frpAppRequirement: "Custom remote domains require Android app 0.3.3 or later; local network, cpolar, and Tailscale remain compatible with older supported apps.",
482
+ frpSaveConnect: "Save and verify connection",
483
+ frpSavingConnecting: "Saving the private settings and verifying the complete FRP path…",
484
+ frpConfigurationReady: "Server settings are saved. The token remains only in the plugin private directory.",
485
+ frpConfigurationMissing: "Complete all fields, copy the VPS template, then save and connect.",
486
+ frpInputInvalid: "Check the VPS address, port, HTTPS origin, and token.",
487
+ frpComponentDetails: "FRP source and complete cleanup",
488
+ frpComponentDetailsText: "The pinned client is downloaded from the official fatedier/frp release. DSH Mobile never installs a service or accepts arbitrary FRP configuration.",
489
+ frpOfficialRelease: "Official FRP release",
490
+ purgeFrp: "Remove FRP component and private configuration",
491
+ purgeFrpConfirm: "Stop FRP and remove frpc, the shared token, generated configuration, staging files, and logs owned by DSH Mobile? The VPS is unchanged.",
492
+ purgingFrp: "Stopping FRP and removing files managed by DSH Mobile…",
493
+ switchingFrp: "Switching to self-hosted FRP…",
494
+ reconnectingFrp: "Verifying and reconnecting self-hosted FRP…",
495
+ resetFrpConfirm: "Disable self-hosted FRP and remove all remote paired devices? The saved VPS settings and server are unchanged.",
496
+ remoteUnavailableFrp: "frpc is not installed or the private server settings are incomplete.",
497
+ remoteStartingFrp: "Checking the VPS and preparing the restricted FRP channel…",
498
+ remoteConnectingFrp: "frpc is running; verifying the public HTTPS endpoint…",
499
+ frpMissing: "Install the official frpc component first.",
500
+ frpInvalid: "frpc verification failed. Remove it completely and reinstall.",
501
+ frpConfigMissing: "Save the self-hosted FRP connection details first.",
502
+ frpConfigVerifyFailed: "frpc rejected the generated configuration. Check the server details and token.",
503
+ frpVhostPublic: "The VPS plaintext HTTP vhost is publicly reachable. Bind it to 127.0.0.1 and let Caddy provide HTTPS.",
504
+ frpVhostProbeFailed: "The VPS address could not be checked. Confirm DNS and retry.",
505
+ frpLaunchFailed: "frpc failed to start.",
506
+ frpTimeout: "The public endpoint did not become ready. Check frps, Caddy, DNS, and firewall.",
507
+ frpDiscoveryMismatch: "The public domain points to another DSH computer. Check the Caddy and frps mappings.",
508
+ frpDiscoveryInvalid: "The public domain did not return a valid DSH Mobile discovery response.",
509
+ frpStopped: "The FRP connection stopped.",
510
+ frpExited: "frpc exited unexpectedly. Check the VPS configuration and reconnect."
511
+ });
512
+ Object.assign(MOBILE_CONTROL_MESSAGES.it, {
513
+ selfHostedConnections: "Connessione autogestita",
514
+ selfHostedDescription: "Per chi dispone di VPS e dominio. Non dipende dalla banda dei tunnel pubblici.",
515
+ advanced: "Avanzato",
516
+ frpName: "FRP autogestito",
517
+ frpDescription: "Canale FRP limitato al solo gateway DSH. VPS e dominio pubblico restano sotto il tuo controllo.",
518
+ prepareFrp: "Configura FRP autogestito",
519
+ frpStep1Title: "1 · Dati di connessione",
520
+ frpStep1Text: "Inserisci indirizzo VPS, porta di controllo frps, token condiviso e origine HTTPS pubblica.",
521
+ frpServerAddress: "Indirizzo VPS",
522
+ frpServerAddressPlaceholder: "frp.example.com",
523
+ frpServerPort: "Porta frps",
524
+ frpToken: "Token condiviso",
525
+ frpTokenPlaceholder: "Almeno 16 caratteri",
526
+ frpPublicOrigin: "Origine HTTPS pubblica",
527
+ frpPublicOriginPlaceholder: "https://dsh.example.com",
528
+ frpStep2Title: "2 · Prepara il VPS",
529
+ frpStep2Text: "Copia il modello limitato per frps e Caddy e applicalo sul VPS.",
530
+ copyServerTemplate: "Copia modello server",
531
+ templateCopied: "Modello server copiato. Applica frps.toml e Caddyfile sul VPS.",
532
+ templateCopyFailed: "Impossibile copiare il modello. Controlla i quattro campi sopra.",
533
+ frpStep3Title: "3 · Prepara questo computer",
534
+ frpStep3Text: "Scarica e verifica il client frpc ufficiale fissato solo su richiesta.",
535
+ installFrpc: "Installa frpc ufficiale",
536
+ frpInstallConfirm: "Scaricare e verificare frpc ufficiale 0.70.1 (circa {size} MB), estraendo solo frpc nella directory privata DSH Mobile? Non vengono aggiunti servizi, PATH o avvio automatico.",
537
+ installingFrp: "Installazione del componente frpc ufficiale. Mantieni DSH in esecuzione.",
538
+ frpNotInstalled: "frpc non è installato. Non viene scaricato nulla finché non scegli Installa.",
539
+ frpUnsupported: "Questa piattaforma desktop non è supportata dal programma di installazione gestito.",
540
+ frpComponentReady: "frpc ufficiale {version} verificato.",
541
+ frpStep4Title: "4 · Verifica e connetti",
542
+ frpStep4Text: "Il plugin avvia solo il vhost HTTP DSH e verifica che l’endpoint pubblico appartenga a questo computer.",
543
+ frpAppRequirement: "I domini remoti personalizzati richiedono l’app Android 0.3.3 o successiva; rete locale, cpolar e Tailscale restano compatibili con le app precedenti supportate.",
544
+ frpSaveConnect: "Salva e verifica connessione",
545
+ frpSavingConnecting: "Salvataggio privato e verifica del percorso FRP completo…",
546
+ frpConfigurationReady: "Impostazioni server salvate. Il token resta solo nella directory privata del plugin.",
547
+ frpConfigurationMissing: "Completa i campi, copia il modello VPS, quindi salva e connetti.",
548
+ frpInputInvalid: "Controlla indirizzo VPS, porta, origine HTTPS e token.",
549
+ frpComponentDetails: "Origine FRP e rimozione completa",
550
+ frpComponentDetailsText: "Il client fissato viene scaricato dalla release ufficiale fatedier/frp. DSH Mobile non installa servizi e non accetta configurazioni FRP arbitrarie.",
551
+ frpOfficialRelease: "Release FRP ufficiale",
552
+ purgeFrp: "Rimuovi componente FRP e configurazione privata",
553
+ purgeFrpConfirm: "Arrestare FRP e rimuovere frpc, token, configurazione, file temporanei e log gestiti da DSH Mobile? Il VPS non viene modificato.",
554
+ purgingFrp: "Arresto FRP e rimozione dei file gestiti da DSH Mobile…",
555
+ switchingFrp: "Passaggio a FRP autogestito…",
556
+ reconnectingFrp: "Verifica e riconnessione FRP autogestito…",
557
+ resetFrpConfirm: "Disattivare FRP autogestito e rimuovere tutti i dispositivi remoti? Impostazioni VPS e server restano invariati.",
558
+ remoteUnavailableFrp: "frpc non è installato o le impostazioni private sono incomplete.",
559
+ remoteStartingFrp: "Controllo VPS e preparazione del canale FRP limitato…",
560
+ remoteConnectingFrp: "frpc è in esecuzione; verifica dell’endpoint HTTPS pubblico…",
561
+ frpMissing: "Installa prima il componente frpc ufficiale.",
562
+ frpInvalid: "Verifica frpc non riuscita. Rimuovilo e reinstallalo.",
563
+ frpConfigMissing: "Salva prima i dati della connessione FRP.",
564
+ frpConfigVerifyFailed: "frpc ha rifiutato la configurazione generata. Controlla server e token.",
565
+ frpVhostPublic: "Il vhost HTTP in chiaro è raggiungibile pubblicamente. Limitalo a 127.0.0.1 e usa Caddy per HTTPS.",
566
+ frpVhostProbeFailed: "Impossibile controllare il VPS. Verifica il DNS e riprova.",
567
+ frpLaunchFailed: "Avvio di frpc non riuscito.",
568
+ frpTimeout: "Endpoint pubblico non pronto. Controlla frps, Caddy, DNS e firewall.",
569
+ frpDiscoveryMismatch: "Il dominio pubblico punta a un altro computer DSH. Controlla Caddy e frps.",
570
+ frpDiscoveryInvalid: "Il dominio pubblico non ha restituito una risposta DSH Mobile valida.",
571
+ frpStopped: "Connessione FRP arrestata.",
572
+ frpExited: "frpc è terminato inaspettatamente. Controlla il VPS e riconnettiti."
573
+ });
574
+ Object.assign(MOBILE_CONTROL_MESSAGES.zh, {
575
+ selfHostedConnections: "自建连接",
576
+ selfHostedDescription: "适合已有 VPS 和域名的用户,不受公共服务带宽限制。",
577
+ advanced: "高级",
578
+ frpName: "自建 FRP",
579
+ frpDescription: "只为当前 DSH 网关建立单用途 FRP 通道,VPS 和公开域名由你掌控。",
580
+ prepareFrp: "配置自建 FRP",
581
+ frpStep1Title: "1 · 填写连接信息",
582
+ frpStep1Text: "填写 VPS 地址、frps 控制端口、共享 Token 和公开 HTTPS 地址。",
583
+ frpServerAddress: "VPS 地址",
584
+ frpServerAddressPlaceholder: "frp.example.com",
585
+ frpServerPort: "frps 端口",
586
+ frpToken: "共享 Token",
587
+ frpTokenPlaceholder: "至少 16 个字符",
588
+ frpPublicOrigin: "公开 HTTPS 地址",
589
+ frpPublicOriginPlaceholder: "https://dsh.example.com",
590
+ frpStep2Title: "2 · 准备 VPS",
591
+ frpStep2Text: "复制受限的 frps 与 Caddy 模板,并在 VPS 上应用。",
592
+ copyServerTemplate: "复制服务器模板",
593
+ templateCopied: "服务器模板已复制,请在 VPS 应用 frps.toml 和 Caddyfile。",
594
+ templateCopyFailed: "无法复制模板,请检查上方四项信息。",
595
+ frpStep3Title: "3 · 准备这台电脑",
596
+ frpStep3Text: "仅在你确认后下载并校验固定版本的官方 frpc。",
597
+ installFrpc: "安装官方 frpc",
598
+ frpInstallConfirm: "从 FRP 官方 Release 下载并校验 frpc 0.70.1(约 {size} MB),只提取 frpc 到 DSH Mobile 私有目录?不会安装服务、写入 PATH 或设置开机启动。",
599
+ installingFrp: "正在安装官方 frpc,完成前请保持 DSH 运行。",
600
+ frpNotInstalled: "frpc 尚未安装。点击安装前不会下载任何内容。",
601
+ frpUnsupported: "当前桌面平台暂不支持托管安装 frpc。",
602
+ frpComponentReady: "官方 frpc {version} 已校验。",
603
+ frpStep4Title: "4 · 验证并连接",
604
+ frpStep4Text: "插件只启动 DSH 的 HTTP vhost,并确认公网端点确实属于这台电脑。",
605
+ frpAppRequirement: "自定义远程域名需要 Android App 0.3.3 或更高版本;局域网、cpolar 和 Tailscale 仍兼容此前受支持的 App。",
606
+ frpSaveConnect: "保存并验证连接",
607
+ frpSavingConnecting: "正在保存私有配置并验证完整 FRP 链路…",
608
+ frpConfigurationReady: "服务器配置已保存,Token 只保存在插件私有目录。",
609
+ frpConfigurationMissing: "请填写完整信息、复制 VPS 模板,然后保存并连接。",
610
+ frpInputInvalid: "请检查 VPS 地址、端口、HTTPS 地址和 Token。",
611
+ frpComponentDetails: "FRP 来源与彻底清理",
612
+ frpComponentDetailsText: "固定版本客户端来自 fatedier/frp 官方 Release。DSH Mobile 不安装系统服务,也不接受任意 FRP 配置。",
613
+ frpOfficialRelease: "FRP 官方 Release",
614
+ purgeFrp: "彻底移除 FRP 组件与私有配置",
615
+ purgeFrpConfirm: "关闭 FRP 并移除 DSH Mobile 管理的 frpc、共享 Token、生成配置、临时文件和日志?不会修改 VPS。",
616
+ purgingFrp: "正在关闭 FRP 并清理 DSH Mobile 管理的文件…",
617
+ switchingFrp: "正在切换到自建 FRP…",
618
+ reconnectingFrp: "正在验证并重新连接自建 FRP…",
619
+ resetFrpConfirm: "关闭自建 FRP 并移除所有远程配对设备?已保存的 VPS 配置和服务器不会改变。",
620
+ remoteUnavailableFrp: "frpc 尚未安装或私有服务器配置不完整。",
621
+ remoteStartingFrp: "正在检查 VPS 并准备受限 FRP 通道…",
622
+ remoteConnectingFrp: "frpc 已启动,正在验证公网 HTTPS 端点…",
623
+ frpMissing: "请先安装官方 frpc。",
624
+ frpInvalid: "frpc 校验失败,请彻底清理后重新安装。",
625
+ frpConfigMissing: "请先保存自建 FRP 连接信息。",
626
+ frpConfigVerifyFailed: "frpc 拒绝了生成的配置,请检查服务器信息和 Token。",
627
+ frpVhostPublic: "VPS 明文 HTTP vhost 可被公网访问。请将其限制到 127.0.0.1,并由 Caddy 提供 HTTPS。",
628
+ frpVhostProbeFailed: "无法检查 VPS 地址,请确认 DNS 后重试。",
629
+ frpLaunchFailed: "frpc 未能启动。",
630
+ frpTimeout: "公网端点未能就绪,请检查 frps、Caddy、DNS 和防火墙。",
631
+ frpDiscoveryMismatch: "公开域名连接到了另一台 DSH 电脑,请检查 Caddy 与 frps 映射。",
632
+ frpDiscoveryInvalid: "公开域名没有返回有效的 DSH Mobile 发现信息。",
633
+ frpStopped: "FRP 连接已停止。",
634
+ frpExited: "frpc 意外退出,请检查 VPS 配置后重新连接。"
635
+ });
417
636
  Object.assign(MOBILE_CONTROL_MESSAGES.en, {
418
637
  requestTimeout: "The operation timed out. Confirm that DSH is still running, then try again.",
419
638
  diagnostics: "Diagnostics",
@@ -673,6 +892,38 @@ window.__ModuleLoader__.load({
673
892
  }
674
893
  };
675
894
  //#endregion
895
+ //#region src/frp-template.ts
896
+ /** Loopback-only HTTP vhost port used between Caddy and frps. */
897
+ const FRP_VHOST_HTTP_PORT = 7080;
898
+ function publicDnsHostname(value) {
899
+ return value.length <= 253 && value.includes(".") && !/^[0-9.]+$/u.test(value) && !value.includes(":") && value.split(".").every((label) => label.length >= 1 && label.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u.test(label));
900
+ }
901
+ /** Build the only supported frps and Caddy configuration from validated user inputs. */
902
+ function createRestrictedFrpServerTemplate(serverPort, token, publicOrigin) {
903
+ if (!Number.isSafeInteger(serverPort) || serverPort < 1 || serverPort > 65535 || token.length < 16 || token.length > 512 || /[\s\u0000-\u001f\u007f]/u.test(token)) throw new Error("frp_template_input_invalid");
904
+ let url;
905
+ try {
906
+ url = new URL(publicOrigin);
907
+ } catch {
908
+ throw new Error("frp_template_input_invalid");
909
+ }
910
+ if (url.protocol !== "https:" || url.port !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "" || url.username !== "" || url.password !== "" || !publicDnsHostname(url.hostname)) throw new Error("frp_template_input_invalid");
911
+ return [
912
+ "# frps.toml",
913
+ `bindPort = ${String(serverPort)}`,
914
+ "proxyBindAddr = \"127.0.0.1\"",
915
+ `vhostHTTPPort = ${String(FRP_VHOST_HTTP_PORT)}`,
916
+ "auth.method = \"token\"",
917
+ `auth.token = ${JSON.stringify(token)}`,
918
+ "",
919
+ "# Caddyfile",
920
+ `${url.hostname} {`,
921
+ ` reverse_proxy 127.0.0.1:${String(FRP_VHOST_HTTP_PORT)}`,
922
+ "}",
923
+ ""
924
+ ].join("\n");
925
+ }
926
+ //#endregion
676
927
  //#region src/native-mobile.ts
677
928
  /** Mobile feature and compatibility rules applied to DSH React surfaces. */
678
929
  const NATIVE_MOBILE_STYLES = `
@@ -706,7 +957,7 @@ window.__ModuleLoader__.load({
706
957
  [data-dsh-mobile-header] [class*="_headerUtilities"] { gap:2px !important; }
707
958
  [data-dsh-mobile-header] [class*="_sessionLogButton"] { width:40px; min-width:40px; padding:0 !important; overflow:hidden; color:transparent; font-size:0 !important; }
708
959
  [data-dsh-mobile-header] [class*="_sessionLogButton"] > * { display:none !important; }
709
- [data-dsh-mobile-header] [class*="_sessionLogButton"]::after { color:var(--dsw-text, #171a21); content:"Log"; font-size:11px; font-weight:600; }
960
+ [data-dsh-mobile-header] [class*="_sessionLogButton"]::after { color:var(--dsw-alias-label-primary, #171a21); content:"Log"; font-size:11px; font-weight:600; }
710
961
  html[data-dsh-mobile-language="zh"] [data-dsh-mobile-header] [class*="_sessionLogButton"]::after { content:"日志"; }
711
962
  [data-dsh-mobile-header] [class*="_tabs"] { box-sizing:border-box !important; width:max-content !important; max-width:calc(100% - 58px) !important; min-height:28px !important; height:28px !important; margin-top:0 !important; padding-left:6px !important; padding-right:6px !important; overflow-x:auto; scrollbar-width:none; }
712
963
  [data-dsh-mobile-header] [class*="_tab"] { padding-bottom:5px !important; }
@@ -845,6 +1096,31 @@ window.__ModuleLoader__.load({
845
1096
  function firstByClassSuffix(root, suffix) {
846
1097
  return Array.from(root.querySelectorAll("[class]")).find((element) => classToken(element, suffix));
847
1098
  }
1099
+ /** Find the stock DSH application frame without mistaking a feature card for the shell. */
1100
+ function resolveNativeMobileFrame(root, dedicatedCenter) {
1101
+ if (dedicatedCenter !== void 0) return void 0;
1102
+ return Array.from(root.querySelectorAll("[class]")).find((candidate) => {
1103
+ return classToken(candidate, "_frame") && firstByClassSuffix(candidate, "_sidebarCol") !== void 0 && firstByClassSuffix(candidate, "_centerCol") !== void 0;
1104
+ });
1105
+ }
1106
+ /** Mark every mounted settings dialog so its mobile layout does not depend on the conversation shell. */
1107
+ function markNativeMobileSettings(root) {
1108
+ let marked = 0;
1109
+ for (const dialog of root.querySelectorAll("[role=\"dialog\"]")) {
1110
+ const children = Array.from(dialog.children);
1111
+ const nav = children.find((child) => classToken(child, "_nav"));
1112
+ const content = children.find((child) => classToken(child, "_content"));
1113
+ if (nav === void 0 || content === void 0) continue;
1114
+ dialog.dataset.dshMobileSettings = "true";
1115
+ nav.dataset.dshMobileSettingsNav = "true";
1116
+ content.dataset.dshMobileSettingsContent = "true";
1117
+ firstByClassSuffix(nav, "_navList")?.setAttribute("data-dsh-mobile-settings-list", "true");
1118
+ firstByClassSuffix(content, "_header")?.setAttribute("data-dsh-mobile-settings-header", "true");
1119
+ firstByClassSuffix(content, "_options")?.setAttribute("data-dsh-mobile-settings-options", "true");
1120
+ marked += 1;
1121
+ }
1122
+ return marked;
1123
+ }
848
1124
  const AUTO_HISTORY_THRESHOLD_PX = 64;
849
1125
  function controlledFileDragEvent(type, files, initialDropEffect) {
850
1126
  const event = new Event(type, {
@@ -1257,13 +1533,16 @@ window.__ModuleLoader__.load({
1257
1533
  };
1258
1534
  const sync = () => {
1259
1535
  scheduled = 0;
1260
- frame = firstByClassSuffix(document, "_frame");
1261
1536
  const dedicatedCenter = document.querySelector(".dshm-main") ?? void 0;
1537
+ const nextFrame = resolveNativeMobileFrame(document, dedicatedCenter);
1538
+ if (frame !== nextFrame) frame?.removeAttribute("data-dsh-mobile-frame");
1539
+ frame = nextFrame;
1262
1540
  if (frame !== void 0) frame.dataset.dshMobileFrame = "true";
1263
1541
  sidebar = frame === void 0 ? document.querySelector(".dshm-drawer") ?? void 0 : firstByClassSuffix(frame, "_sidebarCol");
1264
1542
  const center = frame === void 0 ? dedicatedCenter : firstByClassSuffix(frame, "_centerCol");
1265
1543
  const details = frame === void 0 ? void 0 : firstByClassSuffix(frame, "_detailsCol");
1266
1544
  const handle = frame === void 0 ? void 0 : firstByClassSuffix(frame, "_handle");
1545
+ markNativeMobileSettings(document);
1267
1546
  if (center === void 0) {
1268
1547
  bindHistoryScroller(void 0);
1269
1548
  syncMediaBinding(null);
@@ -1344,23 +1623,6 @@ window.__ModuleLoader__.load({
1344
1623
  const lastColumn = frame?.style.gridTemplateColumns.trim().split(/\s+/).at(-1);
1345
1624
  details.dataset.open = String(lastColumn !== void 0 && lastColumn !== "0px" && lastColumn !== "0");
1346
1625
  }
1347
- const settings = Array.from(document.querySelectorAll("[role=\"dialog\"]")).find((dialog) => {
1348
- return Array.from(dialog.children).find((child) => child instanceof HTMLElement && classToken(child, "_nav")) !== void 0;
1349
- });
1350
- if (settings !== void 0) {
1351
- settings.dataset.dshMobileSettings = "true";
1352
- const nav = Array.from(settings.children).find((child) => child instanceof HTMLElement && classToken(child, "_nav"));
1353
- const content = Array.from(settings.children).find((child) => child instanceof HTMLElement && classToken(child, "_content"));
1354
- if (nav !== void 0) {
1355
- nav.dataset.dshMobileSettingsNav = "true";
1356
- firstByClassSuffix(nav, "_navList")?.setAttribute("data-dsh-mobile-settings-list", "true");
1357
- }
1358
- if (content !== void 0) {
1359
- content.dataset.dshMobileSettingsContent = "true";
1360
- firstByClassSuffix(content, "_header")?.setAttribute("data-dsh-mobile-settings-header", "true");
1361
- firstByClassSuffix(content, "_options")?.setAttribute("data-dsh-mobile-settings-options", "true");
1362
- }
1363
- }
1364
1626
  if (sidebar === void 0) return;
1365
1627
  sidebar.dataset.dshMobileSidebar = "true";
1366
1628
  toggle = firstByClassSuffix(sidebar, "_toggle");
@@ -1552,6 +1814,24 @@ window.__ModuleLoader__.load({
1552
1814
  }
1553
1815
  const CONTROL_REQUEST_TIMEOUT_MS = 15e3;
1554
1816
  const LONG_CONTROL_REQUEST_TIMEOUT_MS = 13e4;
1817
+ const GITHUB_RELEASES_URL = "https://github.com/saya-ch/dsh-mobile/releases";
1818
+ const CONTROL_PANEL_ID = "dsh-mobile-control-panel";
1819
+ function releaseVersion(value) {
1820
+ return typeof value === "string" && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(value) ? value : void 0;
1821
+ }
1822
+ /** Reduce the loopback release response to trusted text and download metadata. */
1823
+ function clientReleaseInfo(data) {
1824
+ const latestVersion = releaseVersion(data.latestVersion);
1825
+ const androidVersion = releaseVersion(data.androidVersion);
1826
+ const expectedAndroidDownloadUrl = androidVersion === void 0 ? void 0 : `${GITHUB_RELEASES_URL}/download/v${androidVersion}/dsh-mobile-android-v${androidVersion}.apk`;
1827
+ const androidDownloadUrl = expectedAndroidDownloadUrl !== void 0 && data.androidDownloadUrl === expectedAndroidDownloadUrl ? expectedAndroidDownloadUrl : GITHUB_RELEASES_URL;
1828
+ return {
1829
+ updateAvailable: data.updateAvailable === true && latestVersion !== void 0,
1830
+ ...latestVersion === void 0 ? {} : { latestVersion },
1831
+ ...androidVersion === void 0 ? {} : { androidVersion },
1832
+ androidDownloadUrl
1833
+ };
1834
+ }
1555
1835
  async function requestJson(path, init, timeoutMs = CONTROL_REQUEST_TIMEOUT_MS) {
1556
1836
  const controller = new AbortController();
1557
1837
  const upstreamSignal = init?.signal;
@@ -1596,6 +1876,10 @@ window.__ModuleLoader__.load({
1596
1876
  if (url.protocol !== "https:" || url.hostname !== "login.tailscale.com" || url.port !== "" || url.username !== "" || url.password !== "") return "";
1597
1877
  return url.toString();
1598
1878
  }
1879
+ /** Build the copy-only restricted VPS template without sending the token to the host API. */
1880
+ function createFrpServerTemplateForClipboard(serverPort, token, publicOrigin) {
1881
+ return createRestrictedFrpServerTemplate(serverPort, token, publicOrigin);
1882
+ }
1599
1883
  function installControl() {
1600
1884
  const locale = selectedMobileControlLocale();
1601
1885
  const localeTag = locale === "it" ? "it-IT" : locale === "zh" ? "zh-CN" : "en-US";
@@ -1608,6 +1892,7 @@ window.__ModuleLoader__.load({
1608
1892
  const root = element("div", "dsh-mobile-control");
1609
1893
  root.lang = locale;
1610
1894
  const panel = element("section", "dsh-mobile-control__panel");
1895
+ panel.id = CONTROL_PANEL_ID;
1611
1896
  panel.hidden = true;
1612
1897
  panel.lang = locale;
1613
1898
  panel.setAttribute("aria-label", t("mobileAccess"));
@@ -1615,6 +1900,10 @@ window.__ModuleLoader__.load({
1615
1900
  const title = element("h2");
1616
1901
  title.textContent = t("mobileAccess");
1617
1902
  const headerActions = element("div", "dsh-mobile-control__header-actions");
1903
+ const updatePlugin = element("button", "dsh-mobile-control__update-plugin");
1904
+ updatePlugin.type = "button";
1905
+ updatePlugin.textContent = t("updatePlugin");
1906
+ updatePlugin.hidden = true;
1618
1907
  const diagnosticsEntry = element("button", "dsh-mobile-control__diagnostic-entry");
1619
1908
  diagnosticsEntry.type = "button";
1620
1909
  diagnosticsEntry.textContent = t("diagnostics");
@@ -1624,9 +1913,13 @@ window.__ModuleLoader__.load({
1624
1913
  close.type = "button";
1625
1914
  close.textContent = "×";
1626
1915
  close.setAttribute("aria-label", t("collapseMobileAccess"));
1627
- headerActions.append(diagnosticsEntry, close);
1916
+ headerActions.append(updatePlugin, diagnosticsEntry, close);
1917
+ const releaseNotice = element("p", "dsh-mobile-control__release-notice");
1918
+ releaseNotice.hidden = true;
1919
+ releaseNotice.setAttribute("role", "status");
1920
+ releaseNotice.setAttribute("aria-live", "polite");
1628
1921
  const appDownload = element("a", "dsh-mobile-control__app-download");
1629
- appDownload.href = "https://github.com/saya-ch/dsh-mobile/releases/latest";
1922
+ appDownload.href = GITHUB_RELEASES_URL;
1630
1923
  appDownload.target = "_blank";
1631
1924
  appDownload.rel = "noopener noreferrer";
1632
1925
  appDownload.textContent = t("downloadAndroid");
@@ -1733,7 +2026,6 @@ window.__ModuleLoader__.load({
1733
2026
  cpolarChoiceTop.append(cpolarChoiceName, cpolarChoiceBadge);
1734
2027
  cpolarChoice.append(cpolarChoiceTop, cpolarChoiceDescription);
1735
2028
  providerChoices.append(cpolarChoice, tailscaleChoice);
1736
- providerSection.append(providerHeading, providerInfo, providerChoices);
1737
2029
  const cpolarSetup = element("section", "dsh-mobile-control__cpolar-setup");
1738
2030
  cpolarSetup.hidden = true;
1739
2031
  const cpolarSetupTitle = element("h3", "dsh-mobile-control__section-title");
@@ -1795,6 +2087,128 @@ window.__ModuleLoader__.load({
1795
2087
  cpolarDetailsBody.append(cpolarDetailsText, cpolarStorage, cpolarOfficial, cpolarTerms, cpolarPurge);
1796
2088
  cpolarDetails.append(cpolarDetailsSummary, cpolarDetailsBody);
1797
2089
  cpolarSetup.append(cpolarSetupTitle, cpolarComponentStatus, cpolarInstall, cpolarAccount, cpolarDetails);
2090
+ const selfHosted = element("details", "dsh-mobile-control__self-hosted");
2091
+ const selfHostedSummary = element("summary", "dsh-mobile-control__self-hosted-summary");
2092
+ const selfHostedSummaryText = element("span");
2093
+ const selfHostedSummaryTitle = element("strong");
2094
+ selfHostedSummaryTitle.textContent = t("selfHostedConnections");
2095
+ const selfHostedSummaryDescription = element("span");
2096
+ selfHostedSummaryDescription.textContent = t("selfHostedDescription");
2097
+ const selfHostedBadge = element("span", "dsh-mobile-control__provider-badge is-frp");
2098
+ selfHostedBadge.textContent = t("advanced");
2099
+ selfHostedSummaryText.append(selfHostedSummaryTitle, selfHostedSummaryDescription);
2100
+ selfHostedSummary.append(selfHostedSummaryText, selfHostedBadge);
2101
+ const selfHostedBody = element("div", "dsh-mobile-control__self-hosted-body");
2102
+ const frpChoice = element("button", "dsh-mobile-control__provider is-frp");
2103
+ frpChoice.type = "button";
2104
+ frpChoice.setAttribute("aria-pressed", "false");
2105
+ const frpChoiceTop = element("span", "dsh-mobile-control__provider-top");
2106
+ const frpChoiceName = element("strong");
2107
+ frpChoiceName.textContent = t("frpName");
2108
+ const frpChoiceDescription = element("span", "dsh-mobile-control__provider-description");
2109
+ frpChoiceDescription.textContent = t("frpDescription");
2110
+ frpChoiceTop.append(frpChoiceName);
2111
+ frpChoice.append(frpChoiceTop, frpChoiceDescription);
2112
+ selfHostedBody.append(frpChoice);
2113
+ selfHosted.append(selfHostedSummary, selfHostedBody);
2114
+ providerSection.append(providerHeading, providerInfo, providerChoices, selfHosted);
2115
+ const frpSetup = element("section", "dsh-mobile-control__frp-setup");
2116
+ frpSetup.hidden = true;
2117
+ const frpSetupTitle = element("h3", "dsh-mobile-control__section-title");
2118
+ frpSetupTitle.textContent = t("prepareFrp");
2119
+ const frpStep1 = element("section", "dsh-mobile-control__frp-step");
2120
+ const frpStep1Title = element("strong");
2121
+ frpStep1Title.textContent = t("frpStep1Title");
2122
+ const frpStep1Text = element("p");
2123
+ frpStep1Text.textContent = t("frpStep1Text");
2124
+ const frpFields = element("div", "dsh-mobile-control__frp-fields");
2125
+ const frpServerLabel = element("label", "dsh-mobile-control__field");
2126
+ frpServerLabel.textContent = t("frpServerAddress");
2127
+ const frpServer = element("input");
2128
+ frpServer.type = "text";
2129
+ frpServer.autocomplete = "off";
2130
+ frpServer.spellcheck = false;
2131
+ frpServer.placeholder = t("frpServerAddressPlaceholder");
2132
+ const frpPortLabel = element("label", "dsh-mobile-control__field");
2133
+ frpPortLabel.textContent = t("frpServerPort");
2134
+ const frpPort = element("input");
2135
+ frpPort.type = "number";
2136
+ frpPort.inputMode = "numeric";
2137
+ frpPort.min = "1";
2138
+ frpPort.max = "65535";
2139
+ frpPort.value = "7000";
2140
+ const frpTokenLabel = element("label", "dsh-mobile-control__field");
2141
+ frpTokenLabel.textContent = t("frpToken");
2142
+ const frpToken = element("input");
2143
+ frpToken.type = "password";
2144
+ frpToken.autocomplete = "off";
2145
+ frpToken.spellcheck = false;
2146
+ frpToken.placeholder = t("frpTokenPlaceholder");
2147
+ const frpOriginLabel = element("label", "dsh-mobile-control__field");
2148
+ frpOriginLabel.textContent = t("frpPublicOrigin");
2149
+ const frpOrigin = element("input");
2150
+ frpOrigin.type = "url";
2151
+ frpOrigin.autocomplete = "off";
2152
+ frpOrigin.spellcheck = false;
2153
+ frpOrigin.placeholder = t("frpPublicOriginPlaceholder");
2154
+ frpServerLabel.append(frpServer);
2155
+ frpPortLabel.append(frpPort);
2156
+ frpTokenLabel.append(frpToken);
2157
+ frpOriginLabel.append(frpOrigin);
2158
+ frpFields.append(frpServerLabel, frpPortLabel, frpTokenLabel, frpOriginLabel);
2159
+ frpStep1.append(frpStep1Title, frpStep1Text, frpFields);
2160
+ const frpStep2 = element("section", "dsh-mobile-control__frp-step");
2161
+ const frpStep2Title = element("strong");
2162
+ frpStep2Title.textContent = t("frpStep2Title");
2163
+ const frpStep2Text = element("p");
2164
+ frpStep2Text.textContent = t("frpStep2Text");
2165
+ const frpCopyTemplate = element("button", "dsh-mobile-control__secondary dsh-mobile-control__frp-action");
2166
+ frpCopyTemplate.type = "button";
2167
+ frpCopyTemplate.textContent = t("copyServerTemplate");
2168
+ frpStep2.append(frpStep2Title, frpStep2Text, frpCopyTemplate);
2169
+ const frpStep3 = element("section", "dsh-mobile-control__frp-step");
2170
+ const frpStep3Title = element("strong");
2171
+ frpStep3Title.textContent = t("frpStep3Title");
2172
+ const frpStep3Text = element("p");
2173
+ frpStep3Text.textContent = t("frpStep3Text");
2174
+ const frpComponentStatus = element("p", "dsh-mobile-control__component-status");
2175
+ frpComponentStatus.textContent = t("checkingComponent");
2176
+ const frpInstall = element("button", "dsh-mobile-control__primary dsh-mobile-control__frp-action");
2177
+ frpInstall.type = "button";
2178
+ frpInstall.textContent = t("installFrpc");
2179
+ frpStep3.append(frpStep3Title, frpStep3Text, frpComponentStatus, frpInstall);
2180
+ const frpStep4 = element("section", "dsh-mobile-control__frp-step");
2181
+ const frpStep4Title = element("strong");
2182
+ frpStep4Title.textContent = t("frpStep4Title");
2183
+ const frpStep4Text = element("p");
2184
+ frpStep4Text.textContent = t("frpStep4Text");
2185
+ const frpAppRequirement = element("p", "dsh-mobile-control__frp-requirement");
2186
+ frpAppRequirement.textContent = t("frpAppRequirement");
2187
+ const frpConfigurationStatus = element("p", "dsh-mobile-control__component-status");
2188
+ frpConfigurationStatus.textContent = t("frpConfigurationMissing");
2189
+ const frpConfigure = element("button", "dsh-mobile-control__primary dsh-mobile-control__frp-action");
2190
+ frpConfigure.type = "button";
2191
+ frpConfigure.textContent = t("frpSaveConnect");
2192
+ frpStep4.append(frpStep4Title, frpStep4Text, frpAppRequirement, frpConfigurationStatus, frpConfigure);
2193
+ const frpDetails = element("details", "dsh-mobile-control__details");
2194
+ const frpDetailsSummary = element("summary");
2195
+ frpDetailsSummary.textContent = t("frpComponentDetails");
2196
+ const frpDetailsBody = element("div", "dsh-mobile-control__details-body");
2197
+ const frpDetailsText = element("p");
2198
+ frpDetailsText.textContent = t("frpComponentDetailsText");
2199
+ const frpStorage = element("code", "dsh-mobile-control__storage");
2200
+ frpStorage.textContent = t("pluginPrivateDirectory");
2201
+ const frpOfficial = element("a", "dsh-mobile-control__text-link");
2202
+ frpOfficial.href = "https://github.com/fatedier/frp/releases/tag/v0.70.1";
2203
+ frpOfficial.target = "_blank";
2204
+ frpOfficial.rel = "noopener noreferrer";
2205
+ frpOfficial.textContent = t("frpOfficialRelease");
2206
+ const frpPurge = element("button", "dsh-mobile-control__danger");
2207
+ frpPurge.type = "button";
2208
+ frpPurge.textContent = t("purgeFrp");
2209
+ frpDetailsBody.append(frpDetailsText, frpStorage, frpOfficial, frpPurge);
2210
+ frpDetails.append(frpDetailsSummary, frpDetailsBody);
2211
+ frpSetup.append(frpSetupTitle, frpStep1, frpStep2, frpStep3, frpStep4, frpDetails);
1798
2212
  const tailscaleInfo = element("details", "dsh-mobile-control__details");
1799
2213
  const tailscaleInfoSummary = element("summary");
1800
2214
  tailscaleInfoSummary.textContent = t("tailscaleHelp");
@@ -1803,6 +2217,18 @@ window.__ModuleLoader__.load({
1803
2217
  tailscaleInfoText.textContent = t("tailscaleHelpText");
1804
2218
  tailscaleInfoBody.append(tailscaleInfoText);
1805
2219
  tailscaleInfo.append(tailscaleInfoSummary, tailscaleInfoBody);
2220
+ const providerSetupHeader = element("div", "dsh-mobile-control__stage-header");
2221
+ const providerSetupHeading = element("h3", "dsh-mobile-control__section-title");
2222
+ providerSetupHeading.textContent = t("currentProvider");
2223
+ const providerSetupName = element("span", "dsh-mobile-control__stage-value");
2224
+ providerSetupName.textContent = "Tailscale Funnel";
2225
+ const remoteStateBadge = element("span", "dsh-mobile-control__state-badge");
2226
+ remoteStateBadge.textContent = t("remoteStateOff");
2227
+ const providerSetupMeta = element("div", "dsh-mobile-control__stage-meta");
2228
+ providerSetupMeta.append(providerSetupName, remoteStateBadge);
2229
+ providerSetupHeader.append(providerSetupHeading, providerSetupMeta);
2230
+ const providerSetupBody = element("div", "dsh-mobile-control__provider-setup-body");
2231
+ providerSetupBody.append(cpolarSetup, frpSetup, tailscaleInfo);
1806
2232
  const remoteAccess = element("div", "dsh-mobile-control__access");
1807
2233
  remoteAccess.hidden = true;
1808
2234
  const remoteAccessLabel = element("span", "dsh-mobile-control__access-label");
@@ -1872,6 +2298,8 @@ window.__ModuleLoader__.load({
1872
2298
  remoteManageRow.append(remoteDevices, remoteReset);
1873
2299
  const remoteDevicePanel = element("div", "dsh-mobile-control__devices");
1874
2300
  remoteDevicePanel.hidden = true;
2301
+ const remoteWorkspace = element("section", "dsh-mobile-control__remote-workspace");
2302
+ remoteWorkspace.append(providerSetupHeader, remoteStatus, remoteAccess, remoteGuide, providerSetupBody, remoteActions, remoteQr, remoteManageRow, remoteDevicePanel);
1875
2303
  const diagnosticsView = element("div", "dsh-mobile-control__view is-diagnostics");
1876
2304
  diagnosticsView.hidden = true;
1877
2305
  const diagnosticsIntro = element("p", "dsh-mobile-control__intro");
@@ -1916,9 +2344,9 @@ window.__ModuleLoader__.load({
1916
2344
  header.append(title, headerActions);
1917
2345
  actions.append(toggle, pair, linkPair);
1918
2346
  lanView.append(access, qrBox, status, extensionStatus, actions, manageRow, devicePanel);
1919
- remoteView.append(remoteIntro, providerSection, cpolarSetup, tailscaleInfo, remoteAccess, remoteQr, remoteStatus, remoteGuide, remoteActions, remoteManageRow, remoteDevicePanel);
2347
+ remoteView.append(remoteIntro, providerSection, remoteWorkspace);
1920
2348
  diagnosticsView.append(diagnosticsIntro, diagnosticsSummary, diagnosticsToolbar, diagnosticsFeedback, diagnosticsChecks, diagnosticsDetails);
1921
- panel.append(header, appDownload, switcher, lanView, remoteView, diagnosticsView);
2349
+ panel.append(header, releaseNotice, appDownload, switcher, lanView, remoteView, diagnosticsView);
1922
2350
  root.append(panel);
1923
2351
  document.body.append(root);
1924
2352
  let running = false;
@@ -1934,11 +2362,32 @@ window.__ModuleLoader__.load({
1934
2362
  let remoteProviderBusy = false;
1935
2363
  let cpolarInstalled = false;
1936
2364
  let cpolarConfigured = false;
2365
+ let frpInstalled = false;
2366
+ let frpConfigured = false;
2367
+ let frpDownloadSize = "14.0";
2368
+ let configuredFrpServer = "";
2369
+ let configuredFrpPort = 7e3;
2370
+ let configuredFrpOrigin = "";
1937
2371
  let providerInfoPinned = false;
1938
2372
  let providerInfoHovered = false;
1939
2373
  let previousAccessView = "lan";
1940
2374
  let diagnosticsBusy = false;
1941
2375
  let copiedDiagnosticReport = "";
2376
+ let pluginUpdateAvailable = false;
2377
+ let pluginLatestVersion = "";
2378
+ const renderRelease = (data) => {
2379
+ const release = clientReleaseInfo(data);
2380
+ pluginUpdateAvailable = release.updateAvailable;
2381
+ pluginLatestVersion = release.latestVersion ?? "";
2382
+ updatePlugin.hidden = !pluginUpdateAvailable || !diagnosticsView.hidden;
2383
+ updatePlugin.textContent = t("updatePlugin");
2384
+ if (pluginLatestVersion !== "") updatePlugin.setAttribute("aria-label", t("updatePluginAria", { version: pluginLatestVersion }));
2385
+ if (release.androidVersion !== void 0) {
2386
+ appDownload.textContent = t("downloadAndroidVersion", { version: release.androidVersion });
2387
+ appDownload.setAttribute("aria-label", t("downloadAndroidVersionAria", { version: release.androidVersion }));
2388
+ }
2389
+ appDownload.href = release.androidDownloadUrl;
2390
+ };
1942
2391
  const syncProviderInfo = () => {
1943
2392
  const open = providerInfoPinned || providerInfoHovered || providerInfo.contains(document.activeElement);
1944
2393
  providerInfoPopover.hidden = !open;
@@ -1979,6 +2428,7 @@ window.__ModuleLoader__.load({
1979
2428
  diagnosticsEntry.setAttribute("aria-pressed", String(view === "diagnostics"));
1980
2429
  diagnosticsEntry.textContent = view === "diagnostics" ? t("back") : t("diagnostics");
1981
2430
  diagnosticsEntry.setAttribute("aria-label", view === "diagnostics" ? t("backToMobile") : t("openDiagnostics"));
2431
+ updatePlugin.hidden = view === "diagnostics" || !pluginUpdateAvailable;
1982
2432
  appDownload.hidden = view === "diagnostics";
1983
2433
  switcher.hidden = view === "diagnostics";
1984
2434
  title.textContent = view === "lan" ? t("lanAccess") : view === "remote" ? t("remoteAccess") : t("connectionDiagnostics");
@@ -2130,17 +2580,25 @@ window.__ModuleLoader__.load({
2130
2580
  });
2131
2581
  const renderRemote = (data) => {
2132
2582
  remoteRunning = data.running === true;
2133
- remoteProvider = data.provider === "cpolar" ? "cpolar" : "tailscale";
2583
+ remoteProvider = data.provider === "cpolar" ? "cpolar" : data.provider === "frp" ? "frp" : "tailscale";
2134
2584
  const cpolar = remoteProvider === "cpolar";
2135
- tailscaleChoice.classList.toggle("is-selected", !cpolar);
2585
+ const frp = remoteProvider === "frp";
2586
+ const tailscale = remoteProvider === "tailscale";
2587
+ tailscaleChoice.classList.toggle("is-selected", tailscale);
2136
2588
  cpolarChoice.classList.toggle("is-selected", cpolar);
2137
- tailscaleChoice.setAttribute("aria-checked", String(!cpolar));
2589
+ frpChoice.classList.toggle("is-selected", frp);
2590
+ tailscaleChoice.setAttribute("aria-checked", String(tailscale));
2138
2591
  cpolarChoice.setAttribute("aria-checked", String(cpolar));
2592
+ frpChoice.setAttribute("aria-pressed", String(frp));
2593
+ providerSetupName.textContent = cpolar ? "cpolar" : frp ? t("frpName") : "Tailscale Funnel";
2139
2594
  tailscaleChoice.disabled = remoteProviderBusy;
2140
2595
  cpolarChoice.disabled = remoteProviderBusy;
2596
+ frpChoice.disabled = remoteProviderBusy;
2141
2597
  cpolarSetup.hidden = !cpolar;
2142
- tailscaleInfo.hidden = cpolar;
2143
- remoteReset.textContent = cpolar ? t("resetRemoteDevices") : t("resetRemoteLogin");
2598
+ frpSetup.hidden = !frp;
2599
+ if (frp) selfHosted.open = true;
2600
+ tailscaleInfo.hidden = !tailscale;
2601
+ remoteReset.textContent = tailscale ? t("resetRemoteLogin") : t("resetRemoteDevices");
2144
2602
  const providers = data.providers !== null && typeof data.providers === "object" ? data.providers : {};
2145
2603
  const cpolarProvider = providers.cpolar !== null && typeof providers.cpolar === "object" ? providers.cpolar : {};
2146
2604
  const component = cpolarProvider.component !== null && typeof cpolarProvider.component === "object" ? cpolarProvider.component : {};
@@ -2160,11 +2618,37 @@ window.__ModuleLoader__.load({
2160
2618
  cpolarConfigure.disabled = remoteProviderBusy;
2161
2619
  cpolarPurge.hidden = !cpolarInstalled && !cpolarConfigured;
2162
2620
  cpolarComponentStatus.textContent = !cpolarSupported ? t("cpolarUnsupported") : !cpolarInstalled ? t("cpolarNotInstalled") : !cpolarConfigured ? t("cpolarNeedsToken", { version: componentVersion }) : t("cpolarReady", { version: componentVersion });
2621
+ const frpProvider = providers.frp !== null && typeof providers.frp === "object" ? providers.frp : {};
2622
+ const frpComponent = frpProvider.component !== null && typeof frpProvider.component === "object" ? frpProvider.component : {};
2623
+ const frpConfiguration = frpProvider.configuration !== null && typeof frpProvider.configuration === "object" ? frpProvider.configuration : {};
2624
+ frpInstalled = frpComponent.installed === true;
2625
+ frpConfigured = frpConfiguration.configured === true;
2626
+ const frpSupported = frpComponent.supported !== false;
2627
+ const frpVersion = typeof frpComponent.version === "string" ? frpComponent.version : "";
2628
+ const frpDownloadBytes = typeof frpComponent.downloadBytes === "number" ? frpComponent.downloadBytes : 0;
2629
+ if (frpDownloadBytes > 0) frpDownloadSize = formatMegabytes(frpDownloadBytes);
2630
+ const frpStoragePath = typeof frpComponent.storagePath === "string" ? frpComponent.storagePath : `DSH Mobile ${t("pluginPrivateDirectory")}`;
2631
+ configuredFrpServer = typeof frpConfiguration.serverAddress === "string" ? frpConfiguration.serverAddress : "";
2632
+ configuredFrpPort = typeof frpConfiguration.serverPort === "number" ? frpConfiguration.serverPort : 7e3;
2633
+ configuredFrpOrigin = typeof frpConfiguration.publicOrigin === "string" ? frpConfiguration.publicOrigin : "";
2634
+ if (frpServer.value === "" && configuredFrpServer !== "") frpServer.value = configuredFrpServer;
2635
+ if ((frpPort.value === "" || frpPort.value === "7000") && configuredFrpPort !== 7e3) frpPort.value = String(configuredFrpPort);
2636
+ if (frpOrigin.value === "" && configuredFrpOrigin !== "") frpOrigin.value = configuredFrpOrigin;
2637
+ frpStorage.textContent = frpStoragePath;
2638
+ frpStorage.title = frpStoragePath;
2639
+ frpInstall.hidden = frpInstalled || !frpSupported;
2640
+ frpInstall.textContent = frpDownloadBytes > 0 ? t("installWithSize", { size: formatMegabytes(frpDownloadBytes) }) : t("installFrpc");
2641
+ frpInstall.disabled = remoteProviderBusy;
2642
+ frpConfigure.disabled = remoteProviderBusy || !frpInstalled;
2643
+ frpPurge.hidden = !frpInstalled && !frpConfigured;
2644
+ frpComponentStatus.textContent = !frpSupported ? t("frpUnsupported") : frpInstalled ? t("frpComponentReady", { version: frpVersion }) : t("frpNotInstalled");
2645
+ frpConfigurationStatus.textContent = frpConfigured ? t("frpConfigurationReady") : t("frpConfigurationMissing");
2646
+ selfHostedBadge.textContent = frpConfigured && frpInstalled ? t("ready") : t("advanced");
2163
2647
  const state = typeof data.state === "string" ? data.state : "error";
2164
2648
  const errorCode = typeof data.errorCode === "string" ? data.errorCode : "";
2165
2649
  const remoteOrigin = typeof data.origin === "string" ? data.origin : "";
2166
2650
  remoteLoginUrl = typeof data.loginUrl === "string" ? data.loginUrl : "";
2167
- const candidateSetupUrl = cpolar ? "" : officialFunnelSetupUrl(data.setupUrl);
2651
+ const candidateSetupUrl = tailscale ? officialFunnelSetupUrl(data.setupUrl) : "";
2168
2652
  remoteSetupUrl = candidateSetupUrl !== "" ? candidateSetupUrl : {
2169
2653
  funnel_permission_required: "https://tailscale.com/s/no-funnel",
2170
2654
  funnel_https_required: "https://tailscale.com/s/https",
@@ -2172,6 +2656,10 @@ window.__ModuleLoader__.load({
2172
2656
  }[errorCode] ?? "";
2173
2657
  const needsFunnelSetup = state === "error" && remoteSetupUrl !== "";
2174
2658
  remoteReady = remoteRunning && state === "ready" && remoteOrigin !== "";
2659
+ remoteStateBadge.classList.toggle("is-ready", remoteReady);
2660
+ remoteStateBadge.classList.toggle("is-busy", state === "starting" || state === "connecting" || state === "needs-login");
2661
+ remoteStateBadge.classList.toggle("is-attention", state === "error" || state === "unavailable");
2662
+ remoteStateBadge.textContent = remoteReady ? t("ready") : state === "starting" || state === "connecting" || state === "needs-login" ? t("remoteStateConnecting") : state === "error" || state === "unavailable" ? t("remoteStateAttention") : t("remoteStateOff");
2175
2663
  remoteAccess.hidden = !remoteReady;
2176
2664
  remoteAccessLink.href = remoteOrigin;
2177
2665
  remoteAccessLink.textContent = remoteOrigin;
@@ -2179,10 +2667,10 @@ window.__ModuleLoader__.load({
2179
2667
  remoteStatus.classList.toggle("is-running", remoteReady);
2180
2668
  const labels = {
2181
2669
  off: t("remoteOff"),
2182
- unavailable: cpolar ? t("remoteUnavailableCpolar") : t("remoteUnavailableTailscale"),
2183
- starting: cpolar ? t("remoteStartingCpolar") : t("remoteStartingTailscale"),
2670
+ unavailable: cpolar ? t("remoteUnavailableCpolar") : frp ? t("remoteUnavailableFrp") : t("remoteUnavailableTailscale"),
2671
+ starting: cpolar ? t("remoteStartingCpolar") : frp ? t("remoteStartingFrp") : t("remoteStartingTailscale"),
2184
2672
  "needs-login": t("remoteNeedsLogin"),
2185
- connecting: cpolar ? t("remoteConnectingCpolar") : t("remoteConnectingTailscale"),
2673
+ connecting: cpolar ? t("remoteConnectingCpolar") : frp ? t("remoteConnectingFrp") : t("remoteConnectingTailscale"),
2186
2674
  ready: t("remoteReady"),
2187
2675
  error: t("remoteError")
2188
2676
  };
@@ -2204,17 +2692,30 @@ window.__ModuleLoader__.load({
2204
2692
  cpolar_stopped: t("cpolarStopped"),
2205
2693
  cpolar_exited: t("cpolarExited"),
2206
2694
  cpolar_invalid_output: t("cpolarOutputInvalid"),
2207
- cpolar_invalid_origin: t("cpolarOriginInvalid")
2695
+ cpolar_invalid_origin: t("cpolarOriginInvalid"),
2696
+ frp_component_missing: t("frpMissing"),
2697
+ frp_component_invalid: t("frpInvalid"),
2698
+ frp_config_missing: t("frpConfigMissing"),
2699
+ frp_config_verify_failed: t("frpConfigVerifyFailed"),
2700
+ frp_vhost_publicly_reachable: t("frpVhostPublic"),
2701
+ frp_vhost_probe_failed: t("frpVhostProbeFailed"),
2702
+ frp_launch_failed: t("frpLaunchFailed"),
2703
+ frp_start_timeout: t("frpTimeout"),
2704
+ frp_discovery_mismatch: t("frpDiscoveryMismatch"),
2705
+ frp_discovery_invalid: t("frpDiscoveryInvalid"),
2706
+ frp_stopped: t("frpStopped"),
2707
+ frp_exited: t("frpExited")
2208
2708
  };
2209
2709
  remoteStatus.textContent = remoteSetupPending && needsFunnelSetup ? t("setupOpened") : state === "error" ? errorLabels[errorCode] ?? labels.error : labels[state] ?? labels.error;
2210
2710
  remoteGuide.hidden = !needsFunnelSetup;
2211
2711
  remoteSetup.disabled = remoteSetupUrl === "" || remoteReconnectBusy;
2212
2712
  remoteSetupRetry.disabled = remoteReconnectBusy;
2213
2713
  remoteToggle.textContent = remoteRunning ? t("disableRemote") : t("enableRemote");
2214
- remoteToggle.disabled = remoteProviderBusy || cpolar && (!cpolarInstalled || !cpolarConfigured);
2215
- remoteLogin.hidden = cpolar || state !== "needs-login" || remoteLoginUrl === "";
2216
- remoteReconnect.hidden = needsFunnelSetup || state !== "error" && state !== "unavailable" || cpolar && (!cpolarInstalled || !cpolarConfigured);
2217
- remoteActions.hidden = cpolar && (!cpolarInstalled || !cpolarConfigured);
2714
+ const providerPrepared = cpolar ? cpolarInstalled && cpolarConfigured : frp ? frpInstalled && frpConfigured : true;
2715
+ remoteToggle.disabled = remoteProviderBusy || !providerPrepared;
2716
+ remoteLogin.hidden = !tailscale || state !== "needs-login" || remoteLoginUrl === "";
2717
+ remoteReconnect.hidden = needsFunnelSetup || state !== "error" && state !== "unavailable" || !providerPrepared;
2718
+ remoteActions.hidden = !providerPrepared;
2218
2719
  remotePair.disabled = !remoteReady;
2219
2720
  remoteDevices.disabled = !remoteReady;
2220
2721
  if (!remoteReady) remoteQr.hidden = true;
@@ -2236,7 +2737,8 @@ window.__ModuleLoader__.load({
2236
2737
  remoteProviderBusy = true;
2237
2738
  tailscaleChoice.disabled = true;
2238
2739
  cpolarChoice.disabled = true;
2239
- remoteStatus.textContent = provider === "cpolar" ? t("switchingCpolar") : t("switchingTailscale");
2740
+ frpChoice.disabled = true;
2741
+ remoteStatus.textContent = provider === "cpolar" ? t("switchingCpolar") : provider === "frp" ? t("switchingFrp") : t("switchingTailscale");
2240
2742
  controlRequestJson("/api/mobile-access/remote/provider", {
2241
2743
  method: "POST",
2242
2744
  body: JSON.stringify({ provider })
@@ -2253,6 +2755,9 @@ window.__ModuleLoader__.load({
2253
2755
  cpolarChoice.addEventListener("click", () => {
2254
2756
  chooseRemoteProvider("cpolar");
2255
2757
  });
2758
+ frpChoice.addEventListener("click", () => {
2759
+ chooseRemoteProvider("frp");
2760
+ });
2256
2761
  cpolarInstall.addEventListener("click", () => {
2257
2762
  if (remoteProviderBusy) return;
2258
2763
  if (!window.confirm(t("installConfirm"))) return;
@@ -2318,6 +2823,98 @@ window.__ModuleLoader__.load({
2318
2823
  loadRemote();
2319
2824
  });
2320
2825
  });
2826
+ const validFrpServer = (value) => value === value.trim() && value.length > 0 && value.length <= 253 && !/[\s\u0000-\u001f\u007f/\\@?#]/u.test(value);
2827
+ const frpForm = () => ({
2828
+ serverAddress: frpServer.value.trim(),
2829
+ serverPort: Number(frpPort.value),
2830
+ token: frpToken.value,
2831
+ publicOrigin: frpOrigin.value.trim()
2832
+ });
2833
+ const validFrpForm = (form) => {
2834
+ if (!validFrpServer(form.serverAddress)) return false;
2835
+ try {
2836
+ createFrpServerTemplateForClipboard(form.serverPort, form.token, form.publicOrigin);
2837
+ return true;
2838
+ } catch {
2839
+ return false;
2840
+ }
2841
+ };
2842
+ frpCopyTemplate.addEventListener("click", () => {
2843
+ const form = frpForm();
2844
+ if (!validFrpForm(form)) {
2845
+ remoteStatus.textContent = t("frpInputInvalid");
2846
+ return;
2847
+ }
2848
+ navigator.clipboard.writeText(createFrpServerTemplateForClipboard(form.serverPort, form.token, form.publicOrigin)).then(() => {
2849
+ remoteStatus.textContent = t("templateCopied");
2850
+ }, () => {
2851
+ remoteStatus.textContent = t("templateCopyFailed");
2852
+ });
2853
+ });
2854
+ frpInstall.addEventListener("click", () => {
2855
+ if (remoteProviderBusy) return;
2856
+ if (!window.confirm(t("frpInstallConfirm", { size: frpDownloadSize }))) return;
2857
+ remoteProviderBusy = true;
2858
+ frpInstall.disabled = true;
2859
+ frpInstall.textContent = t("downloading");
2860
+ remoteStatus.textContent = t("installingFrp");
2861
+ controlRequestJson("/api/mobile-access/remote/frp/component/install", {
2862
+ method: "POST",
2863
+ body: JSON.stringify({ confirm: true })
2864
+ }, LONG_CONTROL_REQUEST_TIMEOUT_MS).then(renderRemote, (error) => {
2865
+ remoteStatus.textContent = t("installFailed", { error: String(error) });
2866
+ }).finally(() => {
2867
+ remoteProviderBusy = false;
2868
+ loadRemote();
2869
+ });
2870
+ });
2871
+ frpConfigure.addEventListener("click", () => {
2872
+ if (remoteProviderBusy || !frpInstalled) return;
2873
+ const form = frpForm();
2874
+ const unchanged = frpConfigured && form.token === "" && form.serverAddress === configuredFrpServer && form.serverPort === configuredFrpPort && form.publicOrigin === configuredFrpOrigin;
2875
+ if (!unchanged && !validFrpForm(form)) {
2876
+ remoteStatus.textContent = t("frpInputInvalid");
2877
+ return;
2878
+ }
2879
+ remoteProviderBusy = true;
2880
+ frpConfigure.disabled = true;
2881
+ frpConfigure.setAttribute("aria-busy", "true");
2882
+ frpConfigure.textContent = t("saving");
2883
+ (unchanged ? Promise.resolve({}) : controlRequestJson("/api/mobile-access/remote/frp/configure", {
2884
+ method: "POST",
2885
+ body: JSON.stringify(form)
2886
+ })).then(() => {
2887
+ frpToken.value = "";
2888
+ remoteStatus.textContent = t("frpSavingConnecting");
2889
+ return controlRequestJson("/api/mobile-access/remote/control", {
2890
+ method: "POST",
2891
+ body: JSON.stringify({ running: true })
2892
+ });
2893
+ }).then(renderRemote, (error) => {
2894
+ remoteStatus.textContent = t("configureFailed", { error: String(error) });
2895
+ }).finally(() => {
2896
+ remoteProviderBusy = false;
2897
+ frpConfigure.setAttribute("aria-busy", "false");
2898
+ frpConfigure.textContent = t("frpSaveConnect");
2899
+ loadRemote();
2900
+ });
2901
+ });
2902
+ frpPurge.addEventListener("click", () => {
2903
+ if (remoteProviderBusy || !window.confirm(t("purgeFrpConfirm"))) return;
2904
+ remoteProviderBusy = true;
2905
+ frpPurge.disabled = true;
2906
+ remoteStatus.textContent = t("purgingFrp");
2907
+ controlRequestJson("/api/mobile-access/remote/frp/component/purge", {
2908
+ method: "POST",
2909
+ body: JSON.stringify({ confirm: true })
2910
+ }, LONG_CONTROL_REQUEST_TIMEOUT_MS).then(renderRemote, (error) => {
2911
+ remoteStatus.textContent = t("purgeFailed", { error: String(error) });
2912
+ }).finally(() => {
2913
+ remoteProviderBusy = false;
2914
+ frpPurge.disabled = false;
2915
+ loadRemote();
2916
+ });
2917
+ });
2321
2918
  remoteToggle.addEventListener("click", () => {
2322
2919
  remoteToggle.disabled = true;
2323
2920
  controlRequestJson("/api/mobile-access/remote/control", {
@@ -2336,7 +2933,7 @@ window.__ModuleLoader__.load({
2336
2933
  remoteReconnect.disabled = true;
2337
2934
  remoteSetup.disabled = true;
2338
2935
  remoteSetupRetry.disabled = true;
2339
- remoteStatus.textContent = remoteProvider === "cpolar" ? t("reconnectingCpolar") : t("reconnectingTailscale");
2936
+ remoteStatus.textContent = remoteProvider === "cpolar" ? t("reconnectingCpolar") : remoteProvider === "frp" ? t("reconnectingFrp") : t("reconnectingTailscale");
2340
2937
  controlRequestJson("/api/mobile-access/remote/reconnect", {
2341
2938
  method: "POST",
2342
2939
  body: "{}"
@@ -2428,7 +3025,7 @@ window.__ModuleLoader__.load({
2428
3025
  if (show) loadRemoteDevices();
2429
3026
  });
2430
3027
  remoteReset.addEventListener("click", () => {
2431
- const prompt = remoteProvider === "cpolar" ? t("resetCpolarConfirm") : t("resetTailscaleConfirm");
3028
+ const prompt = remoteProvider === "cpolar" ? t("resetCpolarConfirm") : remoteProvider === "frp" ? t("resetFrpConfirm") : t("resetTailscaleConfirm");
2432
3029
  if (!window.confirm(prompt)) return;
2433
3030
  controlRequestJson("/api/mobile-access/remote/reset", {
2434
3031
  method: "POST",
@@ -2466,7 +3063,7 @@ window.__ModuleLoader__.load({
2466
3063
  if (templates === void 0) return serverCopy;
2467
3064
  const facts = entry.facts !== null && typeof entry.facts === "object" ? entry.facts : {};
2468
3065
  const values = {
2469
- provider: facts.provider === "tailscale" || facts.provider === "cpolar" ? facts.provider : "",
3066
+ provider: facts.provider === "tailscale" || facts.provider === "cpolar" || facts.provider === "frp" ? facts.provider : "",
2470
3067
  latencyMs: typeof facts.latencyMs === "number" && Number.isFinite(facts.latencyMs) ? new Intl.NumberFormat(localeTag).format(facts.latencyMs) : "",
2471
3068
  interfaceName: typeof facts.interfaceName === "string" ? facts.interfaceName : "",
2472
3069
  endpointSuffix: typeof facts.endpointSuffix === "string" ? facts.endpointSuffix : "",
@@ -2502,6 +3099,18 @@ window.__ModuleLoader__.load({
2502
3099
  cpolar_start_timeout: "cpolarTimeout",
2503
3100
  cpolar_stopped: "cpolarStopped",
2504
3101
  cpolar_exited: "cpolarExited",
3102
+ frp_component_missing: "frpMissing",
3103
+ frp_component_invalid: "frpInvalid",
3104
+ frp_config_missing: "frpConfigMissing",
3105
+ frp_config_verify_failed: "frpConfigVerifyFailed",
3106
+ frp_vhost_publicly_reachable: "frpVhostPublic",
3107
+ frp_vhost_probe_failed: "frpVhostProbeFailed",
3108
+ frp_launch_failed: "frpLaunchFailed",
3109
+ frp_start_timeout: "frpTimeout",
3110
+ frp_discovery_mismatch: "frpDiscoveryMismatch",
3111
+ frp_discovery_invalid: "frpDiscoveryInvalid",
3112
+ frp_stopped: "frpStopped",
3113
+ frp_exited: "frpExited",
2505
3114
  gateway_start_failed: "gatewayStartFailed"
2506
3115
  }[values.controllerCode ?? ""];
2507
3116
  if (actionKey !== void 0) action = t(actionKey);
@@ -2629,6 +3238,30 @@ window.__ModuleLoader__.load({
2629
3238
  loadDiagnostics();
2630
3239
  });
2631
3240
  diagnosticsRun.addEventListener("click", loadDiagnostics);
3241
+ updatePlugin.addEventListener("click", () => {
3242
+ if (!pluginUpdateAvailable || pluginLatestVersion === "") return;
3243
+ updatePlugin.disabled = true;
3244
+ updatePlugin.textContent = t("updatingPlugin");
3245
+ releaseNotice.hidden = true;
3246
+ releaseNotice.classList.remove("is-error");
3247
+ controlRequestJson("/api/mobile-access/release/update", {
3248
+ method: "POST",
3249
+ body: "{}"
3250
+ }, LONG_CONTROL_REQUEST_TIMEOUT_MS).then((data) => {
3251
+ const installedVersion = releaseVersion(data.installedVersion) ?? pluginLatestVersion;
3252
+ pluginUpdateAvailable = false;
3253
+ updatePlugin.hidden = true;
3254
+ releaseNotice.textContent = t("pluginUpdatedRestart", { version: installedVersion });
3255
+ releaseNotice.hidden = false;
3256
+ }, (error) => {
3257
+ updatePlugin.textContent = t("updatePlugin");
3258
+ releaseNotice.textContent = t("pluginUpdateFailed", { error: String(error) });
3259
+ releaseNotice.classList.add("is-error");
3260
+ releaseNotice.hidden = false;
3261
+ }).finally(() => {
3262
+ updatePlugin.disabled = false;
3263
+ });
3264
+ });
2632
3265
  diagnosticsCopy.addEventListener("click", () => {
2633
3266
  if (copiedDiagnosticReport === "") return;
2634
3267
  navigator.clipboard.writeText(copiedDiagnosticReport).then(() => {
@@ -2661,6 +3294,7 @@ window.__ModuleLoader__.load({
2661
3294
  if (!panel.contains(event.target) && !document.querySelector(".dsh-mobile-control__trigger")?.contains(event.target)) setOpen(false);
2662
3295
  };
2663
3296
  document.addEventListener("pointerdown", dismiss);
3297
+ controlRequestJson("/api/mobile-access/release").then(renderRelease, () => {});
2664
3298
  controlRequestJson("/api/mobile-access/lan/control").then(render, (error) => {
2665
3299
  status.textContent = t("requestFailed", { error: String(error) });
2666
3300
  });
@@ -2679,7 +3313,8 @@ window.__ModuleLoader__.load({
2679
3313
  },
2680
3314
  toggle: () => {
2681
3315
  setOpen(panel.hidden !== false);
2682
- }
3316
+ },
3317
+ isOpen: () => !panel.hidden
2683
3318
  };
2684
3319
  }
2685
3320
  function mobileRequest(path, init = {}) {
@@ -3844,17 +4479,20 @@ window.__ModuleLoader__.load({
3844
4479
  return false;
3845
4480
  }
3846
4481
  }
4482
+ /** Theme-aware desktop Mobile Access panel styles. */
3847
4483
  const CONTROL_STYLES = `
3848
4484
  .dsh-mobile-control{position:fixed;z-index:1000;left:16px;bottom:112px;font:14px/1.45 system-ui;color:var(--dsw-alias-label-primary,#16181d)}
3849
4485
  .dsh-mobile-control__panel{box-sizing:border-box;width:min(380px,calc(100vw - 32px));max-height:calc(100vh - 140px);overflow-y:auto;padding:16px;border:1px solid var(--dsw-alias-border-subtle,#e1e5eb);border-radius:18px;background:var(--dsw-alias-bg-layer-2,#fff);box-shadow:0 18px 50px rgb(15 23 42 / 18%)}
3850
- .dsh-mobile-control__header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px}.dsh-mobile-control__panel h2{margin:0;font-size:17px;line-height:24px}.dsh-mobile-control__header-actions{display:flex;align-items:center;gap:2px}.dsh-mobile-control__diagnostic-entry,.dsh-mobile-control__close{display:inline-flex;align-items:center;justify-content:center;min-width:44px;height:44px;padding:0;border:0;border-radius:10px;background:transparent;color:inherit;cursor:pointer}.dsh-mobile-control__diagnostic-entry{padding:0 9px;color:#2563eb;font:650 12px/1 system-ui}.dsh-mobile-control__close{font-size:24px;line-height:1}.dsh-mobile-control__diagnostic-entry:hover,.dsh-mobile-control__close:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f3f6)}
3851
- .dsh-mobile-control__app-download{display:flex;align-items:center;justify-content:space-between;box-sizing:border-box;min-height:38px;margin:0 0 10px;padding:8px 11px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:11px;background:var(--dsw-alias-bg-layer-1,#f7f8fa);color:var(--dsw-alias-label-primary,#16181d);font:600 12px/1.3 system-ui;text-decoration:none}.dsh-mobile-control__app-download[hidden]{display:none}.dsh-mobile-control__app-download::after{color:#2563eb;font-size:14px;content:"↗"}.dsh-mobile-control__app-download:hover{border-color:#9fb9e8;background:#f5f8ff;color:#1d4ed8}
4486
+ .dsh-mobile-control__header{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px}.dsh-mobile-control__panel h2{margin:0;font-size:17px;line-height:24px}.dsh-mobile-control__header-actions{display:flex;align-items:center;gap:2px}.dsh-mobile-control__update-plugin,.dsh-mobile-control__diagnostic-entry,.dsh-mobile-control__close{display:inline-flex;align-items:center;justify-content:center;min-width:44px;height:44px;padding:0;border:0;border-radius:10px;background:transparent;color:inherit;cursor:pointer}.dsh-mobile-control__update-plugin,.dsh-mobile-control__diagnostic-entry{padding:0 8px;color:#2563eb;font:650 12px/1 system-ui;white-space:nowrap}.dsh-mobile-control__update-plugin[hidden]{display:none}.dsh-mobile-control__update-plugin:disabled{cursor:wait;opacity:.55}.dsh-mobile-control__close{font-size:24px;line-height:1}.dsh-mobile-control__update-plugin:hover:not(:disabled),.dsh-mobile-control__diagnostic-entry:hover,.dsh-mobile-control__close:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f3f6)}
4487
+ .dsh-mobile-control__release-notice{margin:-2px 0 10px;padding:8px 10px;border-radius:9px;background:var(--dsw-alias-bg-layer-1,#eff6ff);color:var(--dsw-alias-label-primary,#1d4ed8);font-size:11px;line-height:1.45}.dsh-mobile-control__release-notice.is-error{color:#dc2626}.dsh-mobile-control__release-notice[hidden]{display:none}.dsh-mobile-control__app-download{display:flex;align-items:center;justify-content:space-between;box-sizing:border-box;min-height:38px;margin:0 0 10px;padding:8px 11px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:11px;background:var(--dsw-alias-bg-layer-1,#f7f8fa);color:var(--dsw-alias-label-primary,#16181d);font:600 12px/1.3 system-ui;text-decoration:none}.dsh-mobile-control__app-download[hidden]{display:none}.dsh-mobile-control__app-download::after{color:#2563eb;font-size:14px;content:"↗"}.dsh-mobile-control__app-download:hover{border-color:#9fb9e8;background:var(--dsw-alias-interactive-bg-hover-solid,var(--dsw-alias-bg-layer-2,#f5f8ff));color:var(--dsw-alias-label-primary,#1d4ed8)}
3852
4488
  .dsh-mobile-control__switcher{display:grid;grid-template-columns:repeat(2,1fr);gap:4px;margin:0 0 14px;padding:4px;border-radius:12px;background:var(--dsw-alias-bg-layer-1,#f3f5f8)}.dsh-mobile-control__switcher[hidden]{display:none}.dsh-mobile-control__tab{min-height:36px;border:0;border-radius:9px;background:transparent;color:var(--dsw-alias-label-secondary,#606873);font:600 13px/1 system-ui;cursor:pointer}.dsh-mobile-control__tab.is-active{background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);box-shadow:0 1px 3px rgb(15 23 42 / 10%)}.dsh-mobile-control__view[hidden]{display:none}.dsh-mobile-control__intro{margin:0 0 12px;color:var(--dsw-alias-label-secondary,#606873);font-size:12px;line-height:1.55}.dsh-mobile-control__view.is-remote .dsh-mobile-control__actions{display:grid;grid-template-columns:1fr 1fr;gap:8px}.dsh-mobile-control__view.is-remote .dsh-mobile-control__actions button[hidden]{display:none}
3853
- .dsh-mobile-control__provider-section{position:relative;margin:0 0 12px}.dsh-mobile-control__section-title{margin:0 0 8px;color:var(--dsw-alias-label-primary,#16181d);font:650 13px/1.4 system-ui}.dsh-mobile-control__provider-section>.dsh-mobile-control__section-title{padding-right:42px}.dsh-mobile-control__provider-choices{display:grid;gap:8px}.dsh-mobile-control__provider{display:flex;flex-direction:column;gap:5px;min-height:68px;padding:11px 12px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:13px;background:#fff;color:inherit;text-align:left;cursor:pointer;transition:border-color 160ms ease,background-color 160ms ease,box-shadow 160ms ease}.dsh-mobile-control__provider:hover{border-color:#9fb9e8;background:#f8fbff}.dsh-mobile-control__provider.is-selected{border-color:#2563eb;background:#f5f8ff;box-shadow:0 0 0 1px #2563eb inset}.dsh-mobile-control__provider:disabled{cursor:wait;opacity:.62}.dsh-mobile-control__provider-top{display:flex;align-items:center;justify-content:space-between;gap:8px}.dsh-mobile-control__provider-top strong{font-size:13px}.dsh-mobile-control__provider-badge{flex:none;padding:3px 7px;border-radius:999px;background:#e8f0ff;color:#1d4ed8;font:650 10px/1.2 system-ui}.dsh-mobile-control__provider-badge.is-cpolar{background:#eaf8f2;color:#087454}.dsh-mobile-control__provider-description{color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.45}.dsh-mobile-control__provider-info{position:absolute;z-index:5;top:-13px;right:-8px}.dsh-mobile-control__provider-info-button{display:flex;align-items:center;justify-content:center;width:44px;height:44px;padding:0;border:0;border-radius:50%;background:transparent;color:#475569;cursor:pointer;touch-action:manipulation}.dsh-mobile-control__provider-info-button:hover{background:#f1f5f9;color:#1d4ed8}.dsh-mobile-control__provider-info-glyph{display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:18px;height:18px;border:1.5px solid currentColor;border-radius:50%;font:700 12px/1 system-ui}.dsh-mobile-control__provider-info-popover{position:absolute;z-index:6;top:38px;right:4px;box-sizing:border-box;width:min(292px,calc(100vw - 72px));padding:10px 12px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:12px;background:var(--dsw-alias-bg-layer-2,#fff);box-shadow:0 10px 28px rgb(15 23 42 / 16%)}.dsh-mobile-control__provider-info-popover[hidden]{display:none}.dsh-mobile-control__provider-info-popover strong,.dsh-mobile-control__provider-info-popover span{display:block}.dsh-mobile-control__provider-info-popover strong{margin-bottom:3px;font-size:12px}.dsh-mobile-control__provider-info-popover span{color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.55}
3854
- .dsh-mobile-control__cpolar-setup{margin:0 0 12px;padding:12px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:13px;background:#fff}.dsh-mobile-control__cpolar-setup[hidden],.dsh-mobile-control__cpolar-account[hidden],.dsh-mobile-control__details[hidden],.dsh-mobile-control__view.is-remote .dsh-mobile-control__actions[hidden],.dsh-mobile-control__danger[hidden]{display:none}.dsh-mobile-control__component-status,.dsh-mobile-control__component-note{margin:0 0 10px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.55}.dsh-mobile-control__cpolar-setup>.dsh-mobile-control__primary{width:100%;min-height:44px;padding:9px 12px;border-radius:10px;font:600 12px/1.3 system-ui;cursor:pointer}.dsh-mobile-control__cpolar-account{margin-top:10px}.dsh-mobile-control__link-row{display:flex;flex-wrap:wrap;gap:6px 12px;margin:0 0 10px}.dsh-mobile-control__text-link{color:#2563eb;font-size:11px;text-decoration:none}.dsh-mobile-control__text-link:hover{text-decoration:underline}.dsh-mobile-control__token-label{display:flex;flex-direction:column;gap:5px;margin:0 0 8px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px}.dsh-mobile-control__token{box-sizing:border-box;width:100%;min-height:44px;padding:9px 10px;border:1px solid var(--dsw-alias-border-normal,#cfd5dd);border-radius:10px;background:#fff;color:inherit;font:16px/1.4 system-ui}.dsh-mobile-control__cpolar-connect{display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:100%;min-height:44px;padding:10px 14px;border-radius:12px;font:650 13px/1.2 system-ui;cursor:pointer;transition:background-color 160ms ease,border-color 160ms ease,opacity 160ms ease}.dsh-mobile-control__cpolar-connect:hover:not(:disabled){border-color:#1d4ed8;background:#1d4ed8}.dsh-mobile-control__cpolar-connect:active:not(:disabled){border-color:#1e40af;background:#1e40af}.dsh-mobile-control__cpolar-connect:disabled{cursor:wait;opacity:.55}.dsh-mobile-control__details{margin:10px 0 0;border-top:1px solid var(--dsw-alias-border-subtle,#e1e5eb);padding-top:9px}.dsh-mobile-control__details>summary{min-height:30px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:30px;cursor:pointer}.dsh-mobile-control__details-body{display:flex;flex-wrap:wrap;align-items:center;gap:7px 12px;padding:4px 0}.dsh-mobile-control__details-body p{flex:1 0 100%;margin:0;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.5}.dsh-mobile-control__storage{display:block;flex:1 0 100%;max-width:100%;overflow:hidden;padding:7px 8px;border-radius:8px;background:#f3f5f8;color:#475569;font:10px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__danger{flex:1 0 100%;min-height:38px;margin-top:3px;padding:7px 10px;border:1px solid #dc2626;border-radius:9px;background:transparent;color:#b91c1c;font:12px/1.3 system-ui;cursor:pointer}
4489
+ .dsh-mobile-control__provider-section{position:relative;margin:0 0 14px}.dsh-mobile-control__section-title{margin:0 0 8px;color:var(--dsw-alias-label-primary,#16181d);font:650 13px/1.4 system-ui}.dsh-mobile-control__provider-section>.dsh-mobile-control__section-title{padding-right:42px}.dsh-mobile-control__provider-choices{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.dsh-mobile-control__provider{display:flex;min-width:0;flex-direction:column;gap:6px;min-height:94px;padding:10px 11px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:13px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);text-align:left;cursor:pointer;touch-action:manipulation;transition:border-color 160ms ease,background-color 160ms ease,box-shadow 160ms ease}.dsh-mobile-control__provider:hover{border-color:#6f96db;background:var(--dsw-alias-interactive-bg-hover-solid,var(--dsw-alias-bg-layer-1,#f8fbff))}.dsh-mobile-control__provider.is-selected{border-color:#2563eb;background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-bg-layer-1,#f5f8ff));box-shadow:0 0 0 1px #2563eb inset}.dsh-mobile-control__provider:disabled{cursor:wait;opacity:.62}.dsh-mobile-control__provider-top{display:flex;min-width:0;align-items:flex-start;justify-content:space-between;gap:5px}.dsh-mobile-control__provider-top strong{min-width:0;font-size:12px;line-height:1.3}.dsh-mobile-control__provider-badge{flex:none;padding:2px 5px;border-radius:999px;background:#e8f0ff;color:#1d4ed8;font:650 9px/1.25 system-ui}.dsh-mobile-control__provider-badge.is-cpolar{background:#eaf8f2;color:#087454}.dsh-mobile-control__provider-description{color:var(--dsw-alias-label-secondary,#606873);font-size:10px;line-height:1.45}.dsh-mobile-control__provider-info{position:absolute;z-index:5;top:-13px;right:-8px}.dsh-mobile-control__provider-info-button{display:flex;align-items:center;justify-content:center;width:44px;height:44px;padding:0;border:0;border-radius:50%;background:transparent;color:var(--dsw-alias-label-secondary,#475569);cursor:pointer;touch-action:manipulation}.dsh-mobile-control__provider-info-button:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f5f9);color:#2563eb}.dsh-mobile-control__provider-info-glyph{display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:18px;height:18px;border:1.5px solid currentColor;border-radius:50%;font:700 12px/1 system-ui}.dsh-mobile-control__provider-info-popover{position:absolute;z-index:6;top:38px;right:4px;box-sizing:border-box;width:min(292px,calc(100vw - 72px));padding:10px 12px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:12px;background:var(--dsw-alias-bg-layer-2,#fff);box-shadow:0 10px 28px rgb(15 23 42 / 16%)}.dsh-mobile-control__provider-info-popover[hidden]{display:none}.dsh-mobile-control__provider-info-popover strong,.dsh-mobile-control__provider-info-popover span{display:block}.dsh-mobile-control__provider-info-popover strong{margin-bottom:3px;font-size:12px}.dsh-mobile-control__provider-info-popover span{color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.55}
4490
+ .dsh-mobile-control__self-hosted{margin:8px 0 0;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:12px;background:var(--dsw-alias-bg-layer-1,#f8fafc)}.dsh-mobile-control__self-hosted-summary{display:flex;box-sizing:border-box;min-height:48px;align-items:center;justify-content:space-between;gap:10px;padding:8px 11px;cursor:pointer;list-style-position:inside}.dsh-mobile-control__self-hosted-summary>span:first-child{display:flex;min-width:0;flex-direction:column;gap:1px}.dsh-mobile-control__self-hosted-summary strong{font-size:11px}.dsh-mobile-control__self-hosted-summary span span{color:var(--dsw-alias-label-secondary,#606873);font-size:9px;line-height:1.35}.dsh-mobile-control__provider-badge.is-frp{background:#eef0f3;color:#475569}.dsh-mobile-control__self-hosted-body{padding:0 8px 8px}.dsh-mobile-control__provider.is-frp{width:100%;min-height:64px;background:var(--dsw-alias-bg-layer-2,#fff)}
4491
+ .dsh-mobile-control__cpolar-setup{margin:0 0 12px;padding:12px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:13px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__cpolar-setup[hidden],.dsh-mobile-control__cpolar-account[hidden],.dsh-mobile-control__details[hidden],.dsh-mobile-control__view.is-remote .dsh-mobile-control__actions[hidden],.dsh-mobile-control__danger[hidden]{display:none}.dsh-mobile-control__component-status,.dsh-mobile-control__component-note{margin:0 0 10px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.55}.dsh-mobile-control__cpolar-setup>.dsh-mobile-control__primary{width:100%;min-height:44px;padding:9px 12px;border-radius:10px;font:600 12px/1.3 system-ui;cursor:pointer}.dsh-mobile-control__cpolar-account{margin-top:10px}.dsh-mobile-control__link-row{display:flex;flex-wrap:wrap;gap:6px 12px;margin:0 0 10px}.dsh-mobile-control__text-link{color:#2563eb;font-size:11px;text-decoration:none}.dsh-mobile-control__text-link:hover{text-decoration:underline}.dsh-mobile-control__token-label{display:flex;flex-direction:column;gap:5px;margin:0 0 8px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px}.dsh-mobile-control__token{box-sizing:border-box;width:100%;min-height:44px;padding:9px 10px;border:1px solid var(--dsw-alias-border-normal,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2,#fff));color:var(--dsw-alias-label-primary,#16181d);font:16px/1.4 system-ui}.dsh-mobile-control__cpolar-connect{display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:100%;min-height:44px;padding:10px 14px;border-radius:12px;font:650 13px/1.2 system-ui;cursor:pointer;transition:background-color 160ms ease,border-color 160ms ease,opacity 160ms ease}.dsh-mobile-control__cpolar-connect:hover:not(:disabled){border-color:#1d4ed8;background:#1d4ed8}.dsh-mobile-control__cpolar-connect:active:not(:disabled){border-color:#1e40af;background:#1e40af}.dsh-mobile-control__cpolar-connect:disabled{cursor:wait;opacity:.55}.dsh-mobile-control__details{margin:10px 0 0;border-top:1px solid var(--dsw-alias-border-subtle,#e1e5eb);padding-top:9px}.dsh-mobile-control__details>summary{min-height:30px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:30px;cursor:pointer}.dsh-mobile-control__details-body{display:flex;flex-wrap:wrap;align-items:center;gap:7px 12px;padding:4px 0}.dsh-mobile-control__details-body p{flex:1 0 100%;margin:0;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.5}.dsh-mobile-control__storage{display:block;flex:1 0 100%;max-width:100%;overflow:hidden;padding:7px 8px;border-radius:8px;background:var(--dsw-alias-bg-layer-1,#f3f5f8);color:var(--dsw-alias-label-secondary,#475569);font:10px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__danger{flex:1 0 100%;min-height:38px;margin-top:3px;padding:7px 10px;border:1px solid #dc2626;border-radius:9px;background:transparent;color:#dc2626;font:12px/1.3 system-ui;cursor:pointer}
4492
+ .dsh-mobile-control__frp-setup{margin:0;padding:12px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:13px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__frp-setup[hidden]{display:none}.dsh-mobile-control__frp-step{padding:11px 0}.dsh-mobile-control__frp-step + .dsh-mobile-control__frp-step{border-top:1px solid var(--dsw-alias-border-subtle,#e1e5eb)}.dsh-mobile-control__frp-step>strong{display:block;margin-bottom:3px;font-size:12px;line-height:1.4}.dsh-mobile-control__frp-step>p{margin:0 0 9px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.5}.dsh-mobile-control__frp-step>.dsh-mobile-control__frp-requirement{padding:8px 9px;border-radius:9px;background:var(--dsw-alias-bg-layer-1,#f3f5f8);color:var(--dsw-alias-label-primary,#384152);font-size:11px}.dsh-mobile-control__frp-fields{display:grid;grid-template-columns:minmax(0,1fr) 96px;gap:8px}.dsh-mobile-control__field{display:flex;min-width:0;flex-direction:column;gap:5px;color:var(--dsw-alias-label-secondary,#606873);font-size:11px}.dsh-mobile-control__field:nth-child(3),.dsh-mobile-control__field:nth-child(4){grid-column:1/-1}.dsh-mobile-control__field input{box-sizing:border-box;width:100%;min-height:44px;padding:9px 10px;border:1px solid var(--dsw-alias-border-normal,#cfd5dd);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#16181d);font:16px/1.4 system-ui}.dsh-mobile-control__frp-action{box-sizing:border-box;width:100%;min-height:44px;padding:9px 12px;border-radius:10px;font:650 12px/1.3 system-ui;cursor:pointer}.dsh-mobile-control__frp-action:disabled{cursor:not-allowed;opacity:.5}.dsh-mobile-control__remote-workspace{margin:0;padding:12px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:15px;background:var(--dsw-alias-bg-layer-1,#f7f8fa)}.dsh-mobile-control__stage-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:9px}.dsh-mobile-control__stage-header .dsh-mobile-control__section-title{margin:0}.dsh-mobile-control__stage-meta{display:flex;min-width:0;align-items:center;justify-content:flex-end;gap:5px}.dsh-mobile-control__stage-value{max-width:115px;overflow:hidden;color:var(--dsw-alias-label-primary,#16181d);font:650 10px/1.3 system-ui;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__state-badge{flex:none;padding:3px 7px;border-radius:999px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-secondary,#606873);font:650 9px/1.25 system-ui}.dsh-mobile-control__state-badge.is-ready{background:#e6f7f0;color:#087454}.dsh-mobile-control__state-badge.is-busy{background:#e8f0ff;color:#1d4ed8}.dsh-mobile-control__state-badge.is-attention{background:#fff4dc;color:#935100}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__status{box-sizing:border-box;margin:0 0 10px;padding:9px 10px;border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);font-size:11px;line-height:1.45}.dsh-mobile-control__provider-setup-body{margin:0 0 10px}.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__cpolar-setup{margin:0}.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__cpolar-setup>.dsh-mobile-control__section-title,.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__frp-setup>.dsh-mobile-control__section-title{display:none}.dsh-mobile-control__provider-setup-body>.dsh-mobile-control__details{margin:0;padding:9px 10px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:11px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__actions{margin-top:2px}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__qr{margin:10px 0 0}.dsh-mobile-control__remote-workspace>.dsh-mobile-control__manage-row{margin-top:10px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-subtle,#e1e5eb)}
3855
4493
  .dsh-mobile-control__access{display:flex;align-items:baseline;gap:6px;min-width:0;margin:0 0 12px}.dsh-mobile-control__access[hidden]{display:none}.dsh-mobile-control__access-label{flex:none;color:var(--dsw-alias-label-secondary,#606873);white-space:nowrap}.dsh-mobile-control__access-label::after{content:":"}.dsh-mobile-control__access-link{min-width:0;overflow:hidden;color:#2563eb;text-decoration:none;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__access-link:hover{text-decoration:underline}.dsh-mobile-control__qr{display:flex;justify-content:center;margin:0 0 12px}.dsh-mobile-control__qr[hidden]{display:none}.dsh-mobile-control__qr img{border-radius:12px;background:#fff;padding:8px}
3856
4494
  .dsh-mobile-control__status{margin:0 0 14px;overflow-wrap:anywhere;color:var(--dsw-alias-label-secondary,#606873)}.dsh-mobile-control__status::before{display:inline-block;width:8px;height:8px;margin-right:7px;border-radius:50%;background:#98a1ad;content:""}.dsh-mobile-control__status.is-running::before{background:#16a36a}.dsh-mobile-control__status.is-key{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;word-break:break-all}
3857
- .dsh-mobile-control__guide{margin:0 0 14px;padding:12px;border:1px solid #bfdbfe;border-radius:12px;background:#eff6ff}.dsh-mobile-control__guide[hidden]{display:none}.dsh-mobile-control__guide-title{margin:0;color:#172554;font:650 13px/1.45 system-ui}.dsh-mobile-control__guide-summary,.dsh-mobile-control__guide-note{margin:4px 0 0;color:#475569;font-size:12px;line-height:1.5}.dsh-mobile-control__guide-steps{margin:8px 0 0;padding-left:20px;color:#1e293b;font-size:12px;line-height:1.6}.dsh-mobile-control__guide-note{color:#64748b}.dsh-mobile-control__guide-actions{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px}.dsh-mobile-control__guide-actions button{min-width:0;min-height:44px;padding:8px;border-radius:10px;font:12px/1.25 system-ui;cursor:pointer}.dsh-mobile-control__guide-actions button:disabled{cursor:not-allowed;opacity:.45}
4495
+ .dsh-mobile-control__guide{margin:0 0 14px;padding:12px;border:1px solid #6f96db;border-radius:12px;background:var(--dsw-alias-bg-layer-1,#eff6ff)}.dsh-mobile-control__guide[hidden]{display:none}.dsh-mobile-control__guide-title{margin:0;color:var(--dsw-alias-label-primary,#172554);font:650 13px/1.45 system-ui}.dsh-mobile-control__guide-summary,.dsh-mobile-control__guide-note{margin:4px 0 0;color:var(--dsw-alias-label-secondary,#475569);font-size:12px;line-height:1.5}.dsh-mobile-control__guide-steps{margin:8px 0 0;padding-left:20px;color:var(--dsw-alias-label-primary,#1e293b);font-size:12px;line-height:1.6}.dsh-mobile-control__guide-note{color:var(--dsw-alias-label-secondary,#64748b)}.dsh-mobile-control__guide-actions{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px}.dsh-mobile-control__guide-actions button{min-width:0;min-height:44px;padding:8px;border-radius:10px;font:12px/1.25 system-ui;cursor:pointer}.dsh-mobile-control__guide-actions button:disabled{cursor:not-allowed;opacity:.45}
3858
4496
  .dsh-mobile-control__extensions{margin:0 0 12px;color:var(--dsw-alias-label-secondary,#606873);font-size:12px}
3859
4497
  .dsh-mobile-control__view.is-diagnostics{--dsh-diagnostic-ok:#087454;--dsh-diagnostic-warning:#a35b00;--dsh-diagnostic-error:#c62828;--dsh-diagnostic-info:#526071}.dsh-mobile-control__diagnostic-summary{box-sizing:border-box;margin:0;padding:13px;border:1px solid var(--dsw-alias-border-subtle,#dbe1e8);border-radius:16px;background:var(--dsw-alias-bg-layer-1,#f8fafc)}.dsh-mobile-control__diagnostic-summary-main{display:grid;grid-template-columns:36px minmax(0,1fr);align-items:center;gap:11px}.dsh-mobile-control__diagnostic-summary-icon{position:relative;display:block;width:36px;height:36px;border-radius:50%;background:#e8edf3;color:var(--dsh-diagnostic-info)}.dsh-mobile-control__diagnostic-summary-icon::before,.dsh-mobile-control__diagnostic-summary-icon::after{position:absolute;content:""}.dsh-mobile-control__diagnostic-summary-body{display:flex;min-width:0;flex-direction:column;gap:2px}.dsh-mobile-control__diagnostic-summary-body strong{font-size:13px;line-height:1.35}.dsh-mobile-control__diagnostic-summary-body span{color:var(--dsw-alias-label-secondary,#606873);font-size:11px;line-height:1.5}.dsh-mobile-control__diagnostic-summary-meta{display:block;margin-top:11px;padding-top:9px;border-top:1px solid var(--dsw-alias-border-subtle,#dbe1e8);color:var(--dsw-alias-label-secondary,#606873);font-size:10px;line-height:1.45}.dsh-mobile-control__diagnostic-summary.is-ok .dsh-mobile-control__diagnostic-summary-icon{background:#e6f7f0;color:var(--dsh-diagnostic-ok)}.dsh-mobile-control__diagnostic-summary.is-ok .dsh-mobile-control__diagnostic-summary-icon::before{top:10px;left:10px;width:13px;height:7px;border-bottom:2px solid currentColor;border-left:2px solid currentColor;transform:rotate(-45deg)}.dsh-mobile-control__diagnostic-summary.is-attention .dsh-mobile-control__diagnostic-summary-icon{background:#fff4dc;color:var(--dsh-diagnostic-warning)}.dsh-mobile-control__diagnostic-summary.is-error .dsh-mobile-control__diagnostic-summary-icon{background:#fdecec;color:var(--dsh-diagnostic-error)}.dsh-mobile-control__diagnostic-summary.is-attention .dsh-mobile-control__diagnostic-summary-icon::before,.dsh-mobile-control__diagnostic-summary.is-error .dsh-mobile-control__diagnostic-summary-icon::before{top:8px;left:17px;width:2px;height:13px;border-radius:2px;background:currentColor}.dsh-mobile-control__diagnostic-summary.is-attention .dsh-mobile-control__diagnostic-summary-icon::after,.dsh-mobile-control__diagnostic-summary.is-error .dsh-mobile-control__diagnostic-summary-icon::after{bottom:8px;left:17px;width:2px;height:2px;border-radius:50%;background:currentColor}.dsh-mobile-control__diagnostic-summary.is-running .dsh-mobile-control__diagnostic-summary-icon{background:#e8f0ff;color:#2563eb}.dsh-mobile-control__diagnostic-summary.is-running .dsh-mobile-control__diagnostic-summary-icon::before{inset:9px;border:2px solid rgb(37 99 235 / 24%);border-top-color:currentColor;border-radius:50%;animation:dsh-diagnostic-spin .8s linear infinite}
3860
4498
  .dsh-mobile-control__diagnostic-summary.is-idle .dsh-mobile-control__diagnostic-summary-icon::before{top:8px;left:17px;width:2px;height:2px;border-radius:50%;background:currentColor}.dsh-mobile-control__diagnostic-summary.is-idle .dsh-mobile-control__diagnostic-summary-icon::after{top:13px;left:17px;width:2px;height:11px;border-radius:2px;background:currentColor}
@@ -3864,9 +4502,9 @@ window.__ModuleLoader__.load({
3864
4502
  @keyframes dsh-diagnostic-spin{to{transform:rotate(360deg)}}@keyframes dsh-diagnostic-reveal{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}
3865
4503
  .dsh-mobile-control__actions{display:flex;flex-wrap:nowrap;gap:6px}.dsh-mobile-control__actions button{flex:1 1 0;min-width:0;min-height:40px;padding:8px 4px;border-radius:10px;font:12px/1.2 system-ui;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dsh-mobile-control__secondary{border:1px solid var(--dsw-alias-border-normal,#cfd5dd);background:transparent;color:inherit}.dsh-mobile-control__primary{border:1px solid #2563eb;background:#2563eb;color:#fff}.dsh-mobile-control__actions button:disabled{cursor:not-allowed;opacity:.45}
3866
4504
  .dsh-mobile-control button:focus-visible,.dsh-mobile-control a:focus-visible,.dsh-mobile-control input:focus-visible,.dsh-mobile-control summary:focus-visible{outline:3px solid rgb(37 99 235 / 28%);outline-offset:2px}
3867
- .dsh-mobile-control__trigger{box-sizing:border-box;display:flex;align-items:center;gap:8px;width:calc(100% + 8px);height:34px;margin:4px -4px;padding:6px 2px 6px 10px;border:0;border-radius:12px;background:transparent;color:var(--dsw-alias-label-primary,#16181d);font:14px/22px system-ui;cursor:pointer}.dsh-mobile-control__trigger:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f3f6)}.dsh-mobile-control__trigger.is-rail{width:36px;height:36px;margin:8px 0 10px;padding:0;justify-content:center;border-radius:50%}.dsh-mobile-control__trigger-icon{position:relative;box-sizing:border-box;flex:none;width:14px;height:19px;border:1.7px solid currentColor;border-radius:3px}.dsh-mobile-control__trigger-icon::after{position:absolute;right:4px;bottom:2px;width:4px;height:1.5px;border-radius:2px;background:currentColor;content:""}.dsh-mobile-control__trigger-label{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
4505
+ .dsh-mobile-control__trigger{box-sizing:border-box;flex:none;display:flex;align-items:center;gap:8px;width:calc(100% + 4px);height:42px;margin:4px -2px;padding:0 10px 0 8px;border:0;border-radius:12px;background:transparent;color:var(--dsw-alias-label-primary,#16181d);font-family:inherit;font-size:14px;line-height:22px;cursor:pointer;overflow:hidden}.dsh-mobile-control__trigger:hover{background:var(--dsw-alias-interactive-bg-hover,#f1f3f6)}.dsh-mobile-control__trigger:active,.dsh-mobile-control__trigger[aria-expanded="true"]{background:var(--dsw-alias-interactive-bg-active,#e8ebf0)}.dsh-mobile-control__trigger:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,currentColor);outline-offset:2px}.dsh-mobile-control__trigger.is-rail{width:36px;height:36px;margin:8px 0 10px;padding:0;justify-content:center;gap:0;border-radius:50%}.dsh-mobile-control__trigger-icon{display:block;flex:none}.dsh-mobile-control__trigger-label{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
3868
4506
  .dsh-mobile-control__manage-row{display:flex;justify-content:space-between;gap:8px;margin-top:10px}.dsh-mobile-control__manage{flex:1 1 0;min-width:0;min-height:34px;padding:6px 8px;border:1px solid var(--dsw-alias-border-normal,#cfd5dd);border-radius:10px;background:transparent;color:inherit;font:12px/1.3 system-ui;cursor:pointer}.dsh-mobile-control__devices{margin-top:10px;border:1px solid var(--dsw-alias-border-subtle,#e1e5eb);border-radius:10px;padding:8px;max-height:220px;overflow-y:auto}.dsh-mobile-control__device-empty{color:var(--dsw-alias-label-secondary,#606873);font-size:12px;margin:0}.dsh-mobile-control__device{display:flex;align-items:center;gap:8px;padding:6px 2px}.dsh-mobile-control__device + .dsh-mobile-control__device{border-top:1px solid var(--dsw-alias-border-subtle,#e1e5eb)}.dsh-mobile-control__device-label{flex:1 1 0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px}.dsh-mobile-control__device-meta{flex:none;color:var(--dsw-alias-label-secondary,#606873);font-size:11px;white-space:nowrap}.dsh-mobile-control__device-revoke{flex:none;min-height:28px;padding:4px 8px;border:1px solid #dc2626;border-radius:8px;background:transparent;color:#dc2626;font:12px/1.2 system-ui;cursor:pointer}
3869
- @media (prefers-reduced-motion:reduce){.dsh-mobile-control__provider,.dsh-mobile-control__cpolar-connect{transition:none}.dsh-mobile-control__diagnostic-summary.is-running .dsh-mobile-control__diagnostic-summary-icon::before,.dsh-mobile-control__diagnostic-checks{animation:none}}
4507
+ @media (max-width:359px){.dsh-mobile-control__provider-choices{grid-template-columns:1fr}.dsh-mobile-control__provider{min-height:68px}}@media (prefers-reduced-motion:reduce){.dsh-mobile-control__provider,.dsh-mobile-control__cpolar-connect{transition:none}.dsh-mobile-control__diagnostic-summary.is-running .dsh-mobile-control__diagnostic-summary-icon::before,.dsh-mobile-control__diagnostic-checks{animation:none}}
3870
4508
  `;
3871
4509
  /** Mount the desktop control or mobile feature enhancements. */
3872
4510
  function apply(ctx) {
@@ -3897,17 +4535,33 @@ window.__ModuleLoader__.load({
3897
4535
  name: "sidebar.footer.action",
3898
4536
  id: "dsh-mobile"
3899
4537
  }, ({ wide }) => (0, react.createElement)("button", {
3900
- "aria-expanded": false,
4538
+ "aria-expanded": control.isOpen(),
4539
+ "aria-controls": CONTROL_PANEL_ID,
3901
4540
  "aria-label": t("mobileAccess"),
3902
4541
  className: `dsh-mobile-control__trigger${wide ? "" : " is-rail"}`,
3903
4542
  lang: triggerLocale,
3904
4543
  type: "button",
3905
4544
  title: t("mobileAccess"),
3906
4545
  onClick: control.toggle
3907
- }, (0, react.createElement)("span", {
4546
+ }, (0, react.createElement)("svg", {
3908
4547
  "aria-hidden": true,
3909
- className: "dsh-mobile-control__trigger-icon"
3910
- }), wide ? (0, react.createElement)("span", { className: "dsh-mobile-control__trigger-label" }, t("mobileAccess")) : void 0)));
4548
+ className: "dsh-mobile-control__trigger-icon",
4549
+ focusable: false,
4550
+ width: wide ? 16 : 18,
4551
+ height: wide ? 16 : 18,
4552
+ viewBox: "0 0 16 16",
4553
+ fill: "none",
4554
+ stroke: "currentColor",
4555
+ strokeWidth: 1.5,
4556
+ strokeLinecap: "round",
4557
+ strokeLinejoin: "round"
4558
+ }, (0, react.createElement)("rect", {
4559
+ x: 4,
4560
+ y: 1,
4561
+ width: 8,
4562
+ height: 14,
4563
+ rx: 2
4564
+ }), (0, react.createElement)("path", { d: "M7 12h2" })), wide ? (0, react.createElement)("span", { className: "dsh-mobile-control__trigger-label" }, t("mobileAccess")) : void 0)));
3911
4565
  return () => {
3912
4566
  disposeSlot();
3913
4567
  control.remove();
@@ -3922,13 +4576,16 @@ window.__ModuleLoader__.load({
3922
4576
  /** Client services required by the mobile adaptation. */
3923
4577
  const inject = ["slots"];
3924
4578
  //#endregion
4579
+ exports.CONTROL_STYLES = CONTROL_STYLES;
3925
4580
  exports.DIAGNOSTIC_REASON_MESSAGES = DIAGNOSTIC_REASON_MESSAGES;
3926
4581
  exports.MOBILE_CONTROL_MESSAGES = MOBILE_CONTROL_MESSAGES;
3927
4582
  exports.PerIdActivationLifecycle = PerIdActivationLifecycle;
3928
4583
  exports.apply = apply;
3929
4584
  exports.bindClientResponseLifetime = bindClientResponseLifetime;
4585
+ exports.clientReleaseInfo = clientReleaseInfo;
3930
4586
  exports.combineClientSignalLifetime = combineClientSignalLifetime;
3931
4587
  exports.combineClientSignals = combineClientSignals;
4588
+ exports.createFrpServerTemplateForClipboard = createFrpServerTemplateForClipboard;
3932
4589
  exports.diagnosticEntriesForRender = diagnosticEntriesForRender;
3933
4590
  exports.diagnosticOverallForChecks = diagnosticOverallForChecks;
3934
4591
  exports.diagnosticServerCopy = diagnosticServerCopy;