@linxin666/dsh-remote-web-ui 0.3.22 → 0.3.24
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 +4 -0
- package/README.zh.md +4 -0
- package/lib/client.js +193 -20
- package/lib/client.js.map +1 -1
- package/lib/index.js +181 -34
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +13 -18
- package/lib/types/client/mobile-adapt.d.ts.map +1 -1
- package/lib/types/client/mobile-adapt.js +40 -6
- package/lib/types/client/plugin-card-seat.d.ts +97 -0
- package/lib/types/client/plugin-card-seat.d.ts.map +1 -0
- package/lib/types/client/plugin-card-seat.js +141 -0
- package/lib/types/client/remote-channel.d.ts +35 -0
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +55 -0
- package/lib/types/console-output.d.ts +41 -0
- package/lib/types/console-output.d.ts.map +1 -0
- package/lib/types/console-output.js +89 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +26 -22
- package/lib/types/public-base.d.ts +67 -0
- package/lib/types/public-base.d.ts.map +1 -0
- package/lib/types/public-base.js +110 -0
- package/lib/types/remote-channel-boot.d.ts +5 -2
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +28 -2
- package/lib/types/remote-channel-rules.d.ts +4 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +2 -0
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +5 -12
- package/package.json +1 -1
- package/src/client/index.ts +13 -17
- package/src/client/mobile-adapt.ts +39 -5
- package/src/client/plugin-card-seat.ts +185 -0
- package/src/client/remote-channel.ts +64 -0
- package/src/console-output.ts +97 -0
- package/src/index.ts +26 -22
- package/src/public-base.ts +122 -0
- package/src/remote-channel-boot.ts +28 -2
- package/src/remote-channel-rules.ts +6 -0
- package/src/update.ts +5 -10
package/lib/index.js
CHANGED
|
@@ -3158,6 +3158,153 @@ var TunnelManager = class {
|
|
|
3158
3158
|
} catch {}
|
|
3159
3159
|
}
|
|
3160
3160
|
};
|
|
3161
|
+
//#endregion
|
|
3162
|
+
//#region src/public-base.ts
|
|
3163
|
+
/**
|
|
3164
|
+
* The public base the pairing fence trusts while a tunnel is not running
|
|
3165
|
+
* (issue #1547).
|
|
3166
|
+
*
|
|
3167
|
+
* The fence reads exactly one public host: `service.publicBaseUrl`. Dropping
|
|
3168
|
+
* it the instant the tunnel leaves `running` makes every public request from
|
|
3169
|
+
* the address still printed in the QR code answer 403 — including
|
|
3170
|
+
* `/api/pair/status`, which exists to be callable before pairing. Two of the
|
|
3171
|
+
* three tunnel modes never lose their host: a named tunnel keeps its fixed
|
|
3172
|
+
* dashboard hostname, and a registered relay keeps its stable subdomain. A
|
|
3173
|
+
* quick tunnel does mint a new hostname on restart, so its previous host is
|
|
3174
|
+
* kept only for a bounded grace window: Cloudflare's edge can still deliver a
|
|
3175
|
+
* connection the phone already opened, and a mobile network reconnects
|
|
3176
|
+
* constantly, so an instant drop turns the phone's retry into a silent 403.
|
|
3177
|
+
* @module @linxin666/dsh-remote-web-ui/public-base
|
|
3178
|
+
*/
|
|
3179
|
+
/** How long a quick-tunnel host stays trusted after the tunnel stops running. */
|
|
3180
|
+
const PUBLIC_BASE_GRACE_MS = 6e4;
|
|
3181
|
+
/**
|
|
3182
|
+
* Owns the trusted public base across tunnel and relay transitions.
|
|
3183
|
+
*
|
|
3184
|
+
* The keeper never decides on its own to widen trust: it only postpones
|
|
3185
|
+
* dropping an address the operator (named/relay) or the tunnel (quick) had
|
|
3186
|
+
* already published, and it forgets it once the tunnel comes back with the
|
|
3187
|
+
* address that actually serves the QR.
|
|
3188
|
+
*/
|
|
3189
|
+
var PublicBaseKeeper = class {
|
|
3190
|
+
onChange;
|
|
3191
|
+
graceMs;
|
|
3192
|
+
mode = "off";
|
|
3193
|
+
tunnelUrl;
|
|
3194
|
+
relayUrl;
|
|
3195
|
+
graceTimer;
|
|
3196
|
+
/**
|
|
3197
|
+
* @param onChange - receives the base the fence should trust from now on.
|
|
3198
|
+
* @param graceMs - how long a quick-tunnel host outlives its tunnel.
|
|
3199
|
+
*/
|
|
3200
|
+
constructor(onChange, graceMs = PUBLIC_BASE_GRACE_MS) {
|
|
3201
|
+
this.onChange = onChange;
|
|
3202
|
+
this.graceMs = graceMs;
|
|
3203
|
+
}
|
|
3204
|
+
/** The base a fresh fence read should trust: the stable relay origin wins. */
|
|
3205
|
+
current() {
|
|
3206
|
+
return this.relayUrl ?? this.tunnelUrl;
|
|
3207
|
+
}
|
|
3208
|
+
/** The mode that currently owns the public base. */
|
|
3209
|
+
setMode(mode) {
|
|
3210
|
+
this.mode = mode;
|
|
3211
|
+
}
|
|
3212
|
+
/** The last quick-tunnel URL, for callers that must announce it downstream. */
|
|
3213
|
+
quickUrl() {
|
|
3214
|
+
return this.tunnelUrl;
|
|
3215
|
+
}
|
|
3216
|
+
/** A relay registration result; undefined withdraws the stable origin. */
|
|
3217
|
+
setRelay(url) {
|
|
3218
|
+
this.relayUrl = url;
|
|
3219
|
+
this.emit();
|
|
3220
|
+
}
|
|
3221
|
+
/** The tunnel reported a live URL: it supersedes any pending grace. */
|
|
3222
|
+
markRunning(url) {
|
|
3223
|
+
this.cancelGrace();
|
|
3224
|
+
this.tunnelUrl = url;
|
|
3225
|
+
this.emit();
|
|
3226
|
+
}
|
|
3227
|
+
/**
|
|
3228
|
+
* The tunnel left `running` (starting again, or failed). A named tunnel's
|
|
3229
|
+
* fixed hostname stays trusted; a quick tunnel's previous host stays
|
|
3230
|
+
* trusted for the grace window, then is dropped if nothing replaced it.
|
|
3231
|
+
*/
|
|
3232
|
+
markReconnecting() {
|
|
3233
|
+
if (this.mode === "named") return;
|
|
3234
|
+
this.startGrace();
|
|
3235
|
+
}
|
|
3236
|
+
/** Re-publish the current base after a branch change that did not move it. */
|
|
3237
|
+
refresh() {
|
|
3238
|
+
this.emit();
|
|
3239
|
+
}
|
|
3240
|
+
/** Forget everything (teardown, or a mode change that stops the tunnel). */
|
|
3241
|
+
reset() {
|
|
3242
|
+
this.cancelGrace();
|
|
3243
|
+
this.tunnelUrl = void 0;
|
|
3244
|
+
this.relayUrl = void 0;
|
|
3245
|
+
this.emit();
|
|
3246
|
+
}
|
|
3247
|
+
/** Cancel the pending grace timer; the keeper keeps its last base. */
|
|
3248
|
+
dispose() {
|
|
3249
|
+
this.cancelGrace();
|
|
3250
|
+
}
|
|
3251
|
+
startGrace() {
|
|
3252
|
+
if (this.tunnelUrl === void 0) return;
|
|
3253
|
+
if (this.graceTimer !== void 0) return;
|
|
3254
|
+
this.graceTimer = setTimeout(() => {
|
|
3255
|
+
this.graceTimer = void 0;
|
|
3256
|
+
this.tunnelUrl = void 0;
|
|
3257
|
+
this.emit();
|
|
3258
|
+
}, this.graceMs);
|
|
3259
|
+
}
|
|
3260
|
+
cancelGrace() {
|
|
3261
|
+
if (this.graceTimer === void 0) return;
|
|
3262
|
+
clearTimeout(this.graceTimer);
|
|
3263
|
+
this.graceTimer = void 0;
|
|
3264
|
+
}
|
|
3265
|
+
emit() {
|
|
3266
|
+
this.onChange(this.current());
|
|
3267
|
+
}
|
|
3268
|
+
};
|
|
3269
|
+
//#endregion
|
|
3270
|
+
//#region src/console-output.ts
|
|
3271
|
+
/** Count the U+FFFD replacement characters one candidate decode produced. */
|
|
3272
|
+
function replacementCount(text) {
|
|
3273
|
+
let count = 0;
|
|
3274
|
+
for (const character of text) if (character === "�") count += 1;
|
|
3275
|
+
return count;
|
|
3276
|
+
}
|
|
3277
|
+
/**
|
|
3278
|
+
* Decode one console byte buffer. Strict UTF-8 wins whenever it is valid;
|
|
3279
|
+
* otherwise the bytes are not UTF-8 at all and the console wrote its own code
|
|
3280
|
+
* page, so the candidate that loses the least text is taken.
|
|
3281
|
+
* @param bytes - the captured bytes.
|
|
3282
|
+
* @returns the decoded text.
|
|
3283
|
+
*/
|
|
3284
|
+
function decodeConsoleBytes(bytes) {
|
|
3285
|
+
try {
|
|
3286
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
3287
|
+
} catch {
|
|
3288
|
+
let best;
|
|
3289
|
+
let bestLoss = Number.POSITIVE_INFINITY;
|
|
3290
|
+
for (const label of ["gbk", "utf-8"]) {
|
|
3291
|
+
let text;
|
|
3292
|
+
try {
|
|
3293
|
+
text = new TextDecoder(label).decode(bytes);
|
|
3294
|
+
} catch {
|
|
3295
|
+
continue;
|
|
3296
|
+
}
|
|
3297
|
+
const loss = replacementCount(text);
|
|
3298
|
+
if (loss < bestLoss) {
|
|
3299
|
+
best = text;
|
|
3300
|
+
bestLoss = loss;
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
/* v8 ignore next 3 -- reachable only without the legacy code page AND without the lenient pass */
|
|
3304
|
+
if (best === void 0) return Buffer.from(bytes).toString("utf8");
|
|
3305
|
+
return best;
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3161
3308
|
/** The aggregate package that is the canonical update entry point. */
|
|
3162
3309
|
const AGGREGATE_PACKAGE = "@linxin666/dsh-web-all";
|
|
3163
3310
|
/** Fallback anchor: this plugin's own package when the aggregate is absent. */
|
|
@@ -3512,18 +3659,12 @@ const WIN_CMD_MISSING_RE = /not recognized as an internal or external command|
|
|
|
3512
3659
|
/**
|
|
3513
3660
|
* Decode process output with tolerant decoding:
|
|
3514
3661
|
* Defaults to UTF-8; if invalid byte sequences are encountered (common on
|
|
3515
|
-
* Windows consoles using code page 936/GBK), falls back to
|
|
3662
|
+
* Windows consoles using code page 936/GBK), falls back to the code page that
|
|
3663
|
+
* loses the least text. Bytes are decoded once per whole chunk by the caller's
|
|
3664
|
+
* accumulator — decoding per \`data\` event would split multi-byte characters.
|
|
3516
3665
|
*/
|
|
3517
3666
|
function decodeProcessChunk(chunk) {
|
|
3518
|
-
|
|
3519
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(chunk);
|
|
3520
|
-
} catch {
|
|
3521
|
-
try {
|
|
3522
|
-
return new TextDecoder("gbk").decode(chunk);
|
|
3523
|
-
} catch {
|
|
3524
|
-
return chunk.toString("utf8");
|
|
3525
|
-
}
|
|
3526
|
-
}
|
|
3667
|
+
return decodeConsoleBytes(chunk);
|
|
3527
3668
|
}
|
|
3528
3669
|
/**
|
|
3529
3670
|
* Bypass for pnpm 11's supply-chain gate: `minimumReleaseAge` (default 24 h)
|
|
@@ -3859,7 +4000,9 @@ const REMOTE_CHANNEL_RULES = {
|
|
|
3859
4000
|
],
|
|
3860
4001
|
deviceHeader: REMOTE_DEVICE_HEADER,
|
|
3861
4002
|
deviceKey: "dsh-remote-device",
|
|
3862
|
-
deviceQuery: REMOTE_DEVICE_QUERY
|
|
4003
|
+
deviceQuery: REMOTE_DEVICE_QUERY,
|
|
4004
|
+
uploadPath: "/api/session/uploadFileBinary",
|
|
4005
|
+
uploadHookGlobal: "__DSH_FILE_UPLOAD__"
|
|
3863
4006
|
};
|
|
3864
4007
|
/** The window global the boot patch publishes its seat under. */
|
|
3865
4008
|
const REMOTE_CHANNEL_BOOT_GLOBAL = "__DSH_REMOTE_CHANNEL_BOOT__";
|
|
@@ -3884,8 +4027,11 @@ const REMOTE_CHANNEL_BOOT_GLOBAL = "__DSH_REMOTE_CHANNEL_BOOT__";
|
|
|
3884
4027
|
* transport hook `__DSH_TRANSPORT__ = { ownsHost: true }` before the
|
|
3885
4028
|
* connection plugin reads it: the paired remote desktop gets the full
|
|
3886
4029
|
* settings/credentials/presets surface, and every call still rides the
|
|
3887
|
-
* gated /remote channel.
|
|
3888
|
-
*
|
|
4030
|
+
* gated /remote channel. It finally publishes the official pre-Cordis
|
|
4031
|
+
* upload hook (`__DSH_FILE_UPLOAD__`), because the background upload
|
|
4032
|
+
* transport otherwise runs inside a Web Worker whose own globals the
|
|
4033
|
+
* main-thread rewrite cannot reach (issue #1580). The script self-skips on
|
|
4034
|
+
* loopback origins and never throws.
|
|
3889
4035
|
* @module @linxin666/dsh-remote-web-ui/remote-channel-boot
|
|
3890
4036
|
*/
|
|
3891
4037
|
/**
|
|
@@ -3896,7 +4042,7 @@ const REMOTE_CHANNEL_BOOT_GLOBAL = "__DSH_REMOTE_CHANNEL_BOOT__";
|
|
|
3896
4042
|
function buildRemoteChannelBootScript(rules = REMOTE_CHANNEL_RULES) {
|
|
3897
4043
|
const json = JSON.stringify(rules);
|
|
3898
4044
|
const seat = JSON.stringify(REMOTE_CHANNEL_BOOT_GLOBAL);
|
|
3899
|
-
return "(function(){try{var w=window,loc=w.location,h=loc.hostname;if(h==='localhost'||h==='::1'||/^127(\\.\\d{1,3}){3}$/.test(h))return;try{if(w.__DSH_TRANSPORT__===undefined)w.__DSH_TRANSPORT__={};w.__DSH_TRANSPORT__.ownsHost=true}catch(e){}var R=" + json + ";function rdv(){try{return w.sessionStorage.getItem(R.deviceKey)}catch(e){return null}}function att(init){var dv=rdv();if(dv===null)return init;var h=init&&init.headers;if(typeof Headers!==\"undefined\"&&h instanceof Headers){try{h.set(R.deviceHeader,dv)}catch(e){}return init}if(typeof h===\"object\"&&h!==null){var o={};for(var k in h)o[k]=h[k];o[R.deviceHeader]=dv;return Object.assign({},init,{headers:o})}return init}function sf(p){if(p.indexOf(R.pairPrefix)===0)return false;if(p.indexOf(R.updatePrefix)===0)return false;if(p===R.settingsBridgePrefix||p.indexOf(R.settingsBridgePrefix+\"/\")===0)return false;if(p.indexOf(R.apiPrefix)===0)return true;if(p.indexOf(R.sidebarPrefix)===0||p===\"/sidebar\")return true;if(p.indexOf(R.gitPrefix)===0||p===\"/git\")return true;if(p.indexOf(R.petPrefix)===0||p===\"/pet\")return true;return false}function sw(p){return R.wsPaths.indexOf(p)!==-1}function rp(p){return R.remotePrefix+p}function so(u){return u.origin===loc.origin}function rr(raw){var u;try{u=new URL(raw,loc.href)}catch(e){return raw}if(u.origin!==loc.origin)return raw;if(!sf(u.pathname))return raw;u.pathname=rp(u.pathname);if(raw.charAt(0)===\"/\"&&raw.charAt(1)!==\"/\")return u.pathname+u.search+u.hash;return u.href}function uc(v){if(typeof v!==\"object\"||v===null)return undefined;var n=v.result;if(typeof n===\"object\"&&n!==null){var e=n.error;if(typeof e===\"object\"&&e!==null&&typeof e.code===\"string\")return e.code}var t=v.error;if(typeof t===\"object\"&&t!==null&&typeof t.code===\"string\")return t.code;return undefined}var seat={onUnpaired:null,onPaired:null,pendingUnpaired:false,restore:function(){}};function denied(res){if(res.status!==403)return false;return res.clone().json().then(function(b){return uc(b)===\"unpaired\"}).catch(function(){return false})}function signal(unpaired){if(unpaired){if(seat.onUnpaired)seat.onUnpaired();else seat.pendingUnpaired=true}else{seat.pendingUnpaired=false;if(seat.onPaired)seat.onPaired()}}var of=w.fetch;w.fetch=function(input,init){var raw=typeof input===\"string\"||input instanceof URL?input.toString():input.url;var url=new URL(raw,loc.href);if(so(url)&&sf(url.pathname)){var next=new URL(url);next.pathname=rp(url.pathname);var target=typeof input===\"string\"||input instanceof URL?next.toString():new Request(next,input);return Promise.resolve(of.call(w,target,att(init))).then(function(res){void Promise.resolve(denied(res)).then(signal);return res})}return of.call(w,input,init)};var OW=w.WebSocket;w.WebSocket=function(url,protocols){var p=new URL(url.toString(),loc.href);var o=p.protocol===\"wss:\"?\"https://\"+p.host:p.protocol===\"ws:\"?\"http://\"+p.host:\"\";if(o!==\"\"&&o===loc.origin&&sw(p.pathname)){var nx=new URL(p);nx.pathname=rp(p.pathname);var dvv=rdv();if(dvv!==null)nx.searchParams.set(R.deviceQuery,dvv);return protocols!==undefined?new OW(nx,protocols):new OW(nx)}return protocols!==undefined?new OW(url,protocols):new OW(url)};w.WebSocket.prototype=OW.prototype;w.WebSocket.CONNECTING=OW.CONNECTING;w.WebSocket.OPEN=OW.OPEN;w.WebSocket.CLOSING=OW.CLOSING;w.WebSocket.CLOSED=OW.CLOSED;var OE=w.EventSource;if(OE!==undefined){w.EventSource=function(url,cfg){var p=new URL(url.toString(),loc.href);if(so(p)&&sf(p.pathname)){var nx=new URL(p);nx.pathname=rp(p.pathname);var dvv=rdv();if(dvv!==null)nx.searchParams.set(R.deviceQuery,dvv);return new OE(nx,cfg)}return new OE(url,cfg)};w.EventSource.prototype=OE.prototype}var restores=[];function patchSrc(C){if(C===undefined)return;var d=Object.getOwnPropertyDescriptor(C.prototype,\"src\");if(d===undefined||d.configurable===false||d.set===undefined)return;var os=d.set;Object.defineProperty(C.prototype,\"src\",{configurable:true,enumerable:d.enumerable!==false,get:d.get,set:function(v){os.call(this,rr(String(v)))}});restores.push(function(){Object.defineProperty(C.prototype,\"src\",d)})}patchSrc(w.HTMLImageElement);patchSrc(w.HTMLScriptElement);patchSrc(w.HTMLIFrameElement);seat.restore=function(){w.fetch=of;w.WebSocket=OW;if(OE!==undefined)w.EventSource=OE;for(var i=0;i<restores.length;i++)restores[i]();try{delete w[" + seat + "]}catch(e){w[" + seat + "]=undefined}};w[" + seat + "]=seat" + buildBootWatchdogScript() + "}catch(e){}})();";
|
|
4045
|
+
return "(function(){try{var w=window,loc=w.location,h=loc.hostname;if(h==='localhost'||h==='::1'||/^127(\\.\\d{1,3}){3}$/.test(h))return;try{if(w.__DSH_TRANSPORT__===undefined)w.__DSH_TRANSPORT__={};w.__DSH_TRANSPORT__.ownsHost=true}catch(e){}var R=" + json + ";function rdv(){try{return w.sessionStorage.getItem(R.deviceKey)}catch(e){return null}}function att(init){var dv=rdv();if(dv===null)return init;var h=init&&init.headers;if(typeof Headers!==\"undefined\"&&h instanceof Headers){try{h.set(R.deviceHeader,dv)}catch(e){}return init}if(typeof h===\"object\"&&h!==null){var o={};for(var k in h)o[k]=h[k];o[R.deviceHeader]=dv;return Object.assign({},init,{headers:o})}return init}function sf(p){if(p.indexOf(R.pairPrefix)===0)return false;if(p.indexOf(R.updatePrefix)===0)return false;if(p===R.settingsBridgePrefix||p.indexOf(R.settingsBridgePrefix+\"/\")===0)return false;if(p.indexOf(R.apiPrefix)===0)return true;if(p.indexOf(R.sidebarPrefix)===0||p===\"/sidebar\")return true;if(p.indexOf(R.gitPrefix)===0||p===\"/git\")return true;if(p.indexOf(R.petPrefix)===0||p===\"/pet\")return true;return false}function sw(p){return R.wsPaths.indexOf(p)!==-1}function rp(p){return R.remotePrefix+p}function so(u){return u.origin===loc.origin}function rr(raw){var u;try{u=new URL(raw,loc.href)}catch(e){return raw}if(u.origin!==loc.origin)return raw;if(!sf(u.pathname))return raw;u.pathname=rp(u.pathname);if(raw.charAt(0)===\"/\"&&raw.charAt(1)!==\"/\")return u.pathname+u.search+u.hash;return u.href}function uc(v){if(typeof v!==\"object\"||v===null)return undefined;var n=v.result;if(typeof n===\"object\"&&n!==null){var e=n.error;if(typeof e===\"object\"&&e!==null&&typeof e.code===\"string\")return e.code}var t=v.error;if(typeof t===\"object\"&&t!==null&&typeof t.code===\"string\")return t.code;return undefined}var seat={onUnpaired:null,onPaired:null,pendingUnpaired:false,restore:function(){}};function denied(res){if(res.status!==403)return false;return res.clone().json().then(function(b){return uc(b)===\"unpaired\"}).catch(function(){return false})}function signal(unpaired){if(unpaired){if(seat.onUnpaired)seat.onUnpaired();else seat.pendingUnpaired=true}else{seat.pendingUnpaired=false;if(seat.onPaired)seat.onPaired()}}var of=w.fetch;w.fetch=function(input,init){var raw=typeof input===\"string\"||input instanceof URL?input.toString():input.url;var url=new URL(raw,loc.href);if(so(url)&&sf(url.pathname)){var next=new URL(url);next.pathname=rp(url.pathname);var target=typeof input===\"string\"||input instanceof URL?next.toString():new Request(next,input);return Promise.resolve(of.call(w,target,att(init))).then(function(res){void Promise.resolve(denied(res)).then(signal);return res})}return of.call(w,input,init)};var OW=w.WebSocket;w.WebSocket=function(url,protocols){var p=new URL(url.toString(),loc.href);var o=p.protocol===\"wss:\"?\"https://\"+p.host:p.protocol===\"ws:\"?\"http://\"+p.host:\"\";if(o!==\"\"&&o===loc.origin&&sw(p.pathname)){var nx=new URL(p);nx.pathname=rp(p.pathname);var dvv=rdv();if(dvv!==null)nx.searchParams.set(R.deviceQuery,dvv);return protocols!==undefined?new OW(nx,protocols):new OW(nx)}return protocols!==undefined?new OW(url,protocols):new OW(url)};w.WebSocket.prototype=OW.prototype;w.WebSocket.CONNECTING=OW.CONNECTING;w.WebSocket.OPEN=OW.OPEN;w.WebSocket.CLOSING=OW.CLOSING;w.WebSocket.CLOSED=OW.CLOSED;var OE=w.EventSource;if(OE!==undefined){w.EventSource=function(url,cfg){var p=new URL(url.toString(),loc.href);if(so(p)&&sf(p.pathname)){var nx=new URL(p);nx.pathname=rp(p.pathname);var dvv=rdv();if(dvv!==null)nx.searchParams.set(R.deviceQuery,dvv);return new OE(nx,cfg)}return new OE(url,cfg)};w.EventSource.prototype=OE.prototype}var restores=[];function patchSrc(C){if(C===undefined)return;var d=Object.getOwnPropertyDescriptor(C.prototype,\"src\");if(d===undefined||d.configurable===false||d.set===undefined)return;var os=d.set;Object.defineProperty(C.prototype,\"src\",{configurable:true,enumerable:d.enumerable!==false,get:d.get,set:function(v){os.call(this,rr(String(v)))}});restores.push(function(){Object.defineProperty(C.prototype,\"src\",d)})}patchSrc(w.HTMLImageElement);patchSrc(w.HTMLScriptElement);patchSrc(w.HTMLIFrameElement);function uf(u,init){var raw=typeof u===\"string\"?u:u.href;if(typeof raw!==\"string\")return of.call(w,u,init||{});var q=new URL(raw,loc.href);if(so(q)&&q.pathname===R.uploadPath)return w.fetch(raw,init||{});return of.call(w,u,init||{})}try{if(w[R.uploadHookGlobal]===undefined)w[R.uploadHookGlobal]={fetch:uf}}catch(e){}seat.restore=function(){w.fetch=of;w.WebSocket=OW;if(OE!==undefined)w.EventSource=OE;for(var i=0;i<restores.length;i++)restores[i]();try{delete w[" + seat + "]}catch(e){w[" + seat + "]=undefined}};w[" + seat + "]=seat" + buildBootWatchdogScript() + "}catch(e){}})();";
|
|
3900
4046
|
}
|
|
3901
4047
|
/** Boot-watchdog latch: one self-reload per session while the boot is broken. */
|
|
3902
4048
|
const BOOT_WATCHDOG_KEY = "dsh-remote-boot-reload";
|
|
@@ -4079,17 +4225,20 @@ function applyImpl(ctx, config) {
|
|
|
4079
4225
|
const service = new PairingService(pairingConfigOf(resolved));
|
|
4080
4226
|
const tunnel = new TunnelManager();
|
|
4081
4227
|
let relayRegistrar;
|
|
4082
|
-
|
|
4083
|
-
|
|
4228
|
+
/**
|
|
4229
|
+
* The public base the pairing fence trusts. A tunnel reconnect must not
|
|
4230
|
+
* strip the host the QR still shows: named and relay hosts never change,
|
|
4231
|
+
* and a quick tunnel's host survives a bounded grace window (issue #1547).
|
|
4232
|
+
*/
|
|
4233
|
+
const publicBase = new PublicBaseKeeper((base) => {
|
|
4234
|
+
service.setPublicBaseUrl(base);
|
|
4235
|
+
});
|
|
4084
4236
|
/** The tunnel target the registrar last announced (dedupes sync re-runs). */
|
|
4085
4237
|
let relayAnnouncedFor;
|
|
4086
|
-
const setPublicBase = () => {
|
|
4087
|
-
service.setPublicBaseUrl(relayUrl ?? rawTunnelUrl);
|
|
4088
|
-
};
|
|
4089
4238
|
const disposeRelayRegistrar = (unregister = false) => {
|
|
4090
4239
|
const registrar = relayRegistrar;
|
|
4091
4240
|
relayRegistrar = void 0;
|
|
4092
|
-
|
|
4241
|
+
publicBase.setRelay(void 0);
|
|
4093
4242
|
relayAnnouncedFor = void 0;
|
|
4094
4243
|
if (registrar === void 0) return;
|
|
4095
4244
|
if (unregister) registrar.unregister().catch(() => void 0);
|
|
@@ -4100,10 +4249,9 @@ function applyImpl(ctx, config) {
|
|
|
4100
4249
|
if (relayRegistrar === void 0) try {
|
|
4101
4250
|
relayRegistrar = new RelayRegistrar(loadRelayIdentity(resolve().profile), (state) => {
|
|
4102
4251
|
service.setRelayStatus(state.state === "off" ? void 0 : state);
|
|
4103
|
-
if (state.state === "running")
|
|
4104
|
-
else if (state.state === "off")
|
|
4252
|
+
if (state.state === "running") publicBase.setRelay(state.url);
|
|
4253
|
+
else if (state.state === "off") publicBase.setRelay(void 0);
|
|
4105
4254
|
else if (state.state === "failed") console.warn(`remote-web-ui: relay registration failed (${state.error}) — the stable origin may serve its offline page until the retry lands`);
|
|
4106
|
-
setPublicBase();
|
|
4107
4255
|
});
|
|
4108
4256
|
} catch (error) {
|
|
4109
4257
|
console.warn(`remote-web-ui: relay registry unavailable (${error instanceof Error ? error.message : String(error)}) — the quick URL is the QR base`);
|
|
@@ -4119,8 +4267,7 @@ function applyImpl(ctx, config) {
|
|
|
4119
4267
|
tunnel.onPhase((info) => {
|
|
4120
4268
|
if (tunnelMode === "off") return;
|
|
4121
4269
|
if (info.phase === "running" && info.url !== void 0) {
|
|
4122
|
-
|
|
4123
|
-
setPublicBase();
|
|
4270
|
+
publicBase.markRunning(info.url);
|
|
4124
4271
|
service.setTunnelStatus({
|
|
4125
4272
|
state: "running",
|
|
4126
4273
|
url: info.url
|
|
@@ -4129,13 +4276,10 @@ function applyImpl(ctx, config) {
|
|
|
4129
4276
|
if (registrar !== void 0) announceRelay(registrar, info.url);
|
|
4130
4277
|
runPostureProbe();
|
|
4131
4278
|
} else if (info.phase === "starting") {
|
|
4132
|
-
|
|
4133
|
-
relayUrl = void 0;
|
|
4134
|
-
setPublicBase();
|
|
4279
|
+
publicBase.markReconnecting();
|
|
4135
4280
|
service.setTunnelStatus({ state: "starting" });
|
|
4136
4281
|
} else if (info.phase === "failed") {
|
|
4137
|
-
|
|
4138
|
-
setPublicBase();
|
|
4282
|
+
publicBase.markReconnecting();
|
|
4139
4283
|
service.setTunnelStatus(info.error === void 0 ? { state: "failed" } : {
|
|
4140
4284
|
state: "failed",
|
|
4141
4285
|
error: info.error
|
|
@@ -4144,6 +4288,7 @@ function applyImpl(ctx, config) {
|
|
|
4144
4288
|
});
|
|
4145
4289
|
ctx.effect(() => () => {
|
|
4146
4290
|
disposeRelayRegistrar();
|
|
4291
|
+
publicBase.dispose();
|
|
4147
4292
|
tunnel.dispose();
|
|
4148
4293
|
}, "remote-web-ui: auto tunnel");
|
|
4149
4294
|
if ((ctx.webServer.host === "0.0.0.0" || ctx.webServer.host === "127.0.0.1") && Number.isFinite(ctx.webServer.port)) {
|
|
@@ -4391,15 +4536,17 @@ function applyImpl(ctx, config) {
|
|
|
4391
4536
|
}
|
|
4392
4537
|
const plan = tunnelPlanOf(value, ctx.webServer.port);
|
|
4393
4538
|
tunnelMode = plan.mode;
|
|
4539
|
+
publicBase.setMode(plan.mode);
|
|
4540
|
+
const liveTunnelUrl = publicBase.quickUrl();
|
|
4394
4541
|
if (plan.mode !== "quick") {
|
|
4395
4542
|
disposeRelayRegistrar();
|
|
4396
|
-
if (plan.mode !== "named")
|
|
4543
|
+
if (plan.mode !== "named") publicBase.refresh();
|
|
4397
4544
|
} else if (value.relay === false) {
|
|
4398
4545
|
disposeRelayRegistrar(true);
|
|
4399
|
-
|
|
4400
|
-
} else if (
|
|
4546
|
+
publicBase.refresh();
|
|
4547
|
+
} else if (liveTunnelUrl !== void 0) {
|
|
4401
4548
|
const registrar = ensureRelayRegistrar();
|
|
4402
|
-
if (registrar !== void 0) announceRelay(registrar,
|
|
4549
|
+
if (registrar !== void 0) announceRelay(registrar, liveTunnelUrl);
|
|
4403
4550
|
}
|
|
4404
4551
|
if (plan.mode === "quick") {
|
|
4405
4552
|
for (const ignored of plan.ignored) console.warn(`remote-web-ui: autoTunnel is on — ignoring the configured ${ignored}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;AAelG,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;AAelG,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAoBrD,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACrE,YAAY,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AACnE,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAC7C,YAAY,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AAC/F,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACzD,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAErE,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,0CAA0C;QAC1C,MAAM,EAAE,SAAS,CAAA;KAClB;IAED,UAAU,OAAO;QACf;;;;WAIG;QACH;;;;;WAKG;QACH,oBAAoB,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,4BAA4B,CAAA;SAAE,CAAA;KAC3F;CACF;AAED,4GAA4G;AAC5G,MAAM,WAAW,uBAAuB;IACtC,oEAAoE;IACpE,IAAI,EAAE,OAAO,CAAA;CACd;AAED,mEAAmE;AACnE,MAAM,WAAW,4BAA4B;IAC3C,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,KAAK,CAAA;CACjB;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAYD,wCAAwC;AACxC,eAAO,MAAM,MAAM,UAA+D,CAAA;AAElF;;;GAGG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CA4Q9C"}
|
|
@@ -19,6 +19,7 @@ import { channelTransition, installRemoteChannel, isLoopbackHostname, remoteChan
|
|
|
19
19
|
import { FenceNotice } from "./FenceNotice.js";
|
|
20
20
|
import { reportDailyHeartbeat } from "./telemetry.js";
|
|
21
21
|
import { startMobileAdapt } from "./mobile-adapt.js";
|
|
22
|
+
import { installPluginCard } from "./plugin-card-seat.js";
|
|
22
23
|
/** Dictionary namespace owned by this plugin. */
|
|
23
24
|
const NS = 'remote';
|
|
24
25
|
/** Settings namespace the remote-control card edits (the Host plugin registers it). */
|
|
@@ -150,26 +151,20 @@ export function apply(ctx) {
|
|
|
150
151
|
};
|
|
151
152
|
});
|
|
152
153
|
// Plugin configuration card: one staged form over the `remote-web-ui`
|
|
153
|
-
// settings namespace, contributed to
|
|
154
|
+
// settings namespace, contributed to whichever plugin-card seat the running
|
|
155
|
+
// host declares (the family group's list seat, or the official keyed seat of
|
|
156
|
+
// the plugin-configuration tab when dsh-web-settings is not installed —
|
|
157
|
+
// issue #1589).
|
|
154
158
|
const remoteSettings = new RemoteSettingsCardController(settingsScope);
|
|
155
|
-
ctx
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
inject: () => remoteSettings.inject(),
|
|
163
|
-
}, RemoteSettingsCard);
|
|
164
|
-
return () => {
|
|
165
|
-
remoteSettings.dispose();
|
|
166
|
-
unregister();
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
return () => { };
|
|
171
|
-
}
|
|
159
|
+
installPluginCard(ctx, {
|
|
160
|
+
namespace: REMOTE_WEB_UI_NS,
|
|
161
|
+
id: 'remote-web-ui',
|
|
162
|
+
order: 90,
|
|
163
|
+
locale: NS,
|
|
164
|
+
inject: () => remoteSettings.inject(),
|
|
165
|
+
component: RemoteSettingsCard,
|
|
172
166
|
});
|
|
167
|
+
ctx.effect(() => () => { remoteSettings.dispose(); }, 'remote-web-ui: settings card');
|
|
173
168
|
// Phone-side boot flow + heartbeats. Loopback pages (the desktop) never
|
|
174
169
|
// heartbeat; the server ignores unpaired heartbeats anyway. Both run only
|
|
175
170
|
// while the plugin is enabled.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mobile-adapt.d.ts","sourceRoot":"","sources":["../../../src/client/mobile-adapt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAE7C,0EAA0E;AAC1E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,yDAAyD;IACzD,aAAa,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAClC,yDAAyD;IACzD,YAAY,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IACjC;;;;;;OAMG;IACH,SAAS,EAAE,CAAC,CAAC,GAAG,EAAE,SAAS,KAAK,MAAM,CAAC,GAAG,IAAI,CAAA;IAC9C,wEAAwE;IACxE,UAAU,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAA;IAC7B,4GAA4G;IAC5G,iBAAiB,IAAI,IAAI,CAAA;CAC1B;
|
|
1
|
+
{"version":3,"file":"mobile-adapt.d.ts","sourceRoot":"","sources":["../../../src/client/mobile-adapt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAE7C,0EAA0E;AAC1E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,yDAAyD;IACzD,aAAa,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAClC,yDAAyD;IACzD,YAAY,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IACjC;;;;;;OAMG;IACH,SAAS,EAAE,CAAC,CAAC,GAAG,EAAE,SAAS,KAAK,MAAM,CAAC,GAAG,IAAI,CAAA;IAC9C,wEAAwE;IACxE,UAAU,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAA;IAC7B,4GAA4G;IAC5G,iBAAiB,IAAI,IAAI,CAAA;CAC1B;AAmRD;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAw0BvC"}
|
|
@@ -40,6 +40,12 @@ const EFFORT_BTN_ID = 'dshRemoteEffortPick';
|
|
|
40
40
|
const COMPACT_CLASS = 'dsh-remote-compact-picker';
|
|
41
41
|
/** Body class while the header actions are seated in the tabs row. */
|
|
42
42
|
const HEADER_SEATED_CLASS = 'dsh-remote-header-seated';
|
|
43
|
+
/**
|
|
44
|
+
* Width the seated header actions paint over the tabs row, as a CSS variable
|
|
45
|
+
* on <html>. The row caps its own width with it (see the v80 rules) instead of
|
|
46
|
+
* reserving the space with padding.
|
|
47
|
+
*/
|
|
48
|
+
const HEADER_RESERVE_VAR = '--dsh-remote-header-actions-reserve';
|
|
43
49
|
/** Locale-dependent fast path for the official picker cells (zh/en). */
|
|
44
50
|
const PICKER_CELL_PATTERN = {
|
|
45
51
|
model: /模型|Model/, // i18n-allow: matches official picker cell text, not plugin copy
|
|
@@ -228,6 +234,20 @@ const ADAPT_CSS = [
|
|
|
228
234
|
// [class$="_tab"] misses the active tab (its class ends in "_tabActive")
|
|
229
235
|
// — containment so BOTH tabs match.
|
|
230
236
|
'[class$="_header"] [class$="_tabs"] [class*="_tab"]{font-size:12px;white-space:nowrap}',
|
|
237
|
+
// v80: the tabs row must give the painted actions their width up on its own.
|
|
238
|
+
// The v67 padding reservation could not do it: the row is a nowrap flex
|
|
239
|
+
// container, and once the tab labels plus their gaps overflow the reduced
|
|
240
|
+
// content box the flex items spill over the padding box, so the trailing
|
|
241
|
+
// tabs render underneath the actions (reported on a phone: a long agent
|
|
242
|
+
// preset label covered the last tabs). Cap the row against the reserve and
|
|
243
|
+
// let the labels scroll instead. The value rides a CSS variable on <html>
|
|
244
|
+
// because the update lands before React re-creates the row node, and an
|
|
245
|
+
// inline reservation on that node is dropped until the next sync tick.
|
|
246
|
+
// The 2px bottom padding keeps the active-tab underline inside the clipping
|
|
247
|
+
// box; the matching negative margin keeps the header height official.
|
|
248
|
+
`body.${HEADER_SEATED_CLASS} [class$="_header"] [class$="_tabs"]{max-width:max(0px,calc(100% - var(${HEADER_RESERVE_VAR},0px)));overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-webkit-overflow-scrolling:touch;padding-bottom:2px;margin-bottom:-2px}`,
|
|
249
|
+
`body.${HEADER_SEATED_CLASS} [class$="_header"] [class$="_tabs"]::-webkit-scrollbar{display:none}`,
|
|
250
|
+
`body.${HEADER_SEATED_CLASS} [class$="_header"] [class$="_tabs"] > [class*="_tab"]{flex:0 0 auto}`,
|
|
231
251
|
// Mobile scope: hide the plugin surfaces that do not fit a phone — the
|
|
232
252
|
// right-hand details column and every desktop-oriented tool surface. The
|
|
233
253
|
// list keys on the L2 semantic roots (data-dsh-plugin, ownership stays
|
|
@@ -775,9 +795,14 @@ export function startMobileAdapt() {
|
|
|
775
795
|
let dx = translate !== null ? parseFloat(translate[1] ?? '0') : 0;
|
|
776
796
|
let dy = translate !== null ? parseFloat(translate[2] ?? '0') : 0;
|
|
777
797
|
const actionsRect = actions.getBoundingClientRect();
|
|
778
|
-
|
|
779
|
-
//
|
|
780
|
-
|
|
798
|
+
// Horizontal: the actions' right edge lands on the header's content edge,
|
|
799
|
+
// NOT on the tabs row's right edge. The row caps its own width against the
|
|
800
|
+
// reserve below (v80), so anchoring to the row would let the pair walk
|
|
801
|
+
// leftwards on every measurement.
|
|
802
|
+
const headerRect = header.getBoundingClientRect();
|
|
803
|
+
const headerStyle = getComputedStyle(header);
|
|
804
|
+
const contentRight = headerRect.right - (parseFloat(headerStyle.paddingRight) || 0) - (parseFloat(headerStyle.borderRightWidth) || 0);
|
|
805
|
+
const rightDiff = contentRight - actionsRect.right;
|
|
781
806
|
if (Math.abs(rightDiff) >= 0.5)
|
|
782
807
|
dx = Math.round((dx + rightDiff) * 10) / 10;
|
|
783
808
|
// Vertical: the actions' text bottom lands on the tab text bottom.
|
|
@@ -798,18 +823,27 @@ export function startMobileAdapt() {
|
|
|
798
823
|
const next = `translate(${dx}px, ${dy}px)`;
|
|
799
824
|
if (actions.style.transform !== next)
|
|
800
825
|
actions.style.transform = next;
|
|
801
|
-
// Reserve the painted width so the tab labels never
|
|
826
|
+
// Reserve the painted width on the row itself, so the tab labels never
|
|
827
|
+
// slide under it. The value rides a CSS variable on <html>: React
|
|
828
|
+
// recreates the row node on re-render, which would drop an inline
|
|
829
|
+
// reservation until the next sync tick, and until then the labels render
|
|
830
|
+
// underneath the painted actions.
|
|
802
831
|
const reserve = `${Math.ceil(actionsRect.width) + 8}px`;
|
|
803
|
-
|
|
804
|
-
|
|
832
|
+
const rootStyle = document.documentElement.style;
|
|
833
|
+
if (rootStyle.getPropertyValue(HEADER_RESERVE_VAR) !== reserve)
|
|
834
|
+
rootStyle.setProperty(HEADER_RESERVE_VAR, reserve);
|
|
805
835
|
}
|
|
806
836
|
function unseatHeaderActions() {
|
|
807
837
|
document.body.classList.remove(HEADER_SEATED_CLASS);
|
|
838
|
+
document.documentElement.style.removeProperty(HEADER_RESERVE_VAR);
|
|
808
839
|
const header = document.querySelector('[class$="_header"]');
|
|
809
840
|
const tabs = header !== null ? header.querySelector('[class$="_tabs"]') : null;
|
|
810
841
|
const actions = header !== null ? header.querySelector('[class$="_titleCluster"] [class$="_headerActions"]') : null;
|
|
811
842
|
if (actions instanceof HTMLElement)
|
|
812
843
|
actions.style.transform = '';
|
|
844
|
+
// The v67 padding reservation is gone with the v80 cap; clearing it keeps a
|
|
845
|
+
// profile that hot-updated from an older build from keeping a stale inline
|
|
846
|
+
// width on the row.
|
|
813
847
|
if (tabs instanceof HTMLElement)
|
|
814
848
|
tabs.style.paddingRight = '';
|
|
815
849
|
if (headerObserver !== null) {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Family plugin-card seat.
|
|
3
|
+
*
|
|
4
|
+
* A family plugin contributes its settings card to whichever plugin-card seat
|
|
5
|
+
* the running host actually renders:
|
|
6
|
+
*
|
|
7
|
+
* - `web-ui.plugin.item` — the list seat declared by the dsh-web-settings
|
|
8
|
+
* group section (this family's own first-level "Web UI plugins" section);
|
|
9
|
+
* - `settings.plugin.item` — the official keyed seat of the harness's
|
|
10
|
+
* `ui-settings-plugins` tab, keyed by the settings namespace the card edits.
|
|
11
|
+
*
|
|
12
|
+
* SEAT SELECTION IS NOT A DECLARATION PROBE. The official `ui-settings-plugins`
|
|
13
|
+
* row belongs to the harness bundle and its `configurable` tab always declares
|
|
14
|
+
* `settings.plugin.item` before any external plugin's `apply()` runs, so
|
|
15
|
+
* "is the official seat declared?" answers yes even in the deployment whose
|
|
16
|
+
* whole point is the family group. Choosing on that probe sends every family
|
|
17
|
+
* card to the official Plugins tab and leaves the group's own section
|
|
18
|
+
* permanently empty — the family of reports where the section renders its
|
|
19
|
+
* heading and zero cards.
|
|
20
|
+
*
|
|
21
|
+
* The signal that actually distinguishes the two deployments is whether
|
|
22
|
+
* dsh-web-settings is loaded: it is the package that owns the group section and
|
|
23
|
+
* it publishes the `webUiSettings` service during `apply()`, which every
|
|
24
|
+
* family plugin already reads for its settings scope. Group loaded -> the family
|
|
25
|
+
* seat; group absent -> the official seat.
|
|
26
|
+
*
|
|
27
|
+
* The decision is re-evaluated on every `slots/changed` because the group may
|
|
28
|
+
* apply after this plugin (the family aggregate orders it first, a profile that
|
|
29
|
+
* installs the group separately need not): the initial contribution goes to the
|
|
30
|
+
* official seat, then moves to the family seat the moment the group's section
|
|
31
|
+
* registers. The entry is disposed before the replacement is registered, so a
|
|
32
|
+
* card is never in two seats at once.
|
|
33
|
+
*
|
|
34
|
+
* The shared tree has no client-SDK dependency, so this module reads its
|
|
35
|
+
* context through the structural shape below; callers pass the plugin's own
|
|
36
|
+
* `ctx`.
|
|
37
|
+
*/
|
|
38
|
+
/** The family list seat key. */
|
|
39
|
+
export declare const FAMILY_PLUGIN_CARD_SEAT = "web-ui.plugin.item";
|
|
40
|
+
/** The official keyed plugin-card seat key. */
|
|
41
|
+
export declare const OFFICIAL_PLUGIN_CARD_SEAT = "settings.plugin.item";
|
|
42
|
+
/** The service dsh-web-settings publishes while it is loaded. */
|
|
43
|
+
export declare const FAMILY_GROUP_SERVICE = "webUiSettings";
|
|
44
|
+
/** The slot-registry member this helper uses (structurally satisfied by ctx.slots). */
|
|
45
|
+
export interface PluginCardSlots {
|
|
46
|
+
/** Contribution of one card entry. */
|
|
47
|
+
register(options: never, component: never): unknown;
|
|
48
|
+
}
|
|
49
|
+
/** The slice of the client context a card contribution needs. */
|
|
50
|
+
export interface PluginCardContext {
|
|
51
|
+
slots: PluginCardSlots;
|
|
52
|
+
/** Service lookup; absent on a context double that only models the slots. */
|
|
53
|
+
get?(name: string): unknown;
|
|
54
|
+
/** Event subscription seat; absent on an event-less test double. */
|
|
55
|
+
on?(event: string, listener: (...args: never[]) => void): unknown;
|
|
56
|
+
}
|
|
57
|
+
/** Owner share of a plugin card (both seats supply nothing). */
|
|
58
|
+
export interface SettingsPluginItemOwnerProps {
|
|
59
|
+
/** Marker field: card owner props are intentionally empty. */
|
|
60
|
+
children?: never;
|
|
61
|
+
}
|
|
62
|
+
/** One family plugin's card contribution. */
|
|
63
|
+
export interface PluginCardSeat {
|
|
64
|
+
/** Settings namespace the card edits (the official seat's dispatch key). */
|
|
65
|
+
namespace: string;
|
|
66
|
+
/** Family list-seat entry id. */
|
|
67
|
+
id: string;
|
|
68
|
+
/** Family list-seat sort order. */
|
|
69
|
+
order?: number;
|
|
70
|
+
/** Family list-seat display label; the official keyed seat carries none. */
|
|
71
|
+
label?: () => string;
|
|
72
|
+
/** Locale namespace the card renders with. */
|
|
73
|
+
locale: string;
|
|
74
|
+
/** Business-face factory of the registration. */
|
|
75
|
+
inject?: () => object;
|
|
76
|
+
/**
|
|
77
|
+
* The card component. Its props type is the seat's composed shape, which is
|
|
78
|
+
* chosen at runtime, so the helper takes it erased; the registrant keeps its
|
|
79
|
+
* own precise typing at the call site.
|
|
80
|
+
*/
|
|
81
|
+
component: unknown;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Whether the family group (dsh-web-settings) is loaded in this page. The
|
|
85
|
+
* service is the group package's own contract, so the probe cannot be fooled
|
|
86
|
+
* by a harness release that starts declaring the official seat differently.
|
|
87
|
+
*/
|
|
88
|
+
export declare function familyGroupLoaded(ctx: PluginCardContext): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Contribute one family plugin card to the seat this host renders, following
|
|
91
|
+
* the group if it loads later. The entry is disposed and re-registered on a
|
|
92
|
+
* seat change, never duplicated.
|
|
93
|
+
* @param ctx - client context (its slot registry decides the seat).
|
|
94
|
+
* @param seat - the card contribution.
|
|
95
|
+
*/
|
|
96
|
+
export declare function installPluginCard(ctx: PluginCardContext, seat: PluginCardSeat): void;
|
|
97
|
+
//# sourceMappingURL=plugin-card-seat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-card-seat.d.ts","sourceRoot":"","sources":["../../../src/client/plugin-card-seat.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,gCAAgC;AAChC,eAAO,MAAM,uBAAuB,uBAAuB,CAAA;AAE3D,+CAA+C;AAC/C,eAAO,MAAM,yBAAyB,yBAAyB,CAAA;AAE/D,iEAAiE;AACjE,eAAO,MAAM,oBAAoB,kBAAkB,CAAA;AAEnD,uFAAuF;AACvF,MAAM,WAAW,eAAe;IAC9B,sCAAsC;IACtC,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,GAAG,OAAO,CAAA;CACpD;AAED,iEAAiE;AACjE,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,eAAe,CAAA;IACtB,6EAA6E;IAC7E,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;IAC3B,oEAAoE;IACpE,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,IAAI,GAAG,OAAO,CAAA;CAClE;AAED,gEAAgE;AAChE,MAAM,WAAW,4BAA4B;IAC3C,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,KAAK,CAAA;CACjB;AAED,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAA;IACjB,iCAAiC;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,mCAAmC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,MAAM,CAAA;IACpB,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAA;IACd,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,MAAM,CAAA;IACrB;;;;OAIG;IACH,SAAS,EAAE,OAAO,CAAA;CACnB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAUjE;AAWD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,iBAAiB,EAAE,IAAI,EAAE,cAAc,GAAG,IAAI,CA4DpF"}
|