@linxin666/dsh-remote-web-ui 0.4.3 → 0.4.5
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 +23 -8
- package/README.zh.md +22 -7
- package/lib/client.js +194 -140
- package/lib/client.js.map +1 -1
- package/lib/index.js +231 -23
- package/lib/types/client/PluginSettingsCard.d.ts +7 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +2 -2
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +2 -1
- package/lib/types/client/index.js +3 -3
- package/lib/types/client/locales.d.ts +0 -4
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +5 -9
- package/lib/types/client/mobile-adapt.d.ts +2 -2
- package/lib/types/client/mobile-adapt.d.ts.map +1 -1
- package/lib/types/client/mobile-adapt.js +163 -136
- package/lib/types/client/settings-form.d.ts +16 -0
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +27 -1
- package/lib/types/detached-work.d.ts +45 -0
- package/lib/types/detached-work.d.ts.map +1 -0
- package/lib/types/detached-work.js +54 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +67 -12
- package/lib/types/lan-bind.d.ts +21 -4
- package/lib/types/lan-bind.d.ts.map +1 -1
- package/lib/types/lan-bind.js +103 -17
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +43 -0
- package/lib/types/remote-api.d.ts +13 -1
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +14 -1
- package/package.json +19 -19
- package/src/client/PluginSettingsCard.tsx +9 -0
- package/src/client/RemoteSettingsCard.tsx +2 -0
- package/src/client/index.ts +3 -3
- package/src/client/locales.ts +5 -9
- package/src/client/mobile-adapt.ts +167 -138
- package/src/client/settings-card.module.css +31 -5
- package/src/client/settings-form.ts +27 -1
- package/src/detached-work.ts +57 -0
- package/src/index.ts +67 -12
- package/src/lan-bind.ts +111 -17
- package/src/pairing.ts +44 -1
- package/src/remote-api.ts +14 -1
package/lib/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
13
13
|
import { parseDocument } from "yaml";
|
|
14
14
|
import { EventEmitter } from "node:events";
|
|
15
15
|
import { Tunnel, bin, install } from "cloudflared";
|
|
16
|
+
import { AsyncResource } from "node:async_hooks";
|
|
16
17
|
//#region src/pairing.ts
|
|
17
18
|
/**
|
|
18
19
|
* Pairing state machine: one active one-time token, a device-session table,
|
|
@@ -479,7 +480,7 @@ var PairingService = class {
|
|
|
479
480
|
};
|
|
480
481
|
/** Structural equality over the snapshot's wire fields. */
|
|
481
482
|
function snapshotsEqual(a, b) {
|
|
482
|
-
return a.phase === b.phase && a.lanAvailable === b.lanAvailable && sameStrings(a.lanAddresses, b.lanAddresses) && a.publicUrl === b.publicUrl && tunnelEqual(a.tunnel, b.tunnel) && relayEqual(a.relay, b.relay) && a.tokenId === b.tokenId && a.tokenExpiresAt === b.tokenExpiresAt && a.deviceCount === b.deviceCount && a.onlineCount === b.onlineCount && devicesEqual(a.devices, b.devices);
|
|
483
|
+
return a.phase === b.phase && a.lanAvailable === b.lanAvailable && sameStrings(a.lanAddresses, b.lanAddresses) && a.publicUrl === b.publicUrl && tunnelEqual(a.tunnel, b.tunnel) && relayEqual(a.relay, b.relay) && postureEqual(a.posture, b.posture) && a.tokenId === b.tokenId && a.tokenExpiresAt === b.tokenExpiresAt && a.deviceCount === b.deviceCount && a.onlineCount === b.onlineCount && devicesEqual(a.devices, b.devices);
|
|
483
484
|
}
|
|
484
485
|
/** Per-device roster equality (order is pairing time). */
|
|
485
486
|
function devicesEqual(a, b) {
|
|
@@ -496,6 +497,42 @@ function tunnelEqual(a, b) {
|
|
|
496
497
|
function relayEqual(a, b) {
|
|
497
498
|
return a === b || a !== void 0 && b !== void 0 && a.state === b.state && a.url === b.url && a.error === b.error;
|
|
498
499
|
}
|
|
500
|
+
/**
|
|
501
|
+
* Posture equality. A probe round can change this frame while every other field
|
|
502
|
+
* stays put — the common case, since the probe re-runs on a settings change and
|
|
503
|
+
* on a tunnel reaching running, and a quick-tunnel restart churns the probed
|
|
504
|
+
* host set. Leaving the frame out of the comparison suppressed those emits, so
|
|
505
|
+
* the loopback SSE stream and the desktop panel could sit on a stale "exposed"
|
|
506
|
+
* (or stale "clean") verdict for as long as the phase and roster stayed put.
|
|
507
|
+
*/
|
|
508
|
+
function postureEqual(a, b) {
|
|
509
|
+
if (a === b) return true;
|
|
510
|
+
if (!isPostureFrame(a) || !isPostureFrame(b)) return false;
|
|
511
|
+
return a.checkedAt === b.checkedAt && hostsEqual(a.hosts, b.hosts);
|
|
512
|
+
}
|
|
513
|
+
/** Whether a value has the shape a posture frame is compared on. */
|
|
514
|
+
function isPostureFrame(value) {
|
|
515
|
+
return typeof value === "object" && value !== null;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Per-host posture equality (a round's verdicts, in probe order). Tolerates a
|
|
519
|
+
* frame that is not the expected shape: this runs inside the emit path, outside
|
|
520
|
+
* the listener error containment, so a malformed snapshot handed in by another
|
|
521
|
+
* host-fiber caller must degrade to "changed" instead of throwing out of the
|
|
522
|
+
* setter that produced it.
|
|
523
|
+
*/
|
|
524
|
+
function hostsEqual(a, b) {
|
|
525
|
+
if (a === b) return true;
|
|
526
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
|
527
|
+
return a.length === b.length && a.every((host, index) => {
|
|
528
|
+
const other = b[index];
|
|
529
|
+
return isPostureHost(host) && isPostureHost(other) && host.host === other.host && host.exposed === other.exposed;
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
/** Whether a value has the shape one probed host is compared on. */
|
|
533
|
+
function isPostureHost(value) {
|
|
534
|
+
return typeof value === "object" && value !== null;
|
|
535
|
+
}
|
|
499
536
|
/** Element-wise string list equality (interface order is meaningful). */
|
|
500
537
|
function sameStrings(a, b) {
|
|
501
538
|
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
@@ -2184,7 +2221,19 @@ function makeRemoteApiRoutes(deps) {
|
|
|
2184
2221
|
}];
|
|
2185
2222
|
}
|
|
2186
2223
|
/**
|
|
2187
|
-
* Map one outer upgrade URL onto the loopback path
|
|
2224
|
+
* Map one outer upgrade URL onto the loopback path, dropping the cookieless
|
|
2225
|
+
* device credential on the way in.
|
|
2226
|
+
*
|
|
2227
|
+
* The outer handshake carries the device id as `?device=<id>` because WebSocket
|
|
2228
|
+
* handshakes cannot carry headers from the Web API. That id is a live session
|
|
2229
|
+
* credential — it opens the full host API through this same channel — and the
|
|
2230
|
+
* inner leg is a request the rest of the loopback origin sees (on a WebSocket
|
|
2231
|
+
* leg, a raw handshake request line written to a loopback socket), so forwarding
|
|
2232
|
+
* it handed a working credential to every other observer of the local port for
|
|
2233
|
+
* no benefit: the gate above has already authenticated the caller, and no
|
|
2234
|
+
* upstream route reads the parameter (the gateway mux matches on path alone).
|
|
2235
|
+
* The rest of the query rides through unchanged, so a caller's own parameters
|
|
2236
|
+
* still reach the upstream.
|
|
2188
2237
|
*/
|
|
2189
2238
|
function upgradeInnerPath(reqUrl, fallbackPath) {
|
|
2190
2239
|
if (reqUrl === void 0 || reqUrl === "") return fallbackPath;
|
|
@@ -2196,6 +2245,7 @@ function upgradeInnerPath(reqUrl, fallbackPath) {
|
|
|
2196
2245
|
}
|
|
2197
2246
|
const inner = innerPathOf(url.pathname);
|
|
2198
2247
|
if (inner === void 0) return fallbackPath;
|
|
2248
|
+
url.searchParams.delete(REMOTE_DEVICE_QUERY);
|
|
2199
2249
|
return `${inner}${url.search}`;
|
|
2200
2250
|
}
|
|
2201
2251
|
/**
|
|
@@ -2697,16 +2747,100 @@ function readPatchContent(file) {
|
|
|
2697
2747
|
if (!existsSync(file)) return "";
|
|
2698
2748
|
return readFileSync(file, "utf8");
|
|
2699
2749
|
}
|
|
2700
|
-
/**
|
|
2701
|
-
|
|
2702
|
-
|
|
2750
|
+
/**
|
|
2751
|
+
* Split a block-sequence region into its top-level items. Only a column-0 `-`
|
|
2752
|
+
* starts an item: that is the shape every writer of this file produces - the
|
|
2753
|
+
* managed block itself and the YAML document API's block-style re-emission.
|
|
2754
|
+
* @param region - text between two line boundaries.
|
|
2755
|
+
* @returns the items in source order; text outside them is not returned.
|
|
2756
|
+
*/
|
|
2757
|
+
function blockItems(region) {
|
|
2758
|
+
const items = [];
|
|
2759
|
+
let start = -1;
|
|
2760
|
+
let offset = 0;
|
|
2761
|
+
for (const line of region.split("\n")) {
|
|
2762
|
+
if (line.startsWith("- ") || line === "-") {
|
|
2763
|
+
if (start !== -1) items.push({
|
|
2764
|
+
start,
|
|
2765
|
+
end: offset,
|
|
2766
|
+
text: region.slice(start, offset)
|
|
2767
|
+
});
|
|
2768
|
+
start = offset;
|
|
2769
|
+
}
|
|
2770
|
+
offset += line.length + 1;
|
|
2771
|
+
}
|
|
2772
|
+
if (start !== -1) items.push({
|
|
2773
|
+
start,
|
|
2774
|
+
end: region.length,
|
|
2775
|
+
text: region.slice(start)
|
|
2776
|
+
});
|
|
2777
|
+
return items;
|
|
2778
|
+
}
|
|
2779
|
+
/**
|
|
2780
|
+
* Whether one top-level item is the webserver row this plugin's block owns.
|
|
2781
|
+
* The row id is the whole signal: the markers declare everything inside them
|
|
2782
|
+
* managed, our block always writes `- id: webserver`, and a same-id row later
|
|
2783
|
+
* in the patch replaces this one wholesale anyway - so an id-webserver row
|
|
2784
|
+
* found inside our markers is this plugin's row (a hand-truncated block may
|
|
2785
|
+
* have lost its `name` line).
|
|
2786
|
+
* @param item - one top-level item's text.
|
|
2787
|
+
* @returns true when the item is the managed webserver row.
|
|
2788
|
+
*/
|
|
2789
|
+
function isManagedWebserverItem(item) {
|
|
2790
|
+
return /^- id:\s*['"]?webserver['"]?[ \t]*(?:\r?\n|$)/.test(item);
|
|
2791
|
+
}
|
|
2792
|
+
/**
|
|
2793
|
+
* The text between a block's BEGIN and END markers: from the character after
|
|
2794
|
+
* the BEGIN line's terminator to the start of the END marker's line (or to
|
|
2795
|
+
* the end of the content without an END marker).
|
|
2796
|
+
* @param content - the patch file text.
|
|
2797
|
+
* @param begin - index of the BEGIN marker.
|
|
2798
|
+
* @param end - index of the END marker, or -1 when the block is unterminated.
|
|
2799
|
+
* @returns the region's text.
|
|
2800
|
+
*/
|
|
2801
|
+
function managedRegion(content, begin, end) {
|
|
2802
|
+
const newlineAfterBegin = content.indexOf("\n", begin);
|
|
2803
|
+
const innerStart = newlineAfterBegin === -1 ? content.length : newlineAfterBegin + 1;
|
|
2804
|
+
const endLineStart = end === -1 ? content.length : content.lastIndexOf("\n", end - 1) + 1;
|
|
2805
|
+
return content.slice(innerStart, Math.max(innerStart, endLineStart));
|
|
2703
2806
|
}
|
|
2704
|
-
/**
|
|
2807
|
+
/**
|
|
2808
|
+
* Remove the managed block (its markers and the webserver row this plugin
|
|
2809
|
+
* wrote) from patch content, keeping every other row.
|
|
2810
|
+
*
|
|
2811
|
+
* The markers travel with the file through writers that append rows through
|
|
2812
|
+
* the YAML document API: the appended rows land at the end of the sequence,
|
|
2813
|
+
* which is BEFORE a trailing END comment, so the region between the markers
|
|
2814
|
+
* can hold rows this plugin never wrote. Deleting the whole region silently
|
|
2815
|
+
* removed the user's model, plugin and remote-control rows on the next rewrite
|
|
2816
|
+
* - the remote-control row carries the LAN toggle itself, so the feature reset
|
|
2817
|
+
* to `undefined` and the phone was locked out again after a restart (DSH
|
|
2818
|
+
* Desktop report, 2026-09-30). Only the managed item is removed here; every
|
|
2819
|
+
* foreign row is kept in place and the fresh block is appended after them (the
|
|
2820
|
+
* same-id row last wins).
|
|
2821
|
+
* @param content - the patch file text.
|
|
2822
|
+
* @returns the text with the markers and the managed row removed.
|
|
2823
|
+
*/
|
|
2705
2824
|
function stripManagedBlock(content) {
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2825
|
+
let text = content;
|
|
2826
|
+
for (;;) {
|
|
2827
|
+
const begin = text.indexOf(LAN_BIND_BLOCK_BEGIN);
|
|
2828
|
+
if (begin === -1) return text;
|
|
2829
|
+
const end = text.indexOf(LAN_BIND_BLOCK_END, begin);
|
|
2830
|
+
const blockStart = text.lastIndexOf("\n", begin - 1) + 1;
|
|
2831
|
+
const newlineAfterEnd = end === -1 ? -1 : text.indexOf("\n", end);
|
|
2832
|
+
const resumeAt = end === -1 ? text.length : newlineAfterEnd === -1 ? text.length : newlineAfterEnd + 1;
|
|
2833
|
+
const region = managedRegion(text, begin, end);
|
|
2834
|
+
let kept = "";
|
|
2835
|
+
let cursor = 0;
|
|
2836
|
+
for (const item of blockItems(region)) {
|
|
2837
|
+
if (!isManagedWebserverItem(item.text)) continue;
|
|
2838
|
+
kept += region.slice(cursor, item.start);
|
|
2839
|
+
cursor = item.end;
|
|
2840
|
+
}
|
|
2841
|
+
kept += region.slice(cursor);
|
|
2842
|
+
text = text.slice(0, blockStart) + kept + text.slice(resumeAt);
|
|
2843
|
+
}
|
|
2710
2844
|
}
|
|
2711
2845
|
/**
|
|
2712
2846
|
* Render the managed block for one bind state. The values are static: the
|
|
@@ -2778,10 +2912,11 @@ function managedBlock(host, port) {
|
|
|
2778
2912
|
function managedBindOf(content) {
|
|
2779
2913
|
const begin = content.indexOf(LAN_BIND_BLOCK_BEGIN);
|
|
2780
2914
|
if (begin === -1) return void 0;
|
|
2781
|
-
const
|
|
2782
|
-
const
|
|
2783
|
-
const
|
|
2784
|
-
const
|
|
2915
|
+
const region = managedRegion(content, begin, content.indexOf(LAN_BIND_BLOCK_END, begin));
|
|
2916
|
+
const items = blockItems(region).filter((item) => isManagedWebserverItem(item.text));
|
|
2917
|
+
const scope = items.length > 0 ? items.map((item) => item.text).join("") : region;
|
|
2918
|
+
const hostMatch = /host:\s*'([^']+)'/.exec(scope);
|
|
2919
|
+
const portMatch = /port:\s*(\d+)/.exec(scope);
|
|
2785
2920
|
return {
|
|
2786
2921
|
host: hostMatch?.[1] ?? "",
|
|
2787
2922
|
port: portMatch !== null ? Number(portMatch[1]) : void 0
|
|
@@ -2802,9 +2937,10 @@ function lanBindState(profile, home = dshHome()) {
|
|
|
2802
2937
|
* patch file is preserved; the block is rewritten atomically (unique temp
|
|
2803
2938
|
* file + rename, so concurrent writers can never rename a half-written
|
|
2804
2939
|
* file) with the original file's permissions preserved. A hand-truncated
|
|
2805
|
-
* unterminated block (BEGIN marker without END
|
|
2806
|
-
*
|
|
2807
|
-
* never stack a second webserver row onto the
|
|
2940
|
+
* unterminated block (BEGIN marker without END) is consumed by
|
|
2941
|
+
* stripManagedBlock as well - markers and managed row removed, foreign rows
|
|
2942
|
+
* kept - so the rewrite can never stack a second webserver row onto the
|
|
2943
|
+
* orphan.
|
|
2808
2944
|
*
|
|
2809
2945
|
* The written file is always exactly one valid YAML document: a base the
|
|
2810
2946
|
* plugin manager left in flow style is re-emitted as a block sequence before
|
|
@@ -2812,9 +2948,7 @@ function lanBindState(profile, home = dshHome()) {
|
|
|
2812
2948
|
*/
|
|
2813
2949
|
function writeLanBind(host, port, profile, home = dshHome()) {
|
|
2814
2950
|
const file = profilePatchFile(profile, home);
|
|
2815
|
-
const
|
|
2816
|
-
const orphanBegin = stripped.indexOf(LAN_BIND_BLOCK_BEGIN);
|
|
2817
|
-
const content = mergePatchContent((orphanBegin === -1 ? stripped : stripped.slice(0, orphanBegin)).trimEnd().replace(/\[\s*\]\s*$/, "").trimEnd(), managedBlock(host, port), file);
|
|
2951
|
+
const content = mergePatchContent(stripManagedBlock(readPatchContent(file)).trimEnd().replace(/\[\s*\]\s*$/, "").trimEnd(), managedBlock(host, port), file);
|
|
2818
2952
|
const mode = existsSync(file) ? statSync(file).mode & 511 : 384;
|
|
2819
2953
|
mkdirSync(dirname(file), { recursive: true });
|
|
2820
2954
|
const temp = `${file}.remote-web-ui-tmp-${process.pid.toString(36)}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -3862,6 +3996,61 @@ function mountOnce(packageName, fn) {
|
|
|
3862
3996
|
return mount;
|
|
3863
3997
|
}
|
|
3864
3998
|
//#endregion
|
|
3999
|
+
//#region src/detached-work.ts
|
|
4000
|
+
/**
|
|
4001
|
+
* Run one piece of work outside the caller's async context.
|
|
4002
|
+
*
|
|
4003
|
+
* The Host performs a settings mutation inside `hmr.runExclusive`, which marks
|
|
4004
|
+
* the running async context with an AsyncLocalStorage store. Side effects that
|
|
4005
|
+
* live on that path - writing `cordis.patch.yml`, the very file the HMR config
|
|
4006
|
+
* watcher refreshes from - must not run under that mark, or the watcher's
|
|
4007
|
+
* refresh can re-enter the exclusive transaction and be refused with "HMR
|
|
4008
|
+
* transactions cannot be nested" (#1751, #1754).
|
|
4009
|
+
*
|
|
4010
|
+
* Deferring is NOT enough, and the earlier belt that this one replaces was
|
|
4011
|
+
* built on a disproven premise: Node propagates an AsyncLocalStorage store into
|
|
4012
|
+
* `setImmediate`, into `node:timers`, into promise continuations, and into any
|
|
4013
|
+
* AsyncResource scoped to the current async id. Measured on Node 24:
|
|
4014
|
+
*
|
|
4015
|
+
* ```
|
|
4016
|
+
* als.run(true, () => setImmediate(() => als.getStore())) // -> true
|
|
4017
|
+
* ```
|
|
4018
|
+
*
|
|
4019
|
+
* What DOES start clean is an AsyncResource whose `triggerAsyncId` is not the
|
|
4020
|
+
* transaction. A resource created once at module scope - that is, before any
|
|
4021
|
+
* transaction existed - carries no store, and everything it schedules inherits
|
|
4022
|
+
* that emptiness rather than the caller's mark:
|
|
4023
|
+
*
|
|
4024
|
+
* ```
|
|
4025
|
+
* const detached = new AsyncResource('...') // module scope
|
|
4026
|
+
* als.run(true, () => detached.runInAsyncScope(() => setImmediate(() => als.getStore())))
|
|
4027
|
+
* // -> undefined
|
|
4028
|
+
* ```
|
|
4029
|
+
*
|
|
4030
|
+
* @module
|
|
4031
|
+
*/
|
|
4032
|
+
/**
|
|
4033
|
+
* The escape hatch, created at module scope on purpose: a resource built while
|
|
4034
|
+
* a transaction is running would capture that transaction, so this single
|
|
4035
|
+
* instance is the only one the module ever needs. It carries no context of its
|
|
4036
|
+
* own, so every job run through it starts from the process's root context.
|
|
4037
|
+
*/
|
|
4038
|
+
const DETACHED = new AsyncResource("dsh-remote-web-ui:detached-work", { triggerAsyncId: 0 });
|
|
4039
|
+
/**
|
|
4040
|
+
* Run `work` outside the caller's AsyncLocalStorage context.
|
|
4041
|
+
*
|
|
4042
|
+
* The call itself is synchronous - this schedules, it does not defer. Work that
|
|
4043
|
+
* must not block the caller should schedule its own `setImmediate` inside
|
|
4044
|
+
* `work`, which then also starts clean because it inherits this resource's
|
|
4045
|
+
* empty context rather than the caller's.
|
|
4046
|
+
*
|
|
4047
|
+
* @param work - the side effect to detach from the caller's async context.
|
|
4048
|
+
* @returns whatever `work` returns.
|
|
4049
|
+
*/
|
|
4050
|
+
function runDetached(work) {
|
|
4051
|
+
return DETACHED.runInAsyncScope(work);
|
|
4052
|
+
}
|
|
4053
|
+
//#endregion
|
|
3865
4054
|
//#region src/remote-channel-boot.ts
|
|
3866
4055
|
/**
|
|
3867
4056
|
* Parse-time remote-channel boot patch (issue #987): the browser-half patch
|
|
@@ -4341,9 +4530,7 @@ function applyImpl(ctx, config) {
|
|
|
4341
4530
|
console.log(`remote-web-ui: the paired Web GUI is reachable on LAN at ${urls}`);
|
|
4342
4531
|
}
|
|
4343
4532
|
if (ctx.webServer.host === "0.0.0.0") console.warn("remote-web-ui: LAN-exposed bind — pairing gates the /remote channel; direct /api stays under the harness fence + browser auth (stop() does not revoke an already-redeemed browser credential)");
|
|
4344
|
-
const
|
|
4345
|
-
const value = resolve();
|
|
4346
|
-
service.config = pairingConfigOf(value);
|
|
4533
|
+
const applyLanBindWork = (value) => {
|
|
4347
4534
|
if (value.lanBind !== void 0) {
|
|
4348
4535
|
const startup = ctx.get("webStartup");
|
|
4349
4536
|
const desiredHost = desiredBindHost(value.lanBind === true, startup?.host);
|
|
@@ -4369,6 +4556,27 @@ function applyImpl(ctx, config) {
|
|
|
4369
4556
|
else console.error("remote-web-ui: the host firewall rule could not be updated (admin rights required on managed platforms)");
|
|
4370
4557
|
}
|
|
4371
4558
|
}
|
|
4559
|
+
};
|
|
4560
|
+
let lanBindWorkPending = false;
|
|
4561
|
+
const scheduleLanBindWork = () => {
|
|
4562
|
+
if (lanBindWorkPending) return;
|
|
4563
|
+
lanBindWorkPending = true;
|
|
4564
|
+
runDetached(() => setImmediate(() => {
|
|
4565
|
+
lanBindWorkPending = false;
|
|
4566
|
+
try {
|
|
4567
|
+
applyLanBindWork(resolve());
|
|
4568
|
+
} catch (error) {
|
|
4569
|
+
console.error(`remote-web-ui: the deferred lan-bind assertion failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4570
|
+
}
|
|
4571
|
+
}));
|
|
4572
|
+
};
|
|
4573
|
+
ctx.effect(() => () => {
|
|
4574
|
+
lanBindWorkPending = false;
|
|
4575
|
+
}, "remote-web-ui: lan-bind work");
|
|
4576
|
+
const sync = () => {
|
|
4577
|
+
const value = resolve();
|
|
4578
|
+
service.config = pairingConfigOf(value);
|
|
4579
|
+
scheduleLanBindWork();
|
|
4372
4580
|
const plan = tunnelPlanOf(value, ctx.webServer.port);
|
|
4373
4581
|
tunnelMode = plan.mode;
|
|
4374
4582
|
publicBase.setMode(plan.mode);
|
|
@@ -24,6 +24,13 @@ export interface PluginSettingsCardProps<TKey extends string = string> {
|
|
|
24
24
|
descriptionKey: TKey;
|
|
25
25
|
/** Optional rich header description rendered instead of the plain t(descriptionKey) text (the plain text stays as the tooltip). */
|
|
26
26
|
descriptionNode?: ReactNode;
|
|
27
|
+
/**
|
|
28
|
+
* Optional leading mark identifying the plugin. A card without one keeps the
|
|
29
|
+
* plain official header shape, so the chrome stays a readable sibling of the
|
|
30
|
+
* built-in PluginCard wherever a family plugin lands in the official bundle
|
|
31
|
+
* seat.
|
|
32
|
+
*/
|
|
33
|
+
icon?: ReactNode;
|
|
27
34
|
/** The card's form state: availability, writability, and what a save would do. */
|
|
28
35
|
state: CardShell;
|
|
29
36
|
/** Write every staged edit. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PluginSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/PluginSettingsCard.tsx"],"names":[],"mappings":"AACA;;;;;;;GAOG;AAEH,OAAO,EAAiF,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AACrH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAGnD,oGAAoG;AACpG,eAAO,MAAM,cAAc,kMAUjB,CAAA;AAEV,6CAA6C;AAC7C,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAA;AAEzD,oEAAoE;AACpE,MAAM,MAAM,eAAe,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,IAAI,IAAI,GAAG,WAAW,CAAA;AAE9E,wDAAwD;AACxD,MAAM,WAAW,uBAAuB,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM;IACnE,0CAA0C;IAC1C,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,KAAK,MAAM,CAAA;IACnF,uCAAuC;IACvC,QAAQ,EAAE,IAAI,CAAA;IACd,4EAA4E;IAC5E,cAAc,EAAE,IAAI,CAAA;IACpB,mIAAmI;IACnI,eAAe,CAAC,EAAE,SAAS,CAAA;IAC3B,kFAAkF;IAClF,KAAK,EAAE,SAAS,CAAA;IAChB,+BAA+B;IAC/B,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,8BAA8B;IAC9B,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,mFAAmF;IACnF,4BAA4B,CAAC,EAAE,OAAO,CAAA;IACtC,wFAAwF;IACxF,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,6BAA6B;IAC7B,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,uBAAuB,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"PluginSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/PluginSettingsCard.tsx"],"names":[],"mappings":"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"}
|
|
@@ -40,8 +40,8 @@ export function PluginSettingsCard(props) {
|
|
|
40
40
|
// With alwaysOpen the nav entry already provides the selection, so the
|
|
41
41
|
// header is a static title row instead of a disclosure button.
|
|
42
42
|
const header = alwaysOpen === true
|
|
43
|
-
? (_jsxs("div", { className: css.headerStatic, children: [_jsxs("span", { className: css.headText, children: [_jsx("span", { className: css.name, title: title, children: title }), _jsx("span", { className: css.description, title: description, children: props.descriptionNode ?? description })] }), state.dirty ? _jsx("span", { className: css.pending, title: props.t('settings.unsaved'), children: props.t('settings.unsaved') }) : null] }))
|
|
44
|
-
: (_jsxs("button", { type: "button", className: css.header, "aria-expanded": open, "aria-label": `${props.t(open ? 'settings.collapse' : 'settings.expand')}: ${title}`, onClick: () => { setOpen(!open); }, children: [_jsxs("span", { className: css.headText, children: [_jsx("span", { className: css.name, title: title, children: title }), _jsx("span", { className: css.description, title: description, children: props.descriptionNode ?? description })] }), state.dirty ? _jsx("span", { className: css.pending, title: props.t('settings.unsaved'), children: props.t('settings.unsaved') }) : null, _jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: open ? `${css.chevron} ${css.chevronOpen}` : css.chevron, children: _jsx("path", { d: "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z", fill: "currentColor" }) })] }));
|
|
43
|
+
? (_jsxs("div", { className: css.headerStatic, children: [props.icon !== undefined ? _jsx("span", { className: css.mark, "aria-hidden": "true", children: props.icon }) : null, _jsxs("span", { className: css.headText, children: [_jsx("span", { className: css.name, title: title, children: title }), _jsx("span", { className: css.description, title: description, children: props.descriptionNode ?? description })] }), state.dirty ? _jsx("span", { className: css.pending, title: props.t('settings.unsaved'), children: props.t('settings.unsaved') }) : null] }))
|
|
44
|
+
: (_jsxs("button", { type: "button", className: css.header, "aria-expanded": open, "aria-label": `${props.t(open ? 'settings.collapse' : 'settings.expand')}: ${title}`, onClick: () => { setOpen(!open); }, children: [props.icon !== undefined ? _jsx("span", { className: css.mark, "aria-hidden": "true", children: props.icon }) : null, _jsxs("span", { className: css.headText, children: [_jsx("span", { className: css.name, title: title, children: title }), _jsx("span", { className: css.description, title: description, children: props.descriptionNode ?? description })] }), state.dirty ? _jsx("span", { className: css.pending, title: props.t('settings.unsaved'), children: props.t('settings.unsaved') }) : null, _jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: open ? `${css.chevron} ${css.chevronOpen}` : css.chevron, children: _jsx("path", { d: "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z", fill: "currentColor" }) })] }));
|
|
45
45
|
// The namespace exists but the Host does not serve it to this client (the
|
|
46
46
|
// official settings allowlist omits third-party namespaces): show a card
|
|
47
47
|
// that explains the gap instead of vanishing, so a missing card never
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RemoteSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/RemoteSettingsCard.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAC7F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4CAA4C,CAAA;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;
|
|
1
|
+
{"version":3,"file":"RemoteSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/RemoteSettingsCard.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAC7F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4CAA4C,CAAA;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAGlE,OAAO,EAA+D,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,IAAI,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAIrK,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,6FAA6F;IAC7F,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,4CAA4C;AAC5C,MAAM,WAAW,uBAAwB,SAAQ,SAAS;IACxD,qBAAqB;IACrB,OAAO,EAAE,cAAc,CAAA;IACvB,sBAAsB;IACtB,UAAU,EAAE,cAAc,CAAA;IAC1B,gCAAgC;IAChC,cAAc,EAAE,cAAc,CAAA;IAC9B,yBAAyB;IACzB,UAAU,EAAE,cAAc,CAAA;IAC1B,0BAA0B;IAC1B,YAAY,EAAE,cAAc,CAAA;IAC5B,0BAA0B;IAC1B,UAAU,EAAE,cAAc,CAAA;IAC1B,sBAAsB;IACtB,oBAAoB,EAAE,cAAc,CAAA;IACpC,kCAAkC;IAClC,aAAa,EAAE,cAAc,CAAA;IAC7B,iCAAiC;IACjC,UAAU,EAAE,cAAc,CAAA;IAC1B,wDAAwD;IACxD,WAAW,EAAE,cAAc,CAAA;IAC3B,kCAAkC;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,uBAAuB;IACvB,OAAO,EAAE,cAAc,CAAA;CACxB;AAED,gEAAgE;AAChE,MAAM,WAAW,sBAAuB,SAAQ,WAAW;IACzD,KAAK,EAAE;QACL,oEAAoE;QACpE,kBAAkB,EAAE,aAAa,CAAC,uBAAuB,CAAC,CAAA;KAC3D,CAAA;CACF;AAED,oEAAoE;AACpE,qBAAa,4BAA4B;IACvC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA0B;IAC/C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwC;IAE9D,2EAA2E;gBAC/D,KAAK,EAAE,UAAU,CAAC,cAAc,CAAC;IAkB7C,OAAO,CAAC,UAAU;IAkBlB;;;OAGG;IACH,MAAM,IAAI,sBAAsB;IAIhC;;;OAGG;IACH,OAAO,IAAI,IAAI;CAGhB;AAED,4DAA4D;AAC5D,MAAM,MAAM,uBAAuB,GACjC,YAAY,CAAC,oBAAoB,CAAC,GAChC,WAAW,CAAC,QAAQ,CAAC,GACrB,UAAU,CAAC,sBAAsB,CAAC,CAAA;AAEtC;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,+BAyJhE"}
|
|
@@ -6,6 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
*/
|
|
7
7
|
import { useEffect, useState } from 'react';
|
|
8
8
|
import { PluginSettingsCard, ValueField, BooleanField } from "./PluginSettingsCard.js";
|
|
9
|
+
import { PhoneIcon } from "./PhoneIcon.js";
|
|
9
10
|
import { CardForm, booleanField, numberField, secretField, textField } from "./settings-form.js";
|
|
10
11
|
import { LanBindStatusError, readLanBindStatus, shouldStopLanBindPoll } from "./pair-api.js";
|
|
11
12
|
/** Bridges the `remote-web-ui` form onto the card's staged form. */
|
|
@@ -77,7 +78,7 @@ export function RemoteSettingsCard(props) {
|
|
|
77
78
|
invalidLabel: t('settings.invalidNumber'),
|
|
78
79
|
disabled,
|
|
79
80
|
};
|
|
80
|
-
return (_jsxs(PluginSettingsCard, { t: t, titleKey: "settings.title", descriptionKey: "settings.description", defaultOpen: false, state: state, onSave: props.save, onDiscard: props.discard, children: [_jsx(BooleanField, { id: "settings-remote-enabled", label: t('settings.enabled'), hint: t('settings.enabledHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.enabled, onEdit: (text) => { props.edit('enabled', text); }, onReset: () => { props.resetField('enabled'); } }), _jsx(ValueField, { id: "settings-remote-token-ttl", label: t('settings.tokenTtlMs'), hint: t('settings.tokenTtlMsHint'), numeric: true, ...fieldProps, ...state.tokenTtlMs, onEdit: (text) => { props.edit('tokenTtlMs', text); }, onReset: () => { props.resetField('tokenTtlMs'); } }), _jsx(ValueField, { id: "settings-remote-offline", label: t('settings.offlineAfterMs'), hint: t('settings.offlineAfterMsHint'), numeric: true, ...fieldProps, ...state.offlineAfterMs, onEdit: (text) => { props.edit('offlineAfterMs', text); }, onReset: () => { props.resetField('offlineAfterMs'); } }), _jsx(ValueField, { id: "settings-remote-max-devices", label: t('settings.maxDevices'), hint: t('settings.maxDevicesHint'), numeric: true, ...fieldProps, ...state.maxDevices, onEdit: (text) => { props.edit('maxDevices', text); }, onReset: () => { props.resetField('maxDevices'); } }), _jsx(ValueField, { id: "settings-remote-idle-expire", label: t('settings.idleExpireMs'), hint: t('settings.idleExpireMsHint'), numeric: true, ...fieldProps, ...state.idleExpireMs, onEdit: (text) => { props.edit('idleExpireMs', text); }, onReset: () => { props.resetField('idleExpireMs'); } }), _jsx(ValueField, { id: "settings-remote-cookie", label: t('settings.cookieName'), hint: t('settings.cookieNameHint'), ...fieldProps, ...state.cookieName, onEdit: (text) => { props.edit('cookieName', text); }, onReset: () => { props.resetField('cookieName'); } }), _jsx(BooleanField, { id: "settings-remote-fence", label: t('settings.requirePairingForLan'), hint: t('settings.requirePairingForLanHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.requirePairingForLan, onEdit: (text) => { props.edit('requirePairingForLan', text); }, onReset: () => { props.resetField('requirePairingForLan'); } }), _jsx(ValueField, { id: "settings-remote-public-base", label: t('settings.publicBaseUrl'), hint: t('settings.publicBaseUrlHint'), placeholder: "https://example.trycloudflare.com", ...fieldProps, ...state.publicBaseUrl, onEdit: (text) => { props.edit('publicBaseUrl', text); }, onReset: () => { props.resetField('publicBaseUrl'); } }), _jsx(BooleanField, { id: "settings-remote-auto-tunnel", label: t('settings.autoTunnel'), hint: t('settings.autoTunnelHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.autoTunnel, onEdit: (text) => { props.edit('autoTunnel', text); }, onReset: () => { props.resetField('autoTunnel'); } }), _jsx(ValueField, { id: "settings-remote-tunnel-token", label: t('settings.tunnelToken'), hint: t('settings.tunnelTokenHint'), placeholder: "eyJ...", ...fieldProps, ...state.tunnelToken, onEdit: (text) => { props.edit('tunnelToken', text); }, onReset: () => { props.resetField('tunnelToken'); } }), _jsx(BooleanField, { id: "settings-remote-relay", label: t('settings.relay'), hint: t('settings.relayHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.relay, onEdit: (text) => { props.edit('relay', text); }, onReset: () => { props.resetField('relay'); } }), _jsx(LanBindStatus, { t: t }), _jsx(BooleanField, { id: "settings-remote-lan-bind", label: t('settings.lanBind'), hint: t('settings.lanBindHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.lanBind, onEdit: (text) => { props.edit('lanBind', text); }, onReset: () => { props.resetField('lanBind'); } })] }));
|
|
81
|
+
return (_jsxs(PluginSettingsCard, { t: t, titleKey: "settings.title", descriptionKey: "settings.description", icon: _jsx(PhoneIcon, { size: 18 }), defaultOpen: false, state: state, onSave: props.save, onDiscard: props.discard, children: [_jsx(BooleanField, { id: "settings-remote-enabled", label: t('settings.enabled'), hint: t('settings.enabledHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.enabled, onEdit: (text) => { props.edit('enabled', text); }, onReset: () => { props.resetField('enabled'); } }), _jsx(ValueField, { id: "settings-remote-token-ttl", label: t('settings.tokenTtlMs'), hint: t('settings.tokenTtlMsHint'), numeric: true, ...fieldProps, ...state.tokenTtlMs, onEdit: (text) => { props.edit('tokenTtlMs', text); }, onReset: () => { props.resetField('tokenTtlMs'); } }), _jsx(ValueField, { id: "settings-remote-offline", label: t('settings.offlineAfterMs'), hint: t('settings.offlineAfterMsHint'), numeric: true, ...fieldProps, ...state.offlineAfterMs, onEdit: (text) => { props.edit('offlineAfterMs', text); }, onReset: () => { props.resetField('offlineAfterMs'); } }), _jsx(ValueField, { id: "settings-remote-max-devices", label: t('settings.maxDevices'), hint: t('settings.maxDevicesHint'), numeric: true, ...fieldProps, ...state.maxDevices, onEdit: (text) => { props.edit('maxDevices', text); }, onReset: () => { props.resetField('maxDevices'); } }), _jsx(ValueField, { id: "settings-remote-idle-expire", label: t('settings.idleExpireMs'), hint: t('settings.idleExpireMsHint'), numeric: true, ...fieldProps, ...state.idleExpireMs, onEdit: (text) => { props.edit('idleExpireMs', text); }, onReset: () => { props.resetField('idleExpireMs'); } }), _jsx(ValueField, { id: "settings-remote-cookie", label: t('settings.cookieName'), hint: t('settings.cookieNameHint'), ...fieldProps, ...state.cookieName, onEdit: (text) => { props.edit('cookieName', text); }, onReset: () => { props.resetField('cookieName'); } }), _jsx(BooleanField, { id: "settings-remote-fence", label: t('settings.requirePairingForLan'), hint: t('settings.requirePairingForLanHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.requirePairingForLan, onEdit: (text) => { props.edit('requirePairingForLan', text); }, onReset: () => { props.resetField('requirePairingForLan'); } }), _jsx(ValueField, { id: "settings-remote-public-base", label: t('settings.publicBaseUrl'), hint: t('settings.publicBaseUrlHint'), placeholder: "https://example.trycloudflare.com", ...fieldProps, ...state.publicBaseUrl, onEdit: (text) => { props.edit('publicBaseUrl', text); }, onReset: () => { props.resetField('publicBaseUrl'); } }), _jsx(BooleanField, { id: "settings-remote-auto-tunnel", label: t('settings.autoTunnel'), hint: t('settings.autoTunnelHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.autoTunnel, onEdit: (text) => { props.edit('autoTunnel', text); }, onReset: () => { props.resetField('autoTunnel'); } }), _jsx(ValueField, { id: "settings-remote-tunnel-token", label: t('settings.tunnelToken'), hint: t('settings.tunnelTokenHint'), placeholder: "eyJ...", ...fieldProps, ...state.tunnelToken, onEdit: (text) => { props.edit('tunnelToken', text); }, onReset: () => { props.resetField('tunnelToken'); } }), _jsx(BooleanField, { id: "settings-remote-relay", label: t('settings.relay'), hint: t('settings.relayHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.relay, onEdit: (text) => { props.edit('relay', text); }, onReset: () => { props.resetField('relay'); } }), _jsx(LanBindStatus, { t: t }), _jsx(BooleanField, { id: "settings-remote-lan-bind", label: t('settings.lanBind'), hint: t('settings.lanBindHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.lanBind, onEdit: (text) => { props.edit('lanBind', text); }, onReset: () => { props.resetField('lanBind'); } })] }));
|
|
81
82
|
}
|
|
82
83
|
/**
|
|
83
84
|
* Live LAN-bind facts above the toggle: the managed block's host, the live
|
|
@@ -151,9 +151,9 @@ export function apply(ctx) {
|
|
|
151
151
|
}
|
|
152
152
|
const t = ctx.locale.bind(NS);
|
|
153
153
|
// Hand the `remote` translate seat to the module-scope adaptation layer:
|
|
154
|
-
// the whale
|
|
155
|
-
//
|
|
156
|
-
//
|
|
154
|
+
// the whale label was rendered with the English fallback before any
|
|
155
|
+
// dictionary existed; the wiring plus the layer's own sync tick re-renders
|
|
156
|
+
// it in the active locale.
|
|
157
157
|
if (adapt !== undefined)
|
|
158
158
|
adapt.translate = t;
|
|
159
159
|
// The settings form this card edits. The family group's binder resolves a
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
export declare const zh: {
|
|
4
4
|
'entry.label': string;
|
|
5
5
|
'mobile.whale.open': string;
|
|
6
|
-
'mobile.composer.pickModel': string;
|
|
7
|
-
'mobile.composer.pickEffort': string;
|
|
8
6
|
title: string;
|
|
9
7
|
subtitle: string;
|
|
10
8
|
'card.title': string;
|
|
@@ -129,8 +127,6 @@ export type SettingsCardKey = RemoteKey;
|
|
|
129
127
|
export declare const en: {
|
|
130
128
|
'entry.label': string;
|
|
131
129
|
'mobile.whale.open': string;
|
|
132
|
-
'mobile.composer.pickModel': string;
|
|
133
|
-
'mobile.composer.pickEffort': string;
|
|
134
130
|
title: string;
|
|
135
131
|
subtitle: string;
|
|
136
132
|
'card.title': string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,mEAAmE;AACnE,eAAO,MAAM,EAAE
|
|
1
|
+
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwHmB,CAAA;AAElC,sCAAsC;AACtC,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,EAAE,CAAA;AAEvC,wDAAwD;AACxD,MAAM,MAAM,eAAe,GAAG,SAAS,CAAA;AAEvC,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwHsB,CAAA"}
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
export const zh = {
|
|
4
4
|
'entry.label': '远程访问',
|
|
5
5
|
'mobile.whale.open': '打开侧边栏',
|
|
6
|
-
'mobile.composer.pickModel': '选择模型',
|
|
7
|
-
'mobile.composer.pickEffort': '选择推理等级',
|
|
8
6
|
'title': '远程访问',
|
|
9
7
|
'subtitle': '通过手机或另一台电脑配对,远程使用同一份 Web 界面(官方界面 + 移动端适配)',
|
|
10
8
|
'card.title': '设备配对',
|
|
@@ -14,7 +12,7 @@ export const zh = {
|
|
|
14
12
|
'status.disconnected': '已配对设备离线',
|
|
15
13
|
'status.stopped': '已停止远程访问',
|
|
16
14
|
'status.lanRequired': '此功能需要局域网绑定或公网地址才能使用',
|
|
17
|
-
'status.lanRequiredHint': '当前服务仅绑定在 127.0.0.1 且未配置公网地址,手机无法访问。请在本插件的设置卡片中打开“局域网访问”(装在全家桶里时在「设置 → Web 插件 →
|
|
15
|
+
'status.lanRequiredHint': '当前服务仅绑定在 127.0.0.1 且未配置公网地址,手机无法访问。请在本插件的设置卡片中打开“局域网访问”(装在全家桶里时在「设置 → Web 插件 → 远程访问」,单独安装时在设置 → 插件 → 本插件行),或用 dsh web --host 0.0.0.0 重新启动,或填写内网穿透的公网地址。',
|
|
18
16
|
'status.loopbackRequired': '配对面板仅限本机使用',
|
|
19
17
|
'status.loopbackRequiredHint': '请通过 http://127.0.0.1 打开此页面后重试;手机请使用配对链接访问。',
|
|
20
18
|
'status.unreachable': '无法连接配对服务',
|
|
@@ -71,8 +69,8 @@ export const zh = {
|
|
|
71
69
|
'relay.failed': '固定域名同步失败:{error}。正在自动重试,期间二维码可能使用临时地址。',
|
|
72
70
|
'close.label': '关闭远程访问面板',
|
|
73
71
|
// 插件设置卡片(家族 list 席位,或官方 plugins.bundle.config 席位)。
|
|
74
|
-
'settings.title': '
|
|
75
|
-
'settings.description': '
|
|
72
|
+
'settings.title': '远程访问',
|
|
73
|
+
'settings.description': '手机配对、公网隧道与设备限额。',
|
|
76
74
|
'settings.enabled': '启用远程访问',
|
|
77
75
|
'settings.enabledHint': '关闭后移除侧边栏入口并停用配对路由与局域网栅栏。',
|
|
78
76
|
'settings.tokenTtlMs': '配对令牌有效期(毫秒)',
|
|
@@ -127,8 +125,6 @@ export const zh = {
|
|
|
127
125
|
export const en = {
|
|
128
126
|
'entry.label': 'Remote access',
|
|
129
127
|
'mobile.whale.open': 'Open sidebar',
|
|
130
|
-
'mobile.composer.pickModel': 'Pick model',
|
|
131
|
-
'mobile.composer.pickEffort': 'Pick reasoning effort',
|
|
132
128
|
'title': 'Remote access',
|
|
133
129
|
'subtitle': 'Pair a phone or another computer to share the same Web GUI (official UI + mobile adaptation)',
|
|
134
130
|
'card.title': 'Pair a device',
|
|
@@ -195,8 +191,8 @@ export const en = {
|
|
|
195
191
|
'relay.failed': 'Stable-hostname sync failed: {error}. Retrying automatically; the QR may use the ephemeral address meanwhile.',
|
|
196
192
|
'close.label': 'Close remote access panel',
|
|
197
193
|
// Plugin settings card (the family list seat, or the official bundle-config seat).
|
|
198
|
-
'settings.title': 'Remote access
|
|
199
|
-
'settings.description': '
|
|
194
|
+
'settings.title': 'Remote access',
|
|
195
|
+
'settings.description': 'Phone pairing, public tunnel and device limits.',
|
|
200
196
|
'settings.enabled': 'Enable remote access',
|
|
201
197
|
'settings.enabledHint': 'When off, the sidebar entry is removed and pairing routes plus the LAN fence stop.',
|
|
202
198
|
'settings.tokenTtlMs': 'Pairing token lifetime (ms)',
|
|
@@ -30,8 +30,8 @@ export interface RemoteAdaptGlobal {
|
|
|
30
30
|
closeDetails: (() => void) | null;
|
|
31
31
|
/**
|
|
32
32
|
* Wired by the plugin apply once the `remote` locale namespace is bound:
|
|
33
|
-
* the translate seat the injected
|
|
34
|
-
*
|
|
33
|
+
* the translate seat the injected surface (the whale) reads its label
|
|
34
|
+
* from. Null until then; label reads fall back to English
|
|
35
35
|
* (the SDK's universal fallback) and the 600ms sync tick re-renders the
|
|
36
36
|
* labels once the seat is wired.
|
|
37
37
|
*/
|
|
@@ -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;AAoQD;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAi8BvC"}
|