dsh-mobile 0.6.13 → 0.6.15

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.
@@ -20,11 +20,17 @@ Both [#126](https://github.com/saya-ch/dsh-mobile/issues/126) and [#146](https:/
20
20
 
21
21
  Plugin 0.6.11 and later write limited `auth failure` HTTP diagnostics to the plugin log (`$DSH_HOME/mobile-access/logs/dsh-mobile.log`): authorization versus CSRF rejection, status, method, a safe route or route category, related Cookie/header presence and counts, normalized Origin and recognized fetch-site values. It omits tokens, Cookie values, query strings and arbitrary original paths. Each Gateway rate-limits its two categories separately: the first event is immediate and same-category rejections aggregate within a 30-second window; `suppressed` counts previously suppressed records of that category, not all requests or only the current path. This helps investigate missing credentials and CSRF failures; it does not automatically repair a 401/403. The absence of a record does not establish successful authentication: earlier Host/Origin rejections, WebSocket upgrades and native pairing/renewal are outside these two recording points. Review logs before sharing them.
22
22
 
23
- ## HTTP 400, 408 and 431 (in development)
23
+ ### Internal authentication exchange diagnostics (plugin 0.6.15+)
24
24
 
25
- The unpublished plugin correction distinguishes request receive timeouts as `408 request_timeout` and oversized headers as `431 headers_too_large`; other malformed HTTP requests remain `400 bad_request`. Older versions may return 400 with the same short JSON for all of them, so a status and a 24-byte body do not identify the cause.
25
+ `upstream auth` records the Gateway's internal Cookie exchange with local DSH separately from a phone request's `auth failure`. Each entry contains only the Gateway-local exchange `sequence`, `stage`, fixed `outcome`, `durationMs` and the upstream HTTP `status` after response headers arrive; this is not the phone-facing status. `accepted` means the strict 303, Cookie and expiry checks passed. Other outcomes distinguish an invalid target, unexpected status, invalid Cookie, timeout or transport failure. Logs omit the internal launch URL, Tokens, Cookie names and values, response bodies and raw errors.
26
26
 
27
- New `http failure` diagnostics distinguish three per-Gateway categories: `parser` for HTTP parsing/receive failures, `target` for invalid request targets, and `upstream` for an authenticated `POST /api/session/prompt` rejected upstream. The first event of each kind is immediate; repeats aggregate for 30 seconds, with `suppressed` counting earlier suppressed events of that kind. Logs exclude raw errors, request bodies, raw packets, Cookies, Tokens, queries and arbitrary URLs. No record does not establish success.
27
+ Success and failure each have one fixed sampling slot: the first result is immediate, then at most one per 30 seconds, with `suppressed` counting earlier omitted exchanges of that category. Concurrent requests sharing an exchange produce one record; cached Cookie use produces none. Sequences correlate only within one Gateway instance and are not device identifiers. Absence of a record establishes neither authentication nor successful frontend startup, and these logs cannot determine whether Session data was damaged.
28
+
29
+ ## HTTP 400, 408 and 431 (plugin 0.6.13+)
30
+
31
+ Plugin 0.6.13 and later distinguish request receive timeouts as `408 request_timeout` and oversized headers as `431 headers_too_large`; other malformed HTTP requests remain `400 bad_request`. Older versions may return 400 with the same short JSON for all of them, so a status and a 24-byte body do not identify the cause.
32
+
33
+ The `http failure` diagnostics distinguish three per-Gateway categories: `parser` for HTTP parsing/receive failures, `target` for invalid request targets, and `upstream` for an authenticated `POST /api/session/prompt` rejected upstream. The first event of each kind is immediate; repeats aggregate for 30 seconds, with `suppressed` counting earlier suppressed events of that kind. Logs exclude raw errors, request bodies, raw packets, Cookies, Tokens, queries and arbitrary URLs. No record does not establish success.
28
34
 
29
35
  `upstreamTimeoutMs` governs upstream transport waits and also configures the complete inbound-request receive deadline. A continuously active upload can still exceed that total deadline, and Node's periodic checks affect the observed firing time. Do not delete pairings, clear App data or automatically resend because of HTTP 400 alone. Identify the rejection layer using the actual response body, Content-Type, exact versions and redacted proxy upstream status. Check the Session result before retrying an operation whose execution outcome is unknown.
30
36
 
@@ -32,6 +38,10 @@ New `http failure` diagnostics distinguish three per-Gateway categories: `parser
32
38
 
33
39
  In DSH 0.2.0-rc.2, `/plugins/events` is the SSE stream for plugin-module graph and build updates, not conversation-message delivery. Its `200` response, small body or repeated reconnects alone cannot establish successful delivery, failure or a missing processing animation. Inspect the `/api/remote.mux` WebSocket and the actual HTTP request for that operation as well.
34
40
 
41
+ Plugin 0.6.15 and later repair the idle cutoff and gzip buffering for new dedicated Mobile pages supporting the authenticated transport. The page identifies its current authorized view when requesting module events; only a bodyless `GET /plugins/events` with an actual `200 text/event-stream` response uses `upstreamApiTimeoutMs` for later frames. Event graphs use the same layout, dependency and module-selection projection as the initial Mobile page, rather than replacing the running Mobile layout with the raw desktop graph. Newly saved module choices still apply on the next page load. Connection/header deadlines, authentication, revocation, cancellation and concurrency limits remain. An expired view stops notifications rather than falling back to the raw graph or automatically refreshing the Session. Old pages and `frontend=stock` do not enable this projection; reload the dedicated page after upgrading to use it. This repair does not establish the cause of every startup or reload symptom in [#243](https://github.com/saya-ch/dsh-mobile/issues/243).
42
+
43
+ A page view does not migrate to a new authentication Session. Native renewal may retain the WebView and draft without restoring the old module-event stream. A new Session using the old view receives `409 mobile_document_view_invalid`; loading the dedicated page again obtains a new view. This does not require removing a pairing or automatically refresh the current Session.
44
+
35
45
  An ordinary message bubble and cleared draft can precede `POST /api/session/prompt`: they are an optimistic local echo, not Host acceptance. If no corresponding request appears, retain redacted request timing, status codes and DSH logs to distinguish a request never started by the browser, proxy rejection and Host acceptance. The plugin does not automatically replay messages, approvals or other writes; check the Session before retrying an operation with an unknown outcome.
36
46
 
37
47
  ## Slow transfers can exceed the default deadlines
@@ -106,7 +116,7 @@ Resources without upstream validators do not automatically acquire a reliable on
106
116
 
107
117
  Commands such as `/compact` sent from the phone traverse the Mobile gateway's HTTP API. Automatic compaction inside DSH does not traverse that gateway request. Older gateways used the default 30-second `upstreamTimeoutMs` for every proxied request, so manual compaction could return `502 upstream_unavailable` while the model was still producing the summary. [#171](https://github.com/saya-ch/dsh-mobile/issues/171) records this case.
108
118
 
109
- The gateway now separates transport waits from authenticated API response waits. `upstreamTimeoutMs` still defaults to 30000 milliseconds (1000–300000) for uploads, static assets, boot resources, and WebSocket handshakes; incoming request headers retain their existing 10-second deadline. Only `/api` and `/api/…` HTTP requests that pass device authentication, Host/Origin checks, and any required CSRF check switch to `upstreamApiTimeoutMs` after the request body is sent to DSH. The new field defaults to `0`, disabling the API response idle timeout. To set a limit, use an integer from `1` through `2147483647` milliseconds; the upper limit is Node's timer representation limit. This is not a total execution deadline: arriving response data resets the idle timer.
119
+ The gateway separates transport waits from authenticated API response waits. `upstreamTimeoutMs` still defaults to 30000 milliseconds (1000–300000) for uploads, static assets, boot resources, and WebSocket handshakes; incoming request headers retain their existing 10-second deadline. `/api` and `/api/…` HTTP requests that pass device authentication, Host/Origin checks, and any required CSRF check switch to `upstreamApiTimeoutMs` after the request body is sent to DSH. The field defaults to `0`, disabling these response idle timeouts. To set a limit, use an integer from `1` through `2147483647` milliseconds; the upper limit is Node's timer representation limit. Plugin 0.6.15 and later also apply it to dedicated module-event streams after valid response headers, under the event-stream conditions above. This is not a total execution deadline: arriving response data resets the idle timer.
110
120
 
111
121
  Add the property below to the existing `mobile-access` entry's `config` and keep all other settings. This is a configuration fragment, not a complete profile patch. `0` is the default; this example sets a 10-minute API response idle timeout:
112
122
 
@@ -20,11 +20,17 @@
20
20
 
21
21
  插件 0.6.11 起会在插件日志(`$DSH_HOME/mobile-access/logs/dsh-mobile.log`)记录有限的 `auth failure` HTTP 诊断:区分授权与 CSRF 拒绝,包含状态、方法、安全路径或路径类别、相关 Cookie/请求头是否存在及计数、规范化 Origin 和受支持的 fetch-site 值,不记录令牌、Cookie 值、查询参数或任意原始路径。每个网关的两类诊断分别限频,首条立即记录,30 秒窗口内同类拒绝聚合;`suppressed` 表示此前被抑制的同类记录数量,不是全部请求数,也不限定于当前路径。它帮助排查凭据未携带或 CSRF 校验失败,不会自动修复 401/403。没有日志也不代表认证成功:更早的 Host/Origin 拒绝、WebSocket 升级和原生配对/续期不在这两处记录范围内。分享前仍应检查日志。
22
22
 
23
- ## HTTP 400、408 与 431 的区别(开发中)
23
+ ### 内部认证交换诊断(插件 0.6.15 起)
24
24
 
25
- 尚未发布的插件修正会把请求接收超时区分为 `408 request_timeout`,请求头过大区分为 `431 headers_too_large`;其他非法 HTTP 请求仍为 `400 bad_request`。旧版可能把这几类情况都返回成 400 和相同的短 JSON,因此状态码与“24 字节”不足以定位原因。
25
+ `upstream auth` 单独记录网关向本机 DSH 交换内部认证 Cookie 的结果,不是手机请求的 `auth failure`。每条只包含该网关内的交换序号 `sequence`、阶段 `stage`、固定结果 `outcome`、耗时 `durationMs`,以及收到响应头后才有的上游 HTTP `status`;这个状态不是手机最终收到的状态。`accepted` 表示严格通过 303、Cookie 和有效期检查;其他结果区分目标配置无效、状态不符、Cookie 无效、超时或传输失败。不记录内部启动地址、Token、Cookie 名称或值、响应正文及原始错误。
26
26
 
27
- 新的 `http failure` 日志按网关区分 `parser`、`target`、`upstream` 三类:分别表示 HTTP 解析/接收失败、非法请求目标、已认证的 `POST /api/session/prompt` 被上游拒绝。每类首条立即记录,后续 30 秒内聚合,下一条的 `suppressed` 是此前被抑制的同类数量。日志不保留原始报错、请求正文、原始包、Cookie、Token、查询参数或任意 URL;不记录某条也不能证明它成功。
27
+ 成功与失败分别最多每 30 秒记录一条,首条立即记录,`suppressed` 计入此前同类被抑制的交换。并发请求共用一次交换时只记录一次,使用缓存 Cookie 时不新增记录;序号仅在同一个网关实例内关联,不是设备标识。没有记录不能证明该次请求认证或页面启动成功,也不能用这类日志判断会话数据是否损坏。
28
+
29
+ ## HTTP 400、408 与 431 的区别(插件 0.6.13 起)
30
+
31
+ 插件 0.6.13 起把请求接收超时区分为 `408 request_timeout`,请求头过大区分为 `431 headers_too_large`;其他非法 HTTP 请求仍为 `400 bad_request`。旧版可能把这几类情况都返回成 400 和相同的短 JSON,因此状态码与“24 字节”不足以定位原因。
32
+
33
+ `http failure` 日志按网关区分 `parser`、`target`、`upstream` 三类:分别表示 HTTP 解析/接收失败、非法请求目标、已认证的 `POST /api/session/prompt` 被上游拒绝。每类首条立即记录,后续 30 秒内聚合,下一条的 `suppressed` 是此前被抑制的同类数量。日志不保留原始报错、请求正文、原始包、Cookie、Token、查询参数或任意 URL;不记录某条也不能证明它成功。
28
34
 
29
35
  `upstreamTimeoutMs` 既用于上游传输等待,也配置了入站完整请求的接收期限;持续上传不保证避开这个总期限,Node 的定期检查会影响实际触发时刻。不要仅凭 HTTP 400 就删除配对、清空 App 数据或自动重发。先区分拒绝来源,并查看实际响应正文、Content-Type、准确版本和脱敏的代理上游状态;消息或工具是否已执行不明时,先核对会话结果。
30
36
 
@@ -32,6 +38,10 @@
32
38
 
33
39
  在 DSH 0.2.0-rc.2 中,`/plugins/events` 是插件模块图和构建更新的 SSE,不是对话消息投递通道。它返回 `200`、传输很少字节或反复重连,不能单独证明消息发送成功、失败或处理中动画丢失;同时检查 `/api/remote.mux` WebSocket 与那次操作实际发出的 HTTP 请求。
34
40
 
41
+ 插件 0.6.15 起为支持认证传输的新专用移动页面修复该通知流的静默超时和 gzip 缓冲:页面以绑定当前授权的标识请求模块事件,网关只在无请求体的 `GET /plugins/events` 收到 `200 text/event-stream` 后按 `upstreamApiTimeoutMs` 等待后续帧。通知中的模块图使用与初始移动页面相同的布局、依赖与模块选择投影,不能用桌面原始模块图替换正在运行的移动布局;保存的新选择仍在下一次加载页面时应用。建连和首响应期限、鉴权、撤销、取消及并发限制不变,标识失效时终止通知,不退回原始模块图或自动刷新会话。旧页面与 `frontend=stock` 不启用这套投影;升级后重新加载专用页面才能使用新通知流。这项修复不代表 [#243](https://github.com/saya-ch/dsh-mobile/issues/243) 的所有启动和重载现象已定位。
42
+
43
+ 页面标识不会迁移到新的认证 Session:原生续期即使保留了当前 WebView 和草稿,也不代表旧模块通知流已恢复。新 Session 使用旧标识会收到 `409 mobile_document_view_invalid`;重新加载专用页面会取得新标识。这个限制不要求删除配对,也不会主动刷新当前会话。
44
+
35
45
  普通消息的气泡和清空草稿可能先于 `POST /api/session/prompt`,属于本地乐观回显,不是 Host 受理确认。若找不到对应请求,请保留脱敏后的请求时序、状态码及 DSH 日志,区分浏览器未发起、代理拒绝与 Host 已受理。插件不会自动重放消息、审批或其他写请求;结果不明时先确认会话,避免重复副作用。
36
46
 
37
47
  ## 慢速传输可能超过默认等待时间
@@ -106,7 +116,7 @@ websocketCompression:
106
116
 
107
117
  手机发出的 `/compact` 等命令要通过移动网关的 HTTP API;DSH 内部触发的自动压缩不经过这次网关请求。旧版网关对所有上游代理请求都使用默认 30 秒的 `upstreamTimeoutMs`,因此模型仍在生成摘要时,手动压缩可能先收到 `502 upstream_unavailable`。[#171](https://github.com/saya-ch/dsh-mobile/issues/171) 记录了这一情况。
108
118
 
109
- 当前网关区分传输等待和已鉴权 API 的响应等待。`upstreamTimeoutMs` 仍默认 30000 毫秒(1000–300000),用于上传、静态资源、启动资源和 WebSocket 握手的等待;请求头另有既有的 10 秒接收期限。只有完成设备鉴权、Host/Origin 与必要 CSRF 检查的 `/api` 和 `/api/…` HTTP 请求,在请求体发往 DSH 后切换到 `upstreamApiTimeoutMs`。新字段默认 `0`,表示不对 API 响应施加空闲超时;需要限制时,可配置为 `1–2147483647` 的整数毫秒,范围上限来自 Node 定时器的表示范围。这不是整个操作的总执行期限:持续到达的响应数据会重置空闲计时。
119
+ 当前网关区分传输等待和已鉴权 API 的响应等待。`upstreamTimeoutMs` 仍默认 30000 毫秒(1000–300000),用于上传、静态资源、启动资源和 WebSocket 握手的等待;请求头另有既有的 10 秒接收期限。完成设备鉴权、Host/Origin 与必要 CSRF 检查的 `/api` 和 `/api/…` HTTP 请求,在请求体发往 DSH 后切换到 `upstreamApiTimeoutMs`。该字段默认 `0`,表示不对这些响应施加空闲超时;需要限制时,可配置为 `1–2147483647` 的整数毫秒,范围上限来自 Node 定时器的表示范围。插件 0.6.15 起,专用模块事件流在收到有效响应头后也使用此字段,适用条件见上面的通知流说明。这不是整个操作的总执行期限:持续到达的响应数据会重置空闲计时。
110
120
 
111
121
  以下属性放进现有 `mobile-access` 条目的 `config`,并保留其他配置;这是配置片段,不是完整的 profile 补丁。`0` 是默认值,示例为 API 响应设置 10 分钟的空闲等待期限:
112
122
 
package/lib/client.js CHANGED
@@ -1219,10 +1219,10 @@ window.__ModuleLoader__.load({
1219
1219
  originDescription: "Use managed Caddy or your existing HTTPS reverse proxy with DSH Mobile’s private, authenticated backend.",
1220
1220
  prepareOrigin: "Connect your HTTPS reverse proxy",
1221
1221
  originPublicOrigin: "Public HTTPS address",
1222
- originListenHost: "Private listen IPv4",
1222
+ originListenHost: "Private listen IP (IPv4 / IPv6)",
1223
1223
  originListenPort: "HTTP backend port",
1224
1224
  originAllowedCidrs: "Allowed proxy source CIDRs",
1225
- originHelp: "Same computer: use 127.0.0.1. For a proxy on another LAN device, use this computer’s private IPv4 and allow only the proxy’s direct source IPv4/32. Separate CIDRs with spaces or commas.",
1225
+ originHelp: "Same computer: use 127.0.0.1 or ::1. Across a private network, use this computer’s private IPv4 or IPv6 ULA and allow the proxy’s direct source with /32 or /128 in the same address family. This is the NGINX-to-DSH connection, not the phone’s public IP. Separate CIDRs with spaces or commas.",
1226
1226
  originWarning: "Keep this HTTP backend private. Never expose or port-forward it to the Internet. Your proxy must terminate HTTPS and preserve the external Host, including its port, and WebSocket upgrades.",
1227
1227
  originSaveStart: "Save and start backend",
1228
1228
  originBackendSaved: "Saved HTTP backend (not running)",
@@ -1257,15 +1257,15 @@ window.__ModuleLoader__.load({
1257
1257
  reconnectingOrigin: "Restarting the private HTTP backend…",
1258
1258
  resetOriginConfirm: "Stop this backend and reset remote pairing? All paired remote devices must pair again. Reverse-proxy settings and LAN devices are kept.",
1259
1259
  originPublicOriginInvalid: "Enter a public HTTPS origin only, such as https://phone.example.com:8815; no path, query, credentials, or private address.",
1260
- originListenHostInvalid: "Use an explicit loopback or private IPv4 address on this computer, not 0.0.0.0 or a public address.",
1260
+ originListenHostInvalid: "Use this computer’s explicit loopback/private IPv4, ::1, or IPv6 ULA (fc00::/7). Wildcard and public addresses are not allowed.",
1261
1261
  originListenPortInvalid: "Use a whole-number port from 1024 to 65535; lower ports need privileges the plugin does not have.",
1262
1262
  originListenPortReserved: "Port 3443 belongs to the LAN gateway and port 3080 to DSH itself, so both are always taken. Choose another port, such as 3444 or 3445.",
1263
- originAllowedCidrsInvalid: "Allow 1–16 valid loopback/private IPv4 CIDRs with no host bits. For a LAN listener, include the actual proxy source IPv4/32; public ranges are not accepted.",
1263
+ originAllowedCidrsInvalid: "Allow 1–16 canonical loopback/private CIDRs in the listener’s address family. For a private-network listener, include a non-loopback proxy source, preferably IPv4/32 or IPv6/128. IPv6 supports ::1/128 and ULA (fc00::/7); public ranges are not accepted.",
1264
1264
  originSettingsInvalid: "The reverse-proxy settings are invalid. Check the four fields and save again.",
1265
1265
  originConfigMissing: "Save the reverse-proxy settings before starting the backend.",
1266
1266
  originConfigInvalid: "The saved settings are invalid. Correct and save them again, or clear only these settings.",
1267
1267
  originPortInUse: "The backend port is already in use. Stop the conflicting listener or choose another port. Note that 3443, 3080, and a port already used by a cloudflared tunnel are never free.",
1268
- originAddressUnavailable: "The listen address is not available on this computer. Choose its current private IPv4.",
1268
+ originAddressUnavailable: "The listen address is not available on this computer. Choose its current private IPv4, ::1, or IPv6 ULA.",
1269
1269
  originStartFailed: "The private backend could not start. Check its address, port, permissions, and local logs.",
1270
1270
  originConfigTargetInvalid: "The saved proxy settings file is not a regular file. Remove it and save the settings again.",
1271
1271
  originControllerUnavailable: "The reverse-proxy provider is not available in this session. Restart DSH and try again."
@@ -1278,10 +1278,10 @@ window.__ModuleLoader__.load({
1278
1278
  originDescription: "Usa Caddy gestito o il tuo reverse proxy HTTPS con il backend privato autenticato di DSH Mobile.",
1279
1279
  prepareOrigin: "Collega il tuo reverse proxy HTTPS",
1280
1280
  originPublicOrigin: "Indirizzo HTTPS pubblico",
1281
- originListenHost: "IPv4 privato di ascolto",
1281
+ originListenHost: "IP privato di ascolto (IPv4 / IPv6)",
1282
1282
  originListenPort: "Porta backend HTTP",
1283
1283
  originAllowedCidrs: "CIDR sorgente del proxy consentiti",
1284
- originHelp: "Stesso computer: usa 127.0.0.1. Per un proxy su un altro dispositivo LAN, usa l’IPv4 privato di questo computer e consenti solo l’IPv4 sorgente diretta del proxy con /32. Separa i CIDR con spazi o virgole.",
1284
+ originHelp: "Stesso computer: usa 127.0.0.1 o ::1. Su una rete privata usa l’IPv4 privato o l’IPv6 ULA di questo computer e consenti la sorgente diretta del proxy con /32 o /128 nella stessa famiglia. Si tratta della connessione NGINX–DSH, non dell’IP pubblico del telefono. Separa i CIDR con spazi o virgole.",
1285
1285
  originWarning: "Mantieni privato il backend HTTP. Non esporlo né inoltrarne la porta su Internet. Il proxy deve terminare HTTPS e conservare l’Host esterno, inclusa la porta, e gli upgrade WebSocket.",
1286
1286
  originSaveStart: "Salva e avvia backend",
1287
1287
  originBackendSaved: "Backend HTTP salvato (non attivo)",
@@ -1316,15 +1316,15 @@ window.__ModuleLoader__.load({
1316
1316
  reconnectingOrigin: "Riavvio del backend HTTP privato…",
1317
1317
  resetOriginConfirm: "Fermare il backend e reimpostare gli abbinamenti remoti? Tutti i dispositivi remoti dovranno essere abbinati di nuovo. Le impostazioni proxy e i dispositivi LAN vengono conservati.",
1318
1318
  originPublicOriginInvalid: "Inserisci solo un’origine HTTPS pubblica, ad esempio https://phone.example.com:8815, senza percorso, query, credenziali o indirizzo privato.",
1319
- originListenHostInvalid: "Usa un IPv4 loopback o privato esplicito di questo computer, non 0.0.0.0 né un indirizzo pubblico.",
1319
+ originListenHostInvalid: "Usa l’IPv4 loopback/privato, ::1 o un IPv6 ULA (fc00::/7) esplicito di questo computer. Indirizzi jolly e pubblici non sono ammessi.",
1320
1320
  originListenPortInvalid: "Usa una porta intera da 1024 a 65535; le porte più basse richiedono privilegi che il plugin non ha.",
1321
1321
  originListenPortReserved: "La porta 3443 appartiene al gateway LAN e la 3080 a DSH stesso, quindi sono sempre occupate. Scegli un’altra porta, ad esempio 3444 o 3445.",
1322
- originAllowedCidrsInvalid: "Consenti 1–16 CIDR IPv4 loopback/privati validi senza bit host. Per l’ascolto LAN includi l’IPv4 sorgente reale del proxy con /32; le reti pubbliche non sono ammesse.",
1322
+ originAllowedCidrsInvalid: "Consenti 1–16 CIDR loopback/privati canonici della stessa famiglia dell’ascolto. Su rete privata includi una sorgente proxy non loopback, preferibilmente IPv4/32 o IPv6/128. IPv6 supporta ::1/128 e ULA (fc00::/7); le reti pubbliche non sono ammesse.",
1323
1323
  originSettingsInvalid: "Impostazioni reverse proxy non valide. Controlla i quattro campi e salva di nuovo.",
1324
1324
  originConfigMissing: "Salva le impostazioni reverse proxy prima di avviare il backend.",
1325
1325
  originConfigInvalid: "Impostazioni salvate non valide. Correggile e salvale di nuovo, oppure cancella solo queste impostazioni.",
1326
1326
  originPortInUse: "La porta backend è già occupata. Ferma il processo in conflitto o scegli un’altra porta. 3443, 3080 e una porta già usata da un tunnel cloudflared non sono mai libere.",
1327
- originAddressUnavailable: "L’indirizzo di ascolto non è disponibile su questo computer. Scegli il suo IPv4 privato attuale.",
1327
+ originAddressUnavailable: "L’indirizzo di ascolto non è disponibile su questo computer. Scegli il suo IPv4 privato attuale, ::1 o un IPv6 ULA.",
1328
1328
  originStartFailed: "Impossibile avviare il backend privato. Controlla indirizzo, porta, permessi e log locali.",
1329
1329
  originConfigTargetInvalid: "Il file delle impostazioni proxy salvato non è un file regolare. Rimuovilo e salva di nuovo le impostazioni.",
1330
1330
  originControllerUnavailable: "Il provider reverse proxy non è disponibile in questa sessione. Riavvia DSH e riprova."
@@ -1337,10 +1337,10 @@ window.__ModuleLoader__.load({
1337
1337
  originDescription: "使用托管 Caddy 或已有 HTTPS 反向代理,接入 DSH Mobile 带认证的私有后端。",
1338
1338
  prepareOrigin: "接入自有 HTTPS 反向代理",
1339
1339
  originPublicOrigin: "公网 HTTPS 地址",
1340
- originListenHost: "私有监听 IPv4",
1340
+ originListenHost: "私有监听 IP(IPv4 / IPv6)",
1341
1341
  originListenPort: "HTTP 后端端口",
1342
1342
  originAllowedCidrs: "允许的代理来源 CIDR",
1343
- originHelp: "代理与 DSH 同机时用 127.0.0.1;代理在另一台局域网设备上时,填写本机私有 IPv4,并只允许代理实际直连来源 IPv4/32。多个 CIDR 用空格或逗号分隔。",
1343
+ originHelp: "同机代理用 127.0.0.1 或 ::1;跨私网代理填写本机私有 IPv4 或 IPv6 ULA,并允许同一地址族的代理直连来源 /32 或 /128。这里检查 NGINX 到 DSH 的连接,不是手机的公网 IP。多个 CIDR 用空格或逗号分隔。",
1344
1344
  originWarning: "HTTP 后端必须留在私网,绝不能直接暴露公网或做公网端口映射。反向代理负责 HTTPS,并须保留外部 Host(含端口)及 WebSocket 升级。",
1345
1345
  originSaveStart: "保存并启动后端",
1346
1346
  originBackendSaved: "已保存的 HTTP 后端(未运行)",
@@ -1375,15 +1375,15 @@ window.__ModuleLoader__.load({
1375
1375
  reconnectingOrigin: "正在重启私有 HTTP 后端…",
1376
1376
  resetOriginConfirm: "停止此后端并重置远程配对?所有已配对的远程设备都需要重新配对;反向代理配置和局域网设备保留。",
1377
1377
  originPublicOriginInvalid: "只填写公网 HTTPS 源地址,例如 https://phone.example.com:8815;不能含路径、查询参数、账号密码或私有地址。",
1378
- originListenHostInvalid: "填写本机明确的回环或私有 IPv4,不能使用 0.0.0.0 或公网地址。",
1378
+ originListenHostInvalid: "填写本机明确的回环/私有 IPv4、::1 或 IPv6 ULA(fc00::/7),不能使用全接口监听或公网地址。",
1379
1379
  originListenPortInvalid: "端口必须是 1024–65535 的整数;更低的端口需要插件没有的特权。",
1380
1380
  originListenPortReserved: "3443 属于局域网网关、3080 属于 DSH 自身,两者始终被占用。请换一个端口,例如 3444 或 3445。",
1381
- originAllowedCidrsInvalid: "填写 1–16 个规范的回环/私有 IPv4 CIDR,不能有主机位;局域网监听时须包含代理实际来源 IPv4/32,不接受公网网段。",
1381
+ originAllowedCidrsInvalid: "填写 1–16 个与监听地址同族的规范回环/私有 CIDR;私网监听须包含非回环代理来源,推荐 IPv4/32 或 IPv6/128。IPv6 支持 ::1/128 和 ULA(fc00::/7),不接受公网网段。",
1382
1382
  originSettingsInvalid: "反向代理配置无效,请检查四个字段后重新保存。",
1383
1383
  originConfigMissing: "请先保存反向代理配置,再启动后端。",
1384
1384
  originConfigInvalid: "已保存的配置无效,请修正并重新保存,或仅清除这项配置。",
1385
1385
  originPortInUse: "后端端口已被占用,请停止冲突进程或改用其他端口。3443、3080 以及已被 cloudflared 隧道占用的端口永远不会空闲。",
1386
- originAddressUnavailable: "本机没有这个监听地址,请选择当前本机的私有 IPv4。",
1386
+ originAddressUnavailable: "本机没有这个监听地址,请选择当前本机的私有 IPv4、::1 或 IPv6 ULA。",
1387
1387
  originStartFailed: "私有后端启动失败,请检查地址、端口、权限和本地日志。",
1388
1388
  originConfigTargetInvalid: "已保存的代理配置文件不是普通文件。请移除后重新保存设置。",
1389
1389
  originControllerUnavailable: "当前会话里反向代理提供方不可用。请重启 DSH 后重试。"
@@ -3861,7 +3861,7 @@ window.__ModuleLoader__.load({
3861
3861
  }
3862
3862
  /**
3863
3863
  * Maximum gap between input or scrolling events before an unused loading intent expires.
3864
- * A held finger may re-arm on its next owned move; an undelivered touch end cannot keep the lease alive.
3864
+ * A held pending intent expires without loading; a fresh gesture can create another intent.
3865
3865
  */
3866
3866
  const HISTORY_SCROLL_QUIET_MS = 1600;
3867
3867
  /**
@@ -3908,9 +3908,21 @@ window.__ModuleLoader__.load({
3908
3908
  * that compensation as another upward fling, clicked "Load earlier" again, and shook
3909
3909
  * the view in a chain. Only fresh input arms the gate, one arm buys at most one
3910
3910
  * successful load, and an arm with no further input expires quietly.
3911
+ *
3912
+ * A touch-driven eligible scroll never clicks while the gesture is live: DSH retains
3913
+ * a paging anchor while a page loads, and its preservation writes fight a drag still
3914
+ * in progress. The gate records a pending intent instead and spends it only once the
3915
+ * gesture settles — a `scrollend` after every owned contact ends, or the quiet lease
3916
+ * expiring with no contact listed — and only while the reader still holds the top
3917
+ * zone. The intent dies with its arm: it is never spendable after the arm ends, and
3918
+ * the lease re-anchors ownership instead of clicking under a listed finger. Wheel
3919
+ * input outside a touch gesture keeps the immediate click: a wheel flick has no held
3920
+ * gesture to fight.
3911
3921
  */
3912
3922
  function createHistoryAutoLoadGate(hooks) {
3913
3923
  let armed = false;
3924
+ let armedByWheel = false;
3925
+ let pendingIntent = false;
3914
3926
  const touchIds = /* @__PURE__ */ new Set();
3915
3927
  let disposed = false;
3916
3928
  let previousTop = hooks.getTop();
@@ -3922,10 +3934,32 @@ window.__ModuleLoader__.load({
3922
3934
  hooks.cancel(quietId);
3923
3935
  quietId = void 0;
3924
3936
  };
3937
+ /**
3938
+ * Spend a pending loading intent only after every owned contact ends in the top zone.
3939
+ * @returns whether a load was issued.
3940
+ */
3941
+ const settle = () => {
3942
+ if (!pendingIntent || touchIds.size > 0) return false;
3943
+ if (hooks.getTop() > AUTO_HISTORY_THRESHOLD_PX) {
3944
+ pendingIntent = false;
3945
+ return false;
3946
+ }
3947
+ if (!hooks.tryLoad()) return false;
3948
+ pendingIntent = false;
3949
+ return true;
3950
+ };
3925
3951
  const onQuiet = () => {
3926
3952
  quietPending = false;
3927
3953
  quietId = void 0;
3954
+ if (touchIds.size > 0) {
3955
+ if (pendingIntent) touchIds.clear();
3956
+ pendingIntent = false;
3957
+ armed = false;
3958
+ return;
3959
+ }
3960
+ settle();
3928
3961
  armed = false;
3962
+ pendingIntent = false;
3929
3963
  };
3930
3964
  const restartQuiet = () => {
3931
3965
  cancelQuiet();
@@ -3933,18 +3967,24 @@ window.__ModuleLoader__.load({
3933
3967
  quietPending = true;
3934
3968
  quietId = hooks.schedule(onQuiet, HISTORY_SCROLL_QUIET_MS);
3935
3969
  };
3936
- const arm = () => {
3970
+ const arm = (byWheel) => {
3937
3971
  armed = true;
3972
+ armedByWheel = byWheel;
3938
3973
  restartQuiet();
3939
3974
  };
3940
3975
  const onScroll = () => {
3941
3976
  const top = hooks.getTop();
3942
3977
  const eligible = armed && hooks.shouldLoad(previousTop, top);
3943
3978
  previousTop = top;
3944
- if (eligible && hooks.tryLoad()) {
3945
- armed = false;
3946
- cancelQuiet();
3947
- return;
3979
+ if (eligible) {
3980
+ if (armedByWheel) {
3981
+ if (hooks.tryLoad()) {
3982
+ armed = false;
3983
+ pendingIntent = false;
3984
+ cancelQuiet();
3985
+ return;
3986
+ }
3987
+ } else pendingIntent = true;
3948
3988
  }
3949
3989
  if (armed) restartQuiet();
3950
3990
  };
@@ -3952,15 +3992,20 @@ window.__ModuleLoader__.load({
3952
3992
  handleEvent: (event) => {
3953
3993
  if (disposed) return;
3954
3994
  switch (event.type) {
3955
- case "touchstart":
3956
- for (const touch of Array.from(event.changedTouches ?? [])) touchIds.add(touch.identifier);
3957
- if ((event.changedTouches?.length ?? 0) > 0) arm();
3995
+ case "touchstart": {
3996
+ const changed = Array.from(event.changedTouches ?? []);
3997
+ if (changed.length === 0) break;
3998
+ if (touchIds.size === 0) pendingIntent = false;
3999
+ for (const touch of changed) touchIds.add(touch.identifier);
4000
+ arm(false);
3958
4001
  break;
4002
+ }
3959
4003
  case "touchmove":
3960
- if (Array.from(event.changedTouches ?? []).some((touch) => touchIds.has(touch.identifier))) arm();
4004
+ if (Array.from(event.changedTouches ?? []).some((touch) => touchIds.has(touch.identifier))) arm(false);
3961
4005
  break;
3962
4006
  case "wheel":
3963
- arm();
4007
+ if (touchIds.size === 0) pendingIntent = false;
4008
+ arm(touchIds.size === 0);
3964
4009
  break;
3965
4010
  case "touchend":
3966
4011
  case "touchcancel": {
@@ -3970,6 +4015,15 @@ window.__ModuleLoader__.load({
3970
4015
  break;
3971
4016
  }
3972
4017
  case "scrollend":
4018
+ if (pendingIntent && settle()) {
4019
+ armed = false;
4020
+ cancelQuiet();
4021
+ break;
4022
+ }
4023
+ if (pendingIntent) {
4024
+ restartQuiet();
4025
+ break;
4026
+ }
3973
4027
  armed = false;
3974
4028
  cancelQuiet();
3975
4029
  break;
@@ -3978,6 +4032,8 @@ window.__ModuleLoader__.load({
3978
4032
  },
3979
4033
  reset: (top) => {
3980
4034
  armed = false;
4035
+ armedByWheel = false;
4036
+ pendingIntent = false;
3981
4037
  touchIds.clear();
3982
4038
  previousTop = top;
3983
4039
  cancelQuiet();
@@ -3985,20 +4041,27 @@ window.__ModuleLoader__.load({
3985
4041
  dispose: () => {
3986
4042
  disposed = true;
3987
4043
  armed = false;
4044
+ armedByWheel = false;
4045
+ pendingIntent = false;
3988
4046
  touchIds.clear();
3989
4047
  cancelQuiet();
3990
4048
  },
3991
4049
  state: () => ({
3992
4050
  armed,
3993
- previousTop
4051
+ previousTop,
4052
+ pendingIntent
3994
4053
  })
3995
4054
  };
3996
4055
  }
3997
- /** Attach a gate to one scroller; the disposer removes exactly the listeners it added. */
4056
+ /** Observe descendant input but only this scroller's movement; return its listener disposer. */
3998
4057
  function bindHistoryAutoLoadGate(scroller, gate) {
3999
- for (const type of HISTORY_INTENT_EVENTS) scroller.addEventListener(type, gate.handleEvent, HISTORY_INTENT_LISTENER_OPTIONS);
4058
+ const onEvent = (event) => {
4059
+ if ((event.type === "scroll" || event.type === "scrollend") && event.target !== scroller) return;
4060
+ gate.handleEvent(event);
4061
+ };
4062
+ for (const type of HISTORY_INTENT_EVENTS) scroller.addEventListener(type, onEvent, HISTORY_INTENT_LISTENER_OPTIONS);
4000
4063
  return () => {
4001
- for (const type of HISTORY_INTENT_EVENTS) scroller.removeEventListener(type, gate.handleEvent, HISTORY_INTENT_LISTENER_OPTIONS);
4064
+ for (const type of HISTORY_INTENT_EVENTS) scroller.removeEventListener(type, onEvent, HISTORY_INTENT_LISTENER_OPTIONS);
4002
4065
  };
4003
4066
  }
4004
4067
  /**
@@ -5938,12 +6001,17 @@ window.__ModuleLoader__.load({
5938
6001
  /** Browser-safe desktop-admin Host checks. Do not import Node APIs here. */
5939
6002
  const DESKTOP_ADMIN_HEADER = "x-dsh-mobile-desktop-admin";
5940
6003
  const DESKTOP_ADMIN_MARKER = "dsh-app://app";
5941
- /** Add the forwarded Desktop marker only to this plugin's mutating admin requests. */
6004
+ /** Non-simple source marker for this plugin's same-origin JSON management actions. */
6005
+ const WEB_ADMIN_HEADER = "x-dsh-mobile-web-admin";
6006
+ const WEB_ADMIN_MARKER = "same-origin-json";
6007
+ /** Attach separate Desktop/Web source markers only to matching mutating admin requests. */
5942
6008
  function localAdminRequestHeaders(init, location) {
5943
6009
  const headers = new Headers(init?.headers);
5944
6010
  if (!headers.has("content-type")) headers.set("content-type", "application/json");
5945
6011
  headers.delete(DESKTOP_ADMIN_HEADER);
6012
+ headers.delete(WEB_ADMIN_HEADER);
5946
6013
  if (init?.method?.toUpperCase() === "POST" && location.protocol === "dsh-app:" && location.hostname === "app") headers.set(DESKTOP_ADMIN_HEADER, DESKTOP_ADMIN_MARKER);
6014
+ else if (init?.method?.toUpperCase() === "POST" && (location.protocol === "http:" || location.protocol === "https:") && isLocalAdminHostname(location.hostname) && headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() === "application/json") headers.set(WEB_ADMIN_HEADER, WEB_ADMIN_MARKER);
5947
6015
  return headers;
5948
6016
  }
5949
6017
  function unwrapBrackets(hostname) {
@@ -8585,7 +8653,7 @@ window.__ModuleLoader__.load({
8585
8653
  const originHost = element("input");
8586
8654
  originHost.type = "text";
8587
8655
  originHost.value = "127.0.0.1";
8588
- originHost.maxLength = 15;
8656
+ originHost.maxLength = 45;
8589
8657
  const originPortLabel = element("label", "dsh-mobile-control__field");
8590
8658
  originPortLabel.textContent = t("originListenPort");
8591
8659
  const originPort = element("input");
@@ -12413,6 +12481,8 @@ window.__ModuleLoader__.load({
12413
12481
  .dsh-mobile-control__actions{display:flex;flex-wrap:nowrap;gap:6px}.dsh-mobile-control__actions[hidden],.dsh-mobile-control__manage-row[hidden]{display:none}.dsh-mobile-control__actions button{flex:1 1 0;min-width:0;min-height:44px;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-l3,#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}
12414
12482
  .dsh-mobile-control button:focus-visible,.dsh-mobile-control a:focus-visible,.dsh-mobile-control input:focus-visible,.dsh-mobile-control select:focus-visible,.dsh-mobile-control summary:focus-visible{outline:3px solid rgb(37 99 235 / 28%);outline-offset:2px}
12415
12483
  /* 42px matches the DSH sidebar footer row; it is not one of the panel target tiers. */
12484
+ /* Other footer entries share the shell's row; only our own action becomes compact. */
12485
+ .dsh-mobile-control__trigger:not(.is-rail):not(:only-child){flex:0 0 44px;width:44px;min-width:44px;padding:0;justify-content:center;gap:0}.dsh-mobile-control__trigger:not(.is-rail):not(:only-child) .dsh-mobile-control__trigger-label{display:none}
12416
12486
  .dsh-mobile-control__trigger{box-sizing:border-box;flex:1 1 auto;display:flex;align-items:center;gap:8px;width:100%;height:42px;margin:4px 0;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{flex:0 0 auto;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}
12417
12487
  .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:36px;padding:6px 8px;border:1px solid var(--dsw-alias-border-l3,#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-l2,#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-l2,#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:36px;padding:4px 8px;border:1px solid var(--dsw-alias-state-error-primary,#dc2626);border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary,#dc2626);font:12px/1.2 system-ui;cursor:pointer}.dsh-mobile-control__ws-paths{margin:10px 0 0;padding:12px;border:1px solid var(--dsw-alias-border-l2,#dbe1e8);border-radius:13px;background:var(--dsw-alias-bg-layer-2,#fff)}.dsh-mobile-control__ws-paths-list{margin:0 0 8px;padding:0;list-style:none}.dsh-mobile-control__ws-paths-empty{color:var(--dsw-alias-label-secondary,#606873);font-size:12px}.dsh-mobile-control__ws-paths-item{display:flex;align-items:center;gap:8px;padding:6px 0}.dsh-mobile-control__ws-paths-item + .dsh-mobile-control__ws-paths-item{border-top:1px solid var(--dsw-alias-border-l2,#e1e5eb)}.dsh-mobile-control__ws-paths-item code{flex:1 1 0;min-width:0;overflow:hidden;font:11px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}.dsh-mobile-control__ws-paths-remove{flex:none;min-height:36px;padding:4px 8px;border:1px solid var(--dsw-alias-border-l3,#cfd5dd);border-radius:8px;background:transparent;color:inherit;font:12px/1.2 system-ui;cursor:pointer}.dsh-mobile-control__trigger,.dsh-mobile-control__diagnostic-entry{position:relative}.dsh-mobile-control__ws-dot{position:absolute;top:2px;right:2px;z-index:1;min-width:16px;height:16px;padding:0 4px;border-radius:999px;background:var(--dsw-alias-state-error-primary,#dc2626);color:#fff;font:650 11px/16px system-ui;text-align:center;cursor:pointer}.dsh-mobile-control__ws-dot::after{content:'';position:absolute;inset:-5px}.dsh-mobile-control__ws-sr-status{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}.dsh-mobile-control__ws-paths-row{display:flex;gap:8px}.dsh-mobile-control__ws-paths-row .dsh-mobile-control__primary{width:auto;flex:none;padding:9px 14px;border-radius:10px}.dsh-mobile-control__ws-paths-group{padding:6px 0}.dsh-mobile-control__ws-paths-group + .dsh-mobile-control__ws-paths-group{border-top:1px solid var(--dsw-alias-border-l2,#e1e5eb)}.dsh-mobile-control__ws-paths-group-head{display:flex;align-items:center;gap:8px}.dsh-mobile-control__ws-count{flex:none;padding:1px 7px;border-radius:999px;background:var(--dsw-alias-state-business-tertiary,#e8f0ff);color:var(--dsw-alias-label-primary,#1d4ed8);font:650 11px/1.6 system-ui}.dsh-mobile-control__ws-paths-item.is-child{padding-left:12px}.dsh-mobile-control__ws-paths-group-head .dsh-mobile-control__primary,.dsh-mobile-control__ws-paths-item .dsh-mobile-control__primary{width:auto;flex:none;min-height:36px;padding:6px 12px;border-radius:10px}.dsh-mobile-control__ws-paths-row input{flex:1 1 0;min-width:0;box-sizing:border-box;min-height:44px;padding:9px 10px;border:1px solid var(--dsw-alias-border-l3,#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__ws-paths-row button{flex:none}.dsh-mobile-control__ws-paths-detected-title{margin:0 0 2px;font:600 12px/1.4 system-ui}.dsh-mobile-control__ws-paths-advanced{margin-top:8px}.dsh-mobile-control__ws-paths-advanced summary{cursor:pointer;color:var(--dsw-alias-label-secondary,#606873);font:12px/1.4 system-ui;user-select:none}.dsh-mobile-control__ws-paths-advanced summary:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,currentColor);outline-offset:2px;border-radius:4px}.dsh-mobile-control__ws-paths-advanced[open] summary{margin-bottom:6px}.dsh-mobile-control__ws-paths-advanced .dsh-mobile-control__status{margin:6px 0 0}
12418
12488
  @media (max-width:359px){.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}}