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.
- package/CHANGELOG.md +27 -4
- package/CONTRIBUTORS.md +20 -2
- package/README.en.md +13 -7
- package/README.md +13 -7
- package/SECURITY.md +5 -2
- package/THIRD_PARTY_NOTICES.md +30 -2
- package/apps/mobile/README.md +10 -4
- package/apps/mobile/README.zh-CN.md +10 -4
- package/apps/mobile/release.json +3 -3
- package/docs/SELF_HOSTED_ORIGIN.en.md +34 -6
- package/docs/SELF_HOSTED_ORIGIN.md +34 -6
- package/docs/SLOW_CONNECTIONS.en.md +14 -4
- package/docs/SLOW_CONNECTIONS.md +14 -4
- package/lib/client.js +102 -32
- package/lib/client.js.map +1 -1
- package/lib/extension-worker-runtime.mjs.map +1 -1
- package/lib/index.d.mts +25 -3
- package/lib/index.mjs +478 -54
- package/lib/index.mjs.map +1 -1
- package/lib/mobile-compat.js +1 -1
- package/lib/mobile-compat.js.map +1 -1
- package/package.json +3 -1
- package/packages/question-fixes/package.json +1 -1
|
@@ -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
|
-
|
|
23
|
+
### Internal authentication exchange diagnostics (plugin 0.6.15+)
|
|
24
24
|
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
package/docs/SLOW_CONNECTIONS.md
CHANGED
|
@@ -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
|
-
|
|
23
|
+
### 内部认证交换诊断(插件 0.6.15 起)
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
`upstream auth` 单独记录网关向本机 DSH 交换内部认证 Cookie 的结果,不是手机请求的 `auth failure`。每条只包含该网关内的交换序号 `sequence`、阶段 `stage`、固定结果 `outcome`、耗时 `durationMs`,以及收到响应头后才有的上游 HTTP `status`;这个状态不是手机最终收到的状态。`accepted` 表示严格通过 303、Cookie 和有效期检查;其他结果区分目标配置无效、状态不符、Cookie 无效、超时或传输失败。不记录内部启动地址、Token、Cookie 名称或值、响应正文及原始错误。
|
|
26
26
|
|
|
27
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
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
|
-
|
|
3957
|
-
if (
|
|
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
|
-
|
|
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
|
-
/**
|
|
4056
|
+
/** Observe descendant input but only this scroller's movement; return its listener disposer. */
|
|
3998
4057
|
function bindHistoryAutoLoadGate(scroller, gate) {
|
|
3999
|
-
|
|
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,
|
|
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
|
-
/**
|
|
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 =
|
|
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}}
|