@linxin666/dsh-remote-web-ui 0.4.1 → 0.4.3
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/README.i18n.yaml +2 -2
- package/README.md +9 -7
- package/README.zh.md +9 -7
- package/lib/client.js +282 -619
- package/lib/client.js.map +1 -1
- package/lib/index.js +418 -901
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +9 -10
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +1 -1
- package/lib/types/client/index.d.ts +32 -3
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +56 -11
- package/lib/types/client/locales.d.ts +0 -84
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +2 -86
- package/lib/types/client/pair-api.d.ts +4 -0
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +11 -7
- package/lib/types/client/remote-channel.d.ts +27 -7
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +25 -17
- package/lib/types/client/settings-entry-form.d.ts +50 -0
- package/lib/types/client/settings-entry-form.d.ts.map +1 -0
- package/lib/types/client/settings-entry-form.js +98 -0
- package/lib/types/index.d.ts +99 -7
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +104 -137
- package/lib/types/lan-bind-plan.d.ts +14 -0
- package/lib/types/lan-bind-plan.d.ts.map +1 -1
- package/lib/types/lan-bind-plan.js +16 -0
- package/lib/types/lan-bind.d.ts +5 -1
- package/lib/types/lan-bind.d.ts.map +1 -1
- package/lib/types/lan-bind.js +65 -2
- package/lib/types/mount-once.d.ts +38 -7
- package/lib/types/mount-once.d.ts.map +1 -1
- package/lib/types/mount-once.js +96 -12
- package/lib/types/pairing.d.ts +15 -2
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +26 -14
- package/lib/types/pairing.persist.test.js +43 -3
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +13 -3
- package/lib/types/remote-channel-rules.d.ts +74 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +104 -0
- package/lib/types/tunnel.d.ts +60 -3
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +136 -3
- package/package.json +21 -20
- package/src/client/RemoteEntry.tsx +8 -10
- package/src/client/RemoteSettingsCard.tsx +0 -2
- package/src/client/index.ts +58 -12
- package/src/client/locales.ts +2 -86
- package/src/client/pair-api.ts +11 -7
- package/src/client/remote-channel.ts +38 -15
- package/src/client/remote.module.css +61 -193
- package/src/client/settings-entry-form.ts +142 -0
- package/src/index.ts +156 -152
- package/src/lan-bind-plan.ts +22 -0
- package/src/lan-bind.ts +63 -2
- package/src/mount-once.ts +107 -13
- package/src/pairing.persist.test.ts +47 -3
- package/src/pairing.ts +26 -12
- package/src/remote-channel-boot.ts +13 -3
- package/src/remote-channel-rules.ts +113 -0
- package/src/tunnel.ts +142 -3
- package/lib/types/client/UpdateEntry.d.ts +0 -14
- package/lib/types/client/UpdateEntry.d.ts.map +0 -1
- package/lib/types/client/UpdateEntry.js +0 -112
- package/lib/types/client/UpdatePanel.d.ts +0 -42
- package/lib/types/client/UpdatePanel.d.ts.map +0 -1
- package/lib/types/client/UpdatePanel.js +0 -101
- package/lib/types/client/update-api.d.ts +0 -29
- package/lib/types/client/update-api.d.ts.map +0 -1
- package/lib/types/client/update-api.js +0 -47
- package/lib/types/console-output.d.ts +0 -41
- package/lib/types/console-output.d.ts.map +0 -1
- package/lib/types/console-output.js +0 -89
- package/lib/types/update-routes.d.ts +0 -30
- package/lib/types/update-routes.d.ts.map +0 -1
- package/lib/types/update-routes.js +0 -47
- package/lib/types/update.d.ts +0 -272
- package/lib/types/update.d.ts.map +0 -1
- package/lib/types/update.js +0 -744
- package/src/client/UpdateEntry.tsx +0 -144
- package/src/client/UpdatePanel.tsx +0 -256
- package/src/client/update-api.ts +0 -49
- package/src/console-output.ts +0 -97
- package/src/update-routes.ts +0 -65
- package/src/update.ts +0 -882
package/src/client/locales.ts
CHANGED
|
@@ -15,7 +15,7 @@ export const zh = {
|
|
|
15
15
|
'status.disconnected': '已配对设备离线',
|
|
16
16
|
'status.stopped': '已停止远程访问',
|
|
17
17
|
'status.lanRequired': '此功能需要局域网绑定或公网地址才能使用',
|
|
18
|
-
'status.lanRequiredHint': '当前服务仅绑定在 127.0.0.1
|
|
18
|
+
'status.lanRequiredHint': '当前服务仅绑定在 127.0.0.1 且未配置公网地址,手机无法访问。请在本插件的设置卡片中打开“局域网访问”(装在全家桶里时在「设置 → Web 插件 → 远程访问设置」,单独安装时在设置 → 插件 → 本插件行),或用 dsh web --host 0.0.0.0 重新启动,或填写内网穿透的公网地址。',
|
|
19
19
|
'status.loopbackRequired': '配对面板仅限本机使用',
|
|
20
20
|
'status.loopbackRequiredHint': '请通过 http://127.0.0.1 打开此页面后重试;手机请使用配对链接访问。',
|
|
21
21
|
'status.unreachable': '无法连接配对服务',
|
|
@@ -123,48 +123,6 @@ export const zh = {
|
|
|
123
123
|
'settings.saveFailed': '部署未接受这些值,已保留供你修改。',
|
|
124
124
|
'settings.invalidNumber': '请输入数字,留空则使用默认值。',
|
|
125
125
|
// 更新面板(侧边栏更新按钮)。
|
|
126
|
-
'update.label': '检查更新',
|
|
127
|
-
'update.badge': '有更新',
|
|
128
|
-
'update.availableLabel': '发现新版本,检查更新',
|
|
129
|
-
'update.title': '检查更新',
|
|
130
|
-
'update.checking': '正在检查远程仓库中的新版本…',
|
|
131
|
-
'update.upToDate': '当前已是最新版本',
|
|
132
|
-
'update.upToDateDetail': '{name} 已是最新版本 v{version}。',
|
|
133
|
-
'update.found': '发现新版本',
|
|
134
|
-
'update.foundDetail': '{name} 有新版本 v{version}。确认后开始更新。',
|
|
135
|
-
'update.start': '开始更新',
|
|
136
|
-
'update.updating': '正在更新 {name} 至 v{version},可能需要 1-2 分钟…',
|
|
137
|
-
'update.cooldownNotice': '本次更新由你主动触发,将跳过 pnpm 的 24 小时发布冷却期(供应链保护)直接安装所选版本。',
|
|
138
|
-
'update.updatingTitle': '正在更新…',
|
|
139
|
-
'update.done': '更新完成',
|
|
140
|
-
'update.doneDetail': '所有组件已更新到最新版本。',
|
|
141
|
-
'update.restartHint': '请重启 dsh web 使新版本生效:在终端按 Ctrl+C 停止后重新运行 dsh web。',
|
|
142
|
-
'update.linkMode': '当前为本地开发模式',
|
|
143
|
-
'update.linkModeDetail': 'dsh-web 或其组件通过本地链接安装(开发模式),无法自动更新。npm 上最新版本:{version}。请同步本地仓库,或移除与全家桶重复的独立链接。',
|
|
144
|
-
'update.missing': '未找到 dsh-web 安装',
|
|
145
|
-
'update.missingDetail': '无法定位已安装的 dsh-web 聚合包。请先运行 dsh plugin --profile <profile> add @linxin666/dsh-web-all。',
|
|
146
|
-
'update.offline': '无法连接更新源',
|
|
147
|
-
'update.offlineDetail': '网络不可用或更新服务异常,请稍后重试。',
|
|
148
|
-
'update.unmounted': '更新服务未加载',
|
|
149
|
-
'update.unmountedDetail': '当前 dsh web 进程运行的插件版本不含更新服务,请重启 dsh web(Ctrl+C 后重新运行)后再试。',
|
|
150
|
-
'update.error': '更新失败',
|
|
151
|
-
'update.packages': '组件版本',
|
|
152
|
-
'update.releaseNotes': 'v{version} 更新说明',
|
|
153
|
-
'update.releaseFeatures': '新增功能',
|
|
154
|
-
'update.releaseFixes': '修复',
|
|
155
|
-
'update.releaseOther': '其他变动',
|
|
156
|
-
'update.releaseUnavailable': '本次发布说明暂不可用,可前往 GitHub Release 查看。',
|
|
157
|
-
'update.componentVersions': '查看组件版本',
|
|
158
|
-
'update.retry': '重新检查',
|
|
159
|
-
'update.close': '关闭更新面板',
|
|
160
|
-
'update.error.pnpmMissing': '未找到 pnpm,已尝试 pnpm / corepack / npx 仍不可用。请安装 pnpm 后重启应用(Windows: npm install -g pnpm 或 corepack enable;macOS: brew install pnpm 或 corepack enable)。',
|
|
161
|
-
'update.error.timeout': '更新超时,已终止安装进程,请重试。',
|
|
162
|
-
'update.error.notFound': '未找到 dsh-web 聚合包安装,无法更新。',
|
|
163
|
-
'update.error.link': 'dsh-web 或其组件使用本地链接,无法自动更新;请同步本地仓库,或移除与全家桶重复的独立链接。',
|
|
164
|
-
'update.error.pnpmFailed': '更新执行失败(pnpm 退出码 {code}),详见下方输出。',
|
|
165
|
-
'update.error.stale': '更新命令已执行,但安装版本未变化。常见原因:pnpm 11 的 minimumReleaseAge 门禁会静默跳过发布不足 24 小时的新版本。请在 profile 目录(~/.dsh/profiles/<profile>)的 pnpm-workspace.yaml 中配置 minimumReleaseAgeExclude(例如 "@linxin666/*")或 minimumReleaseAge: 0 后重新点击更新。',
|
|
166
|
-
'update.error.verifyFailed': '更新命令已执行,但无法核对安装后的版本(可能是 registry 探测失败或安装路径变化)。请查看下方输出,或稍后重试。',
|
|
167
|
-
'update.error.unknown': '更新失败,请重试。',
|
|
168
126
|
} satisfies Record<string, string>
|
|
169
127
|
|
|
170
128
|
/** The remote namespace key union. */
|
|
@@ -188,7 +146,7 @@ export const en = {
|
|
|
188
146
|
'status.disconnected': 'Paired devices offline',
|
|
189
147
|
'status.stopped': 'Remote access stopped',
|
|
190
148
|
'status.lanRequired': 'This feature needs a LAN bind or a public address',
|
|
191
|
-
'status.lanRequiredHint': 'The server is bound to 127.0.0.1 and no public address is configured, so a phone cannot reach it. Turn on LAN access in
|
|
149
|
+
'status.lanRequiredHint': 'The server is bound to 127.0.0.1 and no public address is configured, so a phone cannot reach it. Turn on LAN access in this plugin settings card (under Settings → Web Plugins → Remote access when the full family is installed, or on this plugin row under Settings → Plugins for a standalone install), or restart with dsh web --host 0.0.0.0, or set the tunneled public address.',
|
|
192
150
|
'status.loopbackRequired': 'The pairing panel works on this machine only',
|
|
193
151
|
'status.loopbackRequiredHint': 'Open this page at http://127.0.0.1 to mint a QR code; phones use the paired link.',
|
|
194
152
|
'status.unreachable': 'Cannot reach the pairing service',
|
|
@@ -296,46 +254,4 @@ export const en = {
|
|
|
296
254
|
'settings.saveFailed': 'The deployment did not accept these values; they were left for you to correct.',
|
|
297
255
|
'settings.invalidNumber': 'Enter a number, or leave blank to use the default.',
|
|
298
256
|
// Update panel (the sidebar update trigger).
|
|
299
|
-
'update.label': 'Check for updates',
|
|
300
|
-
'update.badge': 'Update available',
|
|
301
|
-
'update.availableLabel': 'New version available. Check for updates',
|
|
302
|
-
'update.title': 'Check for updates',
|
|
303
|
-
'update.checking': 'Checking the remote repository for newer releases…',
|
|
304
|
-
'update.upToDate': 'Everything is up to date',
|
|
305
|
-
'update.upToDateDetail': '{name} is at the latest version v{version}.',
|
|
306
|
-
'update.found': 'A new version is available',
|
|
307
|
-
'update.foundDetail': '{name} has a newer release v{version}. Confirm to start the update.',
|
|
308
|
-
'update.start': 'Update now',
|
|
309
|
-
'update.updating': 'Updating {name} to v{version}; this may take 1-2 minutes…',
|
|
310
|
-
'update.cooldownNotice': 'This user-initiated update bypasses pnpm\'s 24-hour release cooldown (supply-chain protection) and installs the selected version immediately.',
|
|
311
|
-
'update.updatingTitle': 'Updating…',
|
|
312
|
-
'update.done': 'Update complete',
|
|
313
|
-
'update.doneDetail': 'All components are up to date.',
|
|
314
|
-
'update.restartHint': 'Restart dsh web for the new version to take effect: stop it with Ctrl+C in the terminal, then run dsh web again.',
|
|
315
|
-
'update.linkMode': 'Local development mode',
|
|
316
|
-
'update.linkModeDetail': 'dsh-web or one of its components is installed via a local link and cannot be updated remotely. Latest npm release: {version}. Sync the local checkout, or remove a standalone link duplicated by the aggregate.',
|
|
317
|
-
'update.missing': 'dsh-web not found',
|
|
318
|
-
'update.missingDetail': 'Could not locate the installed dsh-web aggregate package. Install it with: dsh plugin --profile <profile> add @linxin666/dsh-web-all',
|
|
319
|
-
'update.offline': 'Cannot reach the update source',
|
|
320
|
-
'update.offlineDetail': 'The network is unavailable or the update service is failing; try again later.',
|
|
321
|
-
'update.unmounted': 'The update service is not loaded',
|
|
322
|
-
'update.unmountedDetail': 'The running dsh web process loads a plugin build without the update service; restart dsh web (Ctrl+C, then run dsh web again) and retry.',
|
|
323
|
-
'update.error': 'Update failed',
|
|
324
|
-
'update.packages': 'Component versions',
|
|
325
|
-
'update.releaseNotes': 'v{version} release notes',
|
|
326
|
-
'update.releaseFeatures': 'New Features',
|
|
327
|
-
'update.releaseFixes': 'Bug Fixes',
|
|
328
|
-
'update.releaseOther': 'Other Changes',
|
|
329
|
-
'update.releaseUnavailable': 'Release notes are unavailable for this version; see the GitHub release instead.',
|
|
330
|
-
'update.componentVersions': 'View component versions',
|
|
331
|
-
'update.retry': 'Check again',
|
|
332
|
-
'update.close': 'Close update panel',
|
|
333
|
-
'update.error.pnpmMissing': 'pnpm was not found (tried pnpm / corepack / npx). Install pnpm and restart the app (Windows: npm install -g pnpm or corepack enable; macOS: brew install pnpm or corepack enable).',
|
|
334
|
-
'update.error.timeout': 'The update timed out and the install process was killed; try again.',
|
|
335
|
-
'update.error.notFound': 'The dsh-web aggregate package is not installed; nothing to update.',
|
|
336
|
-
'update.error.link': 'dsh-web or one of its components uses a local link. Sync the checkout, or remove a standalone link duplicated by the aggregate.',
|
|
337
|
-
'update.error.pnpmFailed': 'The update failed (pnpm exited with code {code}); see the output below.',
|
|
338
|
-
'update.error.stale': 'The update command ran but the installed versions did not change. Likely cause: the pnpm 11 minimumReleaseAge gate silently skips releases published less than 24 hours ago. Add minimumReleaseAgeExclude (e.g. "@linxin666/*") or set minimumReleaseAge: 0 in pnpm-workspace.yaml under the profile directory (~/.dsh/profiles/<profile>), then run the update again.',
|
|
339
|
-
'update.error.verifyFailed': 'The update command ran but the installed versions could not be verified afterwards (registry probe failed or the install path changed). Inspect the output below, or retry later.',
|
|
340
|
-
'update.error.unknown': 'Update failed; try again.',
|
|
341
257
|
} satisfies Record<RemoteKey, string>
|
package/src/client/pair-api.ts
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* Browser-side wire helpers for the /api/pair surface. Plain fetch over
|
|
3
3
|
* same-origin /api (like the connection client); JSON bodies, string
|
|
4
4
|
* responses for the error codes.
|
|
5
|
+
*
|
|
6
|
+
* The paths are DOCUMENT-RELATIVE (issue #1707): the GUI is served with
|
|
7
|
+
* `<base href="./">`, so a sub-path deployment resolves them against its entry
|
|
8
|
+
* directory instead of escaping to the origin root.
|
|
5
9
|
*/
|
|
6
10
|
|
|
7
11
|
/** issue() response. */
|
|
@@ -43,7 +47,7 @@ export interface PairGatePolicy {
|
|
|
43
47
|
|
|
44
48
|
/** Read the host-authoritative desktop pairing policy. */
|
|
45
49
|
export async function readPairGatePolicy(): Promise<PairGatePolicy> {
|
|
46
|
-
const response = await fetch('
|
|
50
|
+
const response = await fetch('api/pair/status')
|
|
47
51
|
if (!response.ok) throw new Error(`remote-web-ui: status failed with ${String(response.status)}`)
|
|
48
52
|
const value = await response.json() as { requirePairingForLan?: unknown }
|
|
49
53
|
if (typeof value.requirePairingForLan !== 'boolean') {
|
|
@@ -116,7 +120,7 @@ export interface DeviceFrame {
|
|
|
116
120
|
* origin — the panel is a desktop control endpoint).
|
|
117
121
|
*/
|
|
118
122
|
export async function issuePair(address?: string): Promise<IssueResponse> {
|
|
119
|
-
const response = await fetch('
|
|
123
|
+
const response = await fetch('api/pair/issue', {
|
|
120
124
|
method: 'POST',
|
|
121
125
|
headers: { 'content-type': 'application/json' },
|
|
122
126
|
body: JSON.stringify({
|
|
@@ -139,7 +143,7 @@ export async function issuePair(address?: string): Promise<IssueResponse> {
|
|
|
139
143
|
* @returns the wire result.
|
|
140
144
|
*/
|
|
141
145
|
export async function acceptPair(token: string): Promise<{ ok: true } | AcceptFailure> {
|
|
142
|
-
const response = await fetch('
|
|
146
|
+
const response = await fetch('api/pair/accept', {
|
|
143
147
|
method: 'POST',
|
|
144
148
|
headers: { 'content-type': 'application/json' },
|
|
145
149
|
body: JSON.stringify({ token }),
|
|
@@ -153,7 +157,7 @@ export async function acceptPair(token: string): Promise<{ ok: true } | AcceptFa
|
|
|
153
157
|
|
|
154
158
|
/** Revoke mobile access (paired devices + the current token). */
|
|
155
159
|
export async function stopPair(): Promise<void> {
|
|
156
|
-
const response = await fetch('
|
|
160
|
+
const response = await fetch('api/pair/stop', { method: 'POST' })
|
|
157
161
|
if (!response.ok) throw new Error(`remote-web-ui: stop failed with ${String(response.status)}`)
|
|
158
162
|
}
|
|
159
163
|
|
|
@@ -162,7 +166,7 @@ export async function stopPair(): Promise<void> {
|
|
|
162
166
|
* @param deviceId - the session id of the row to drop.
|
|
163
167
|
*/
|
|
164
168
|
export async function revokePair(deviceId: string): Promise<void> {
|
|
165
|
-
const response = await fetch('
|
|
169
|
+
const response = await fetch('api/pair/revoke', {
|
|
166
170
|
method: 'POST',
|
|
167
171
|
headers: { 'content-type': 'application/json' },
|
|
168
172
|
body: JSON.stringify({ deviceId }),
|
|
@@ -177,7 +181,7 @@ export async function revokePair(deviceId: string): Promise<void> {
|
|
|
177
181
|
* proves this page is not paired (see {@link shouldStopHeartbeat}).
|
|
178
182
|
*/
|
|
179
183
|
export async function sendHeartbeat(): Promise<number> {
|
|
180
|
-
const response = await fetch('
|
|
184
|
+
const response = await fetch('api/pair/heartbeat', { method: 'POST' })
|
|
181
185
|
return response.status
|
|
182
186
|
}
|
|
183
187
|
|
|
@@ -242,7 +246,7 @@ export class LanBindStatusError extends Error {
|
|
|
242
246
|
|
|
243
247
|
/** Read the LAN-bind facts (loopback-only endpoint). */
|
|
244
248
|
export async function readLanBindStatus(): Promise<LanBindFrame> {
|
|
245
|
-
const response = await fetch('
|
|
249
|
+
const response = await fetch('api/pair/lan-bind')
|
|
246
250
|
if (!response.ok) throw new LanBindStatusError(response.status, `remote-web-ui: lan-bind status failed with ${String(response.status)}`)
|
|
247
251
|
return await response.json() as LanBindFrame
|
|
248
252
|
}
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import {
|
|
26
|
+
isLoopbackHostname as isLoopbackName,
|
|
27
|
+
isLocalPage,
|
|
26
28
|
REMOTE_API_PREFIX,
|
|
27
29
|
REMOTE_CHANNEL_RULES,
|
|
28
30
|
REMOTE_PREFIX,
|
|
@@ -40,13 +42,41 @@ export interface RemoteChannelSettingsSnapshot {
|
|
|
40
42
|
value?: { enabled?: boolean; requirePairingForLan?: boolean }
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Facts about the live page that decide whether it is local. Defaults come
|
|
47
|
+
* from `window` so ordinary call sites stay argument-free; tests pass them
|
|
48
|
+
* explicitly.
|
|
49
|
+
*/
|
|
50
|
+
export interface PageOriginFacts {
|
|
51
|
+
hostname: string
|
|
52
|
+
protocol?: string
|
|
53
|
+
transportOwnsHost?: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Read the page-origin facts from a window-like object: the hostname and
|
|
58
|
+
* protocol of the page itself plus the official transport's host-owner flag
|
|
59
|
+
* (`__DSH_TRANSPORT__.ownsHost`, set by the desktop shell before any boot
|
|
60
|
+
* entry, and by this plugin's own paired landing).
|
|
61
|
+
* @param win - the window to read (defaults to the real one).
|
|
62
|
+
* @returns the facts `isLocalPage`/`remoteChannelRequired` decide from.
|
|
63
|
+
*/
|
|
64
|
+
export function pageOriginFacts(win: Pick<Window, 'location'> & Partial<Pick<Window, 'navigator'>> = window): PageOriginFacts {
|
|
65
|
+
const transport = (win as unknown as { __DSH_TRANSPORT__?: { ownsHost?: unknown } }).__DSH_TRANSPORT__
|
|
66
|
+
return {
|
|
67
|
+
hostname: win.location.hostname,
|
|
68
|
+
protocol: win.location.protocol,
|
|
69
|
+
...(transport?.ownsHost === true ? { transportOwnsHost: true } : {}),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
43
73
|
/** Decide whether a remote desktop channel is required from local or host policy. */
|
|
44
74
|
export function remoteChannelRequired(
|
|
45
|
-
|
|
75
|
+
origin: PageOriginFacts,
|
|
46
76
|
snapshot: RemoteChannelSettingsSnapshot,
|
|
47
77
|
hostPairingPolicy: boolean | undefined,
|
|
48
78
|
): boolean {
|
|
49
|
-
if (
|
|
79
|
+
if (isLocalPage(origin.hostname, origin.protocol, origin.transportOwnsHost)) return false
|
|
50
80
|
if (snapshot.status === 'ready') {
|
|
51
81
|
return (snapshot.value?.enabled ?? true) && (snapshot.value?.requirePairingForLan ?? true)
|
|
52
82
|
}
|
|
@@ -56,20 +86,13 @@ export function remoteChannelRequired(
|
|
|
56
86
|
}
|
|
57
87
|
|
|
58
88
|
/**
|
|
59
|
-
* Browser-safe loopback classification for the page origin
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
89
|
+
* Browser-safe loopback classification for the page origin. The definition
|
|
90
|
+
* lives once in the rules module, which the inlined boot script mirrors
|
|
91
|
+
* verbatim, so the browser half and the parse-time patch can never disagree
|
|
92
|
+
* (an IPv6-loopback origin judged remote would rewrite every call onto a
|
|
93
|
+
* channel it can never pair on).
|
|
63
94
|
*/
|
|
64
|
-
export
|
|
65
|
-
// WHATWG location.hostname keeps IPv6 literals bracketed ("[::1]"); the
|
|
66
|
-
// host-side shared predicate accepts that spelling, so this browser copy must
|
|
67
|
-
// too, or an IPv6-loopback origin is judged remote and rewrites every call
|
|
68
|
-
// onto the gated channel where it can never pair.
|
|
69
|
-
if (hostname === 'localhost' || hostname === '::1' || hostname === '[::1]') return true
|
|
70
|
-
const parts = hostname.split('.')
|
|
71
|
-
return parts.length === 4 && parts[0] === '127' && parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255)
|
|
72
|
-
}
|
|
95
|
+
export const isLoopbackHostname = isLoopbackName
|
|
73
96
|
|
|
74
97
|
/**
|
|
75
98
|
* Whether one same-origin path must ride the gated channel (fetch, EventSource,
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/* Trigger: the 36px rail circle, or a fully rounded (pill) full-row button
|
|
22
|
-
in wide mode — both maximally rounded so the
|
|
23
|
-
|
|
24
|
-
geometry. */
|
|
22
|
+
in wide mode — both maximally rounded so the seat's occupants (this
|
|
23
|
+
trigger, the dsh-update trigger, the usage card) share one shape family
|
|
24
|
+
(#1035); the seat's owner (ui-sidebar) lays out the geometry. */
|
|
25
25
|
.trigger {
|
|
26
26
|
position: relative;
|
|
27
27
|
flex: none;
|
|
@@ -40,52 +40,18 @@
|
|
|
40
40
|
transition: background-color 120ms ease, color 120ms ease, box-shadow 120ms ease;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
.trigger[data-update-available] {
|
|
44
|
-
color: var(--dsw-alias-brand-primary);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.trigger[data-update-available]::after {
|
|
48
|
-
position: absolute;
|
|
49
|
-
top: 4px;
|
|
50
|
-
right: 4px;
|
|
51
|
-
width: 8px;
|
|
52
|
-
height: 8px;
|
|
53
|
-
box-sizing: border-box;
|
|
54
|
-
border: 2px solid var(--dsw-alias-bg-layer-1);
|
|
55
|
-
border-radius: 50%;
|
|
56
|
-
background: var(--dsw-alias-brand-primary);
|
|
57
|
-
content: '';
|
|
58
|
-
pointer-events: none;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
43
|
.trigger[data-wide='wide'] {
|
|
62
44
|
flex: 1 1 auto;
|
|
63
45
|
justify-content: flex-start;
|
|
64
46
|
width: auto;
|
|
65
47
|
min-width: 0;
|
|
66
|
-
/* Pill: maximal rounding, same shape family as the 36px rail circle
|
|
67
|
-
|
|
48
|
+
/* Pill: maximal rounding, the same shape family as the 36px rail circle
|
|
49
|
+
beside it — one consistent footer-action look (#1035). */
|
|
68
50
|
border-radius: 999px;
|
|
69
51
|
gap: 8px;
|
|
70
52
|
padding: 0 10px;
|
|
71
53
|
}
|
|
72
54
|
|
|
73
|
-
.trigger[data-update-available][data-wide='wide'] {
|
|
74
|
-
flex: none;
|
|
75
|
-
gap: 6px;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.trigger[data-update-available][data-wide='wide']::after {
|
|
79
|
-
display: none;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
.updateBadgeText {
|
|
83
|
-
font-size: 13px;
|
|
84
|
-
font-weight: 500;
|
|
85
|
-
line-height: 1;
|
|
86
|
-
white-space: nowrap;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
55
|
.trigger:hover:not(:disabled) {
|
|
90
56
|
background: var(--dsw-alias-interactive-bg-hover);
|
|
91
57
|
color: var(--dsw-alias-label-primary);
|
|
@@ -768,9 +734,9 @@
|
|
|
768
734
|
}
|
|
769
735
|
}
|
|
770
736
|
|
|
771
|
-
/* Footer seat row:
|
|
772
|
-
|
|
773
|
-
|
|
737
|
+
/* Footer seat row: this plugin's own occupant (the remote trigger) sits in
|
|
738
|
+
the shared sidebar.footer.action seat; the rail marker lets the seat stack
|
|
739
|
+
its occupants in the 56px icon column (see the rail rule below). */
|
|
774
740
|
.entryRow {
|
|
775
741
|
flex: none;
|
|
776
742
|
display: flex;
|
|
@@ -788,12 +754,13 @@
|
|
|
788
754
|
|
|
789
755
|
The official Settings trigger owns a whole row while the footer actions
|
|
790
756
|
stack above it, so the trigger's right half sits empty. On the wide column
|
|
791
|
-
the two share one line (the Settings trigger, then the
|
|
792
|
-
|
|
793
|
-
child (the usage glance card, another plugin's block)
|
|
794
|
-
its own above them. The rail (data-sidebar-collapsed on
|
|
795
|
-
the shell's stacked foot, where the circles stack by
|
|
796
|
-
the container classes; the semantic suffixes survive
|
|
757
|
+
the two share one line (the Settings trigger, then the footer-action
|
|
758
|
+
occupants — this trigger, the dsh-update trigger, another plugin's control)
|
|
759
|
+
and every other foot child (the usage glance card, another plugin's block)
|
|
760
|
+
keeps a full row of its own above them. The rail (data-sidebar-collapsed on
|
|
761
|
+
the frame) keeps the shell's stacked foot, where the circles stack by
|
|
762
|
+
design. The shell owns the container classes; the semantic suffixes survive
|
|
763
|
+
its rebuilds. */
|
|
797
764
|
:global([data-dsh-frame]:not([data-sidebar-collapsed])) [class*='footArea'] {
|
|
798
765
|
flex-direction: row;
|
|
799
766
|
flex-wrap: wrap;
|
|
@@ -805,162 +772,63 @@
|
|
|
805
772
|
min-width: 0;
|
|
806
773
|
}
|
|
807
774
|
|
|
808
|
-
:global([data-dsh-frame]:not([data-sidebar-collapsed])) [class*='
|
|
775
|
+
:global([data-dsh-frame]:not([data-sidebar-collapsed])) [class*='footArea'] > [class*='footerActions'] {
|
|
776
|
+
/* The seat's own slot host is already `display: contents`, so the seat's box
|
|
777
|
+
is what decided its occupants' layout. That made this plugin's rule fatal
|
|
778
|
+
to a SHARED container (issue #1710): the moment any other plugin registered
|
|
779
|
+
into `sidebar.footer.action` — the reporter had a 339px balance stack and a
|
|
780
|
+
context panel — the seat became a tall column, and the settings trigger,
|
|
781
|
+
which is a shell element this plugin does not own, was squeezed to 102px and
|
|
782
|
+
vertically centred inside it (`y534 = 351 + (417-50)/2`).
|
|
783
|
+
Dropping the box hands every occupant back to the foot's own wrapping
|
|
784
|
+
layout, where each one is sized on its own merits. */
|
|
785
|
+
display: contents;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/* A registrant that is not one of the family's own footer entries (an
|
|
789
|
+
icon-sized row) takes a full line of its own above the bottom row. This is
|
|
790
|
+
the shell's own intent for the seat, and it is what keeps a tall third-party
|
|
791
|
+
block from displacing the settings trigger. */
|
|
792
|
+
:global([data-dsh-frame]:not([data-sidebar-collapsed])) [class*='footerActions'] > [data-slot='sidebar.footer.action'] > :not([data-dsh-part='entry']):not([class*='entryRow']) {
|
|
793
|
+
flex: 1 1 100%;
|
|
809
794
|
order: 1;
|
|
810
|
-
/* Content-sized basis: the trigger's own width forces it onto the line after
|
|
811
|
-
a full-width sibling, then flex-shrink gives the actions their space. */
|
|
812
|
-
flex: 1 1 auto;
|
|
813
|
-
width: auto;
|
|
814
795
|
min-width: 0;
|
|
815
796
|
}
|
|
816
797
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
798
|
+
/* The family's own footer entries keep their intrinsic width and close the
|
|
799
|
+
bottom row, sharing it with the settings trigger. `data-dsh-part="entry"` is
|
|
800
|
+
the family contract for these triggers (dsh-update, dsh-session-id); the
|
|
801
|
+
remote trigger marks its row with its own class. */
|
|
802
|
+
:global([data-dsh-frame]:not([data-sidebar-collapsed])) [class*='footerActions'] > [data-slot='sidebar.footer.action'] > [data-dsh-part='entry'],
|
|
803
|
+
:global([data-dsh-frame]:not([data-sidebar-collapsed])) [class*='footerActions'] > [data-slot='sidebar.footer.action'] > [class*='entryRow'] {
|
|
804
|
+
flex: 0 0 auto;
|
|
805
|
+
order: 4;
|
|
821
806
|
min-width: 0;
|
|
822
|
-
align-items: center;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
/* ── update panel extras ─────────────────────────────────────────────── */
|
|
826
|
-
|
|
827
|
-
.updateStatus {
|
|
828
|
-
margin: 0;
|
|
829
|
-
font-weight: 600;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
.updateDetail {
|
|
833
|
-
margin: 6px 0 0;
|
|
834
|
-
color: var(--dsw-alias-label-secondary);
|
|
835
|
-
font-size: 13px;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
.updateError {
|
|
839
|
-
margin: 0;
|
|
840
|
-
color: var(--dsw-alias-text-danger, var(--dsw-alias-label-primary));
|
|
841
|
-
font-weight: 600;
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
.updateOutput {
|
|
845
|
-
max-height: 180px;
|
|
846
|
-
margin: 10px 0 0;
|
|
847
|
-
overflow: auto;
|
|
848
|
-
padding: 10px 12px;
|
|
849
|
-
border-radius: 10px;
|
|
850
|
-
background: var(--dsw-alias-bg-layer-1);
|
|
851
|
-
color: var(--dsw-alias-label-secondary);
|
|
852
|
-
font-size: 12px;
|
|
853
|
-
line-height: 18px;
|
|
854
|
-
white-space: pre-wrap;
|
|
855
|
-
word-break: break-all;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
.updateList {
|
|
859
|
-
margin: 12px 0 0;
|
|
860
|
-
padding: 0;
|
|
861
|
-
list-style: none;
|
|
862
|
-
display: flex;
|
|
863
|
-
flex-direction: column;
|
|
864
|
-
gap: 6px;
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
.updateListItem {
|
|
868
|
-
display: flex;
|
|
869
|
-
align-items: baseline;
|
|
870
|
-
justify-content: space-between;
|
|
871
|
-
gap: 12px;
|
|
872
|
-
font-size: 13px;
|
|
873
807
|
}
|
|
874
808
|
|
|
875
|
-
|
|
809
|
+
:global([data-dsh-frame]:not([data-sidebar-collapsed])) [class*='settingsArea'] {
|
|
810
|
+
order: 3;
|
|
811
|
+
/* Content-sized basis: the trigger's own width forces it onto the line after
|
|
812
|
+
a full-width sibling, then flex-shrink gives the actions their space. */
|
|
813
|
+
flex: 1 1 auto;
|
|
814
|
+
width: auto;
|
|
876
815
|
min-width: 0;
|
|
877
|
-
overflow: hidden;
|
|
878
|
-
text-overflow: ellipsis;
|
|
879
|
-
white-space: nowrap;
|
|
880
|
-
color: var(--dsw-alias-label-primary);
|
|
881
816
|
}
|
|
882
817
|
|
|
883
|
-
|
|
884
|
-
flex: none;
|
|
885
|
-
color: var(--dsw-alias-label-secondary);
|
|
886
|
-
font-variant-numeric: tabular-nums;
|
|
887
|
-
}
|
|
818
|
+
/* ── Collapsed rail foot seat ────────────────────────────────────────────
|
|
888
819
|
|
|
889
|
-
.
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
display: flex;
|
|
820
|
+
The shell renders `sidebar.footer.action` as a centered horizontal row
|
|
821
|
+
in both column states. In the 56px rail that row is only as wide as its
|
|
822
|
+
occupants, so a second registrar overflows the icon column on both sides:
|
|
823
|
+
with two 36px circles the seat measures 78px against a 35px column, and
|
|
824
|
+
the outer buttons land at x=-11 and x=31 while every other rail row sits
|
|
825
|
+
at x=10 (issue #1678). Stack the seat vertically whenever a rail-marked
|
|
826
|
+
occupant is inside it, so each circle keeps the 36px rail column the
|
|
827
|
+
shell uses elsewhere; the wide column never matches this rule. The anchor
|
|
828
|
+
is this plugin's own `data-rail` marker, so the rule is an equal-value
|
|
829
|
+
no-op for a shell that already stacks its foot. */
|
|
830
|
+
:global([data-dsh-frame][data-sidebar-collapsed]) [class*='footerActions']:has([data-rail='rail']) {
|
|
901
831
|
flex-direction: column;
|
|
902
|
-
gap: 4px;
|
|
903
|
-
margin-top: 10px;
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
.updateNoteHeading {
|
|
907
|
-
margin: 0;
|
|
908
|
-
font-size: 13px;
|
|
909
|
-
font-weight: 600;
|
|
910
|
-
color: var(--dsw-alias-label-secondary);
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
.updateNoteList {
|
|
914
|
-
margin: 0;
|
|
915
|
-
padding-left: 18px;
|
|
916
|
-
display: flex;
|
|
917
|
-
flex-direction: column;
|
|
918
|
-
gap: 4px;
|
|
919
|
-
font-size: 13px;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
.updateNoteItem {
|
|
923
|
-
color: var(--dsw-alias-label-primary);
|
|
924
|
-
overflow-wrap: anywhere;
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
.updateVersions {
|
|
928
|
-
margin-top: 12px;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
.updateVersionsSummary {
|
|
932
|
-
cursor: pointer;
|
|
933
|
-
color: var(--dsw-alias-label-secondary);
|
|
934
|
-
font-size: 12px;
|
|
935
|
-
user-select: none;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
.updateActions {
|
|
939
|
-
display: flex;
|
|
940
|
-
justify-content: flex-end;
|
|
941
|
-
margin-top: 16px;
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
.updateRetry {
|
|
945
|
-
display: inline-flex;
|
|
946
832
|
align-items: center;
|
|
947
|
-
gap:
|
|
948
|
-
border: none;
|
|
949
|
-
border-radius: 10px;
|
|
950
|
-
padding: 8px 14px;
|
|
951
|
-
background: var(--dsw-alias-interactive-bg-hover);
|
|
952
|
-
color: var(--dsw-alias-label-primary);
|
|
953
|
-
font-size: 13px;
|
|
954
|
-
cursor: pointer;
|
|
955
|
-
transition: background-color 120ms ease, color 120ms ease;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
.updateRetry:hover {
|
|
959
|
-
background: var(--dsw-alias-interactive-bg-active);
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
@media (prefers-reduced-motion: reduce) {
|
|
963
|
-
.updateRetry {
|
|
964
|
-
transition: none;
|
|
965
|
-
}
|
|
833
|
+
gap: 4px;
|
|
966
834
|
}
|