@yezack/dsh-remote-mobile 0.0.0-stage → 1.9.1

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/lib/client.js ADDED
@@ -0,0 +1,4650 @@
1
+ /**
2
+ * dsh-remote-mobile 客户端运行时 Bundle
3
+ * 由 scripts/build-client.js 基于 esbuild 自动化编译打包,请勿直接手动修改此文件!
4
+ * 源码请移步 src/client/ 目录进行模块化开发与维护。
5
+ */
6
+ window.__ModuleLoader__.load({
7
+ id: "dsh-remote-mobile",
8
+ factory: function(require) {
9
+ var module = { exports: {} };
10
+ var exports = module.exports;
11
+
12
+ "use strict";
13
+ var __create = Object.create;
14
+ var __defProp = Object.defineProperty;
15
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
16
+ var __getOwnPropNames = Object.getOwnPropertyNames;
17
+ var __getProtoOf = Object.getPrototypeOf;
18
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, { get: all[name], enumerable: true });
22
+ };
23
+ var __copyProps = (to, from, except, desc) => {
24
+ if (from && typeof from === "object" || typeof from === "function") {
25
+ for (let key of __getOwnPropNames(from))
26
+ if (!__hasOwnProp.call(to, key) && key !== except)
27
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
28
+ }
29
+ return to;
30
+ };
31
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
32
+ // If the importer is in node compatibility mode or this is not an ESM
33
+ // file that has been converted to a CommonJS file using a Babel-
34
+ // compatible transform (i.e. "__esModule" has not been set), then set
35
+ // "default" to the CommonJS "module.exports" for node compatibility.
36
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
37
+ mod
38
+ ));
39
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
40
+
41
+ // src/client/index.jsx
42
+ var index_exports = {};
43
+ __export(index_exports, {
44
+ TailscaleMobileSection: () => TailscaleMobileSection,
45
+ apply: () => apply,
46
+ default: () => index_default,
47
+ inject: () => inject
48
+ });
49
+ module.exports = __toCommonJS(index_exports);
50
+ var import_react10 = __toESM(require("react"), 1);
51
+
52
+ // src/client/components/MainSection.jsx
53
+ var import_react9 = __toESM(require("react"), 1);
54
+
55
+ // src/client/api.js
56
+ var RM_API_BASE = (function() {
57
+ try {
58
+ if (typeof document !== "undefined" && document.baseURI) {
59
+ return new URL("api/remote-mobile/", document.baseURI).toString();
60
+ }
61
+ return "/api/remote-mobile/";
62
+ } catch (e) {
63
+ return "/api/remote-mobile/";
64
+ }
65
+ })();
66
+ function safeFetchJson(url, options) {
67
+ return fetch(url, options).then(function(res) {
68
+ if (!res.ok) {
69
+ return res.text().then(function(text) {
70
+ var msg = "HTTP " + res.status;
71
+ try {
72
+ var parsed = JSON.parse(text);
73
+ if (parsed && (parsed.reason || parsed.message || parsed.error)) {
74
+ msg = parsed.reason || parsed.message || parsed.error;
75
+ }
76
+ } catch (e) {
77
+ }
78
+ throw new Error(msg);
79
+ });
80
+ }
81
+ return res.json().catch(function() {
82
+ throw new Error("\u54CD\u5E94\u89E3\u6790\u5931\u8D25 (Invalid JSON response)");
83
+ });
84
+ });
85
+ }
86
+ function fetchStatus() {
87
+ return safeFetchJson(RM_API_BASE + "status?_t=" + Date.now());
88
+ }
89
+ function generatePairCode() {
90
+ return safeFetchJson(RM_API_BASE + "generate-code", { method: "POST" });
91
+ }
92
+ function updateBypassConfig(allowTailscale, allowLan) {
93
+ return safeFetchJson(RM_API_BASE + "update-options", {
94
+ method: "POST",
95
+ headers: { "Content-Type": "application/json" },
96
+ body: JSON.stringify({ allowTailscale, allowLan })
97
+ });
98
+ }
99
+ function updateAdvancedSecurityOptions(payload) {
100
+ return safeFetchJson(RM_API_BASE + "update-options", {
101
+ method: "POST",
102
+ headers: { "Content-Type": "application/json" },
103
+ body: JSON.stringify(payload)
104
+ });
105
+ }
106
+ function updateSecret(secret) {
107
+ return safeFetchJson(RM_API_BASE + "set-secret", {
108
+ method: "POST",
109
+ headers: { "Content-Type": "application/json" },
110
+ body: JSON.stringify({ secret })
111
+ });
112
+ }
113
+ function clearSecretApi() {
114
+ return safeFetchJson(RM_API_BASE + "clear-secret", {
115
+ method: "POST",
116
+ headers: { "Content-Type": "application/json" }
117
+ });
118
+ }
119
+ function revokeDeviceApi(token) {
120
+ return safeFetchJson(RM_API_BASE + "revoke-device", {
121
+ method: "POST",
122
+ headers: { "Content-Type": "application/json" },
123
+ body: JSON.stringify({ token })
124
+ });
125
+ }
126
+ function revokeAllDevicesApi() {
127
+ return safeFetchJson(RM_API_BASE + "revoke-all", {
128
+ method: "POST",
129
+ headers: { "Content-Type": "application/json" }
130
+ });
131
+ }
132
+ function unlockIpApi(ip) {
133
+ return safeFetchJson(RM_API_BASE + "unlock-ip", {
134
+ method: "POST",
135
+ headers: { "Content-Type": "application/json" },
136
+ body: JSON.stringify({ ip })
137
+ });
138
+ }
139
+ function clearIpStatsApi() {
140
+ return safeFetchJson(RM_API_BASE + "clear-ip-stats", {
141
+ method: "POST",
142
+ headers: { "Content-Type": "application/json" }
143
+ });
144
+ }
145
+ function fetchStyles() {
146
+ return safeFetchJson(RM_API_BASE + "styles?_t=" + Date.now());
147
+ }
148
+ function saveStyleSnippet(payload) {
149
+ return safeFetchJson(RM_API_BASE + "styles", {
150
+ method: "POST",
151
+ headers: { "Content-Type": "application/json" },
152
+ body: JSON.stringify(payload)
153
+ });
154
+ }
155
+ function toggleStyleApi(id, scope, enabled) {
156
+ return safeFetchJson(RM_API_BASE + "styles/toggle", {
157
+ method: "POST",
158
+ headers: { "Content-Type": "application/json" },
159
+ body: JSON.stringify({ id, scope, enabled })
160
+ });
161
+ }
162
+ function deleteStyleSnippetApi(id) {
163
+ return safeFetchJson(RM_API_BASE + "styles/delete", {
164
+ method: "POST",
165
+ headers: { "Content-Type": "application/json" },
166
+ body: JSON.stringify({ id })
167
+ });
168
+ }
169
+ function resetStylesApi() {
170
+ return safeFetchJson(RM_API_BASE + "styles/reset", {
171
+ method: "POST",
172
+ headers: { "Content-Type": "application/json" }
173
+ });
174
+ }
175
+
176
+ // src/client/qrcode.js
177
+ var generateQrSvg = (function() {
178
+ var PAD0 = 236, PAD1 = 17;
179
+ function QRCodeModel(typeNumber, errorCorrectLevel) {
180
+ this.typeNumber = typeNumber;
181
+ this.errorCorrectLevel = errorCorrectLevel;
182
+ this.modules = null;
183
+ this.moduleCount = 0;
184
+ this.dataCache = null;
185
+ this.dataList = [];
186
+ }
187
+ QRCodeModel.prototype = {
188
+ addData: function(data) {
189
+ this.dataList.push(new QR8bitByte(data));
190
+ this.dataCache = null;
191
+ },
192
+ isDark: function(row, col) {
193
+ if (row < 0 || this.moduleCount <= row || col < 0 || this.moduleCount <= col) throw new Error(row + "," + col);
194
+ return this.modules[row][col];
195
+ },
196
+ getModuleCount: function() {
197
+ return this.moduleCount;
198
+ },
199
+ make: function() {
200
+ if (this.typeNumber < 1) {
201
+ var typeNumber = 1;
202
+ for (typeNumber = 1; typeNumber < 40; typeNumber++) {
203
+ var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, this.errorCorrectLevel);
204
+ var buffer = new QRBitBuffer();
205
+ var totalDataCount = 0;
206
+ for (var i2 = 0; i2 < rsBlocks.length; i2++) totalDataCount += rsBlocks[i2].dataCount;
207
+ for (var i2 = 0; i2 < this.dataList.length; i2++) {
208
+ var data = this.dataList[i2];
209
+ buffer.put(data.mode, 4);
210
+ buffer.put(data.getLength(), QRUtil.getLengthInBits(data.mode, typeNumber));
211
+ data.write(buffer);
212
+ }
213
+ if (buffer.getLengthInBits() <= totalDataCount * 8) break;
214
+ }
215
+ this.typeNumber = typeNumber;
216
+ }
217
+ this.makeImpl(false, this.getBestMaskPattern());
218
+ },
219
+ makeImpl: function(test, maskPattern) {
220
+ this.moduleCount = this.typeNumber * 4 + 17;
221
+ this.modules = new Array(this.moduleCount);
222
+ for (var row = 0; row < this.moduleCount; row++) {
223
+ this.modules[row] = new Array(this.moduleCount);
224
+ for (var col = 0; col < this.moduleCount; col++) this.modules[row][col] = null;
225
+ }
226
+ this.setupPositionProbePattern(0, 0);
227
+ this.setupPositionProbePattern(this.moduleCount - 7, 0);
228
+ this.setupPositionProbePattern(0, this.moduleCount - 7);
229
+ this.setupPositionAdjustPattern();
230
+ this.setupTimingPattern();
231
+ this.setupTypeInfo(test, maskPattern);
232
+ if (this.typeNumber >= 7) this.setupTypeNumber(test);
233
+ if (this.dataCache == null) this.dataCache = QRCodeModel.createData(this.typeNumber, this.errorCorrectLevel, this.dataList);
234
+ this.mapData(this.dataCache, maskPattern);
235
+ },
236
+ setupPositionProbePattern: function(row, col) {
237
+ for (var r = -1; r <= 7; r++) {
238
+ if (row + r <= -1 || this.moduleCount <= row + r) continue;
239
+ for (var c = -1; c <= 7; c++) {
240
+ if (col + c <= -1 || this.moduleCount <= col + c) continue;
241
+ if (0 <= r && r <= 6 && (c == 0 || c == 6) || 0 <= c && c <= 6 && (r == 0 || r == 6) || 2 <= r && r <= 4 && 2 <= c && c <= 4) {
242
+ this.modules[row + r][col + c] = true;
243
+ } else {
244
+ this.modules[row + r][col + c] = false;
245
+ }
246
+ }
247
+ }
248
+ },
249
+ getBestMaskPattern: function() {
250
+ var minLostPoint = 0, pattern = 0;
251
+ for (var i2 = 0; i2 < 8; i2++) {
252
+ this.makeImpl(true, i2);
253
+ var lostPoint = QRUtil.getLostPoint(this);
254
+ if (i2 == 0 || minLostPoint > lostPoint) {
255
+ minLostPoint = lostPoint;
256
+ pattern = i2;
257
+ }
258
+ }
259
+ return pattern;
260
+ },
261
+ setupTimingPattern: function() {
262
+ for (var r = 8; r < this.moduleCount - 8; r++) {
263
+ if (this.modules[r][6] !== null) continue;
264
+ this.modules[r][6] = r % 2 == 0;
265
+ }
266
+ for (var c = 8; c < this.moduleCount - 8; c++) {
267
+ if (this.modules[6][c] !== null) continue;
268
+ this.modules[6][c] = c % 2 == 0;
269
+ }
270
+ },
271
+ setupPositionAdjustPattern: function() {
272
+ var pos = QRUtil.getPatternPosition(this.typeNumber);
273
+ for (var i2 = 0; i2 < pos.length; i2++) {
274
+ for (var j = 0; j < pos.length; j++) {
275
+ var row = pos[i2], col = pos[j];
276
+ if (this.modules[row][col] !== null) continue;
277
+ for (var r = -2; r <= 2; r++) {
278
+ for (var c = -2; c <= 2; c++) {
279
+ if (r == -2 || r == 2 || c == -2 || c == 2 || r == 0 && c == 0) this.modules[row + r][col + c] = true;
280
+ else this.modules[row + r][col + c] = false;
281
+ }
282
+ }
283
+ }
284
+ }
285
+ },
286
+ setupTypeNumber: function(test) {
287
+ var bits = QRUtil.getBCHTypeNumber(this.typeNumber);
288
+ for (var i2 = 0; i2 < 18; i2++) {
289
+ var mod = !test && (bits >> i2 & 1) == 1;
290
+ this.modules[Math.floor(i2 / 3)][i2 % 3 + this.moduleCount - 8 - 3] = mod;
291
+ this.modules[i2 % 3 + this.moduleCount - 8 - 3][Math.floor(i2 / 3)] = mod;
292
+ }
293
+ },
294
+ setupTypeInfo: function(test, maskPattern) {
295
+ var data = this.errorCorrectLevel << 3 | maskPattern;
296
+ var bits = QRUtil.getBCHTypeInfo(data);
297
+ for (var i2 = 0; i2 < 15; i2++) {
298
+ var mod = !test && (bits >> i2 & 1) == 1;
299
+ if (i2 < 6) this.modules[i2][8] = mod;
300
+ else if (i2 < 8) this.modules[i2 + 1][8] = mod;
301
+ else this.modules[this.moduleCount - 15 + i2][8] = mod;
302
+ if (i2 < 8) this.modules[8][this.moduleCount - i2 - 1] = mod;
303
+ else if (i2 < 9) this.modules[8][15 - i2 - 1 + 1] = mod;
304
+ else this.modules[8][15 - i2 - 1] = mod;
305
+ }
306
+ this.modules[this.moduleCount - 8][8] = !test;
307
+ },
308
+ mapData: function(data, maskPattern) {
309
+ var inc = -1, row = this.moduleCount - 1, bitIndex = 7, byteIndex = 0;
310
+ for (var col = this.moduleCount - 1; col > 0; col -= 2) {
311
+ if (col == 6) col--;
312
+ while (true) {
313
+ for (var c = 0; c < 2; c++) {
314
+ if (this.modules[row][col - c] === null) {
315
+ var dark = false;
316
+ if (byteIndex < data.length) dark = (data[byteIndex] >>> bitIndex & 1) == 1;
317
+ var mask = QRUtil.getMask(maskPattern, row, col - c);
318
+ if (mask) dark = !dark;
319
+ this.modules[row][col - c] = dark;
320
+ bitIndex--;
321
+ if (bitIndex == -1) {
322
+ byteIndex++;
323
+ bitIndex = 7;
324
+ }
325
+ }
326
+ }
327
+ row += inc;
328
+ if (row < 0 || this.moduleCount <= row) {
329
+ row -= inc;
330
+ inc = -inc;
331
+ break;
332
+ }
333
+ }
334
+ }
335
+ }
336
+ };
337
+ QRCodeModel.createData = function(typeNumber, errorCorrectLevel, dataList) {
338
+ var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel);
339
+ var buffer = new QRBitBuffer();
340
+ for (var i2 = 0; i2 < dataList.length; i2++) {
341
+ var data = dataList[i2];
342
+ buffer.put(data.mode, 4);
343
+ buffer.put(data.getLength(), QRUtil.getLengthInBits(data.mode, typeNumber));
344
+ data.write(buffer);
345
+ }
346
+ var totalDataCount = 0;
347
+ for (var i2 = 0; i2 < rsBlocks.length; i2++) totalDataCount += rsBlocks[i2].dataCount;
348
+ if (buffer.getLengthInBits() > totalDataCount * 8) throw new Error("code length overflow");
349
+ if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) buffer.put(0, 4);
350
+ while (buffer.getLengthInBits() % 8 != 0) buffer.putBit(false);
351
+ while (true) {
352
+ if (buffer.getLengthInBits() >= totalDataCount * 8) break;
353
+ buffer.put(PAD0, 8);
354
+ if (buffer.getLengthInBits() >= totalDataCount * 8) break;
355
+ buffer.put(PAD1, 8);
356
+ }
357
+ return QRCodeModel.createBytes(buffer, rsBlocks);
358
+ };
359
+ QRCodeModel.createBytes = function(buffer, rsBlocks) {
360
+ var offset = 0, maxDcCount = 0, maxEcCount = 0;
361
+ var dcdata = new Array(rsBlocks.length), ecdata = new Array(rsBlocks.length);
362
+ for (var r = 0; r < rsBlocks.length; r++) {
363
+ var dcCount = rsBlocks[r].dataCount, ecCount = rsBlocks[r].totalCount - dcCount;
364
+ maxDcCount = Math.max(maxDcCount, dcCount);
365
+ maxEcCount = Math.max(maxEcCount, ecCount);
366
+ dcdata[r] = new Array(dcCount);
367
+ for (var i2 = 0; i2 < dcdata[r].length; i2++) dcdata[r][i2] = 255 & buffer.buffer[i2 + offset];
368
+ offset += dcCount;
369
+ var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount);
370
+ var rawPoly = new QRPolynomial(dcdata[r], rsPoly.getLength() - 1);
371
+ var modPoly = rawPoly.mod(rsPoly);
372
+ ecdata[r] = new Array(rsPoly.getLength() - 1);
373
+ for (var i2 = 0; i2 < ecdata[r].length; i2++) {
374
+ var modIndex = i2 + modPoly.getLength() - ecdata[r].length;
375
+ ecdata[r][i2] = modIndex >= 0 ? modPoly.get(modIndex) : 0;
376
+ }
377
+ }
378
+ var totalCodeCount = 0;
379
+ for (var i2 = 0; i2 < rsBlocks.length; i2++) totalCodeCount += rsBlocks[i2].totalCount;
380
+ var data = new Array(totalCodeCount), index = 0;
381
+ for (var i2 = 0; i2 < maxDcCount; i2++) {
382
+ for (var r = 0; r < rsBlocks.length; r++) {
383
+ if (i2 < dcdata[r].length) data[index++] = dcdata[r][i2];
384
+ }
385
+ }
386
+ for (var i2 = 0; i2 < maxEcCount; i2++) {
387
+ for (var r = 0; r < rsBlocks.length; r++) {
388
+ if (i2 < ecdata[r].length) data[index++] = ecdata[r][i2];
389
+ }
390
+ }
391
+ return data;
392
+ };
393
+ function QR8bitByte(data) {
394
+ this.mode = 4;
395
+ this.data = data;
396
+ this.parsedData = [];
397
+ for (var i2 = 0, l = this.data.length; i2 < l; i2++) {
398
+ var byteArray = [], code = this.data.charCodeAt(i2);
399
+ if (code > 65536) {
400
+ byteArray[0] = 240 | (code & 1835008) >>> 18;
401
+ byteArray[1] = 128 | (code & 258048) >>> 12;
402
+ byteArray[2] = 128 | (code & 4032) >>> 6;
403
+ byteArray[3] = 128 | code & 63;
404
+ } else if (code > 2048) {
405
+ byteArray[0] = 224 | (code & 61440) >>> 12;
406
+ byteArray[1] = 128 | (code & 4032) >>> 6;
407
+ byteArray[2] = 128 | code & 63;
408
+ } else if (code > 127) {
409
+ byteArray[0] = 192 | (code & 1984) >>> 6;
410
+ byteArray[1] = 128 | code & 63;
411
+ } else {
412
+ byteArray[0] = code;
413
+ }
414
+ this.parsedData.push(byteArray);
415
+ }
416
+ this.parsedData = Array.prototype.concat.apply([], this.parsedData);
417
+ if (this.parsedData.length != this.data.length) this.parsedData.unshift(191, 187, 239);
418
+ }
419
+ QR8bitByte.prototype = {
420
+ getLength: function() {
421
+ return this.parsedData.length;
422
+ },
423
+ write: function(buffer) {
424
+ for (var i2 = 0, l = this.parsedData.length; i2 < l; i2++) buffer.put(this.parsedData[i2], 8);
425
+ }
426
+ };
427
+ var QRUtil = {
428
+ PATTERN_POSITION_TABLE: [
429
+ [],
430
+ [6, 18],
431
+ [6, 22],
432
+ [6, 26],
433
+ [6, 30],
434
+ [6, 34],
435
+ [6, 22, 38],
436
+ [6, 24, 42],
437
+ [6, 26, 46],
438
+ [6, 28, 50],
439
+ [6, 30, 54],
440
+ [6, 32, 58],
441
+ [6, 34, 62],
442
+ [6, 26, 46, 66],
443
+ [6, 26, 48, 70],
444
+ [6, 26, 50, 74],
445
+ [6, 30, 54, 78],
446
+ [6, 30, 56, 82],
447
+ [6, 30, 58, 86],
448
+ [6, 34, 62, 90],
449
+ [6, 28, 50, 72, 94],
450
+ [6, 26, 50, 74, 98],
451
+ [6, 30, 54, 78, 102],
452
+ [6, 28, 54, 80, 106],
453
+ [6, 32, 58, 84, 110],
454
+ [6, 30, 58, 86, 114],
455
+ [6, 34, 62, 90, 118],
456
+ [6, 26, 50, 74, 98, 122],
457
+ [6, 30, 54, 78, 102, 126],
458
+ [6, 26, 52, 78, 104, 130],
459
+ [6, 30, 56, 82, 108, 134],
460
+ [6, 34, 60, 86, 112, 138],
461
+ [6, 30, 58, 86, 114, 142],
462
+ [6, 34, 62, 90, 118, 146],
463
+ [6, 30, 54, 78, 102, 126, 150],
464
+ [6, 24, 50, 76, 102, 128, 154],
465
+ [6, 28, 54, 80, 106, 132, 158],
466
+ [6, 32, 58, 84, 110, 136, 162],
467
+ [6, 26, 54, 82, 110, 138, 166],
468
+ [6, 30, 58, 86, 114, 142, 170]
469
+ ],
470
+ G15: 1 << 10 | 1 << 8 | 1 << 5 | 1 << 4 | 1 << 2 | 1 << 1 | 1 << 0,
471
+ G18: 1 << 12 | 1 << 11 | 1 << 10 | 1 << 9 | 1 << 8 | 1 << 5 | 1 << 2 | 1 << 0,
472
+ G15_MASK: 1 << 14 | 1 << 12 | 1 << 10 | 1 << 4 | 1 << 1,
473
+ getBCHTypeInfo: function(data) {
474
+ var d = data << 10;
475
+ while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) >= 0) {
476
+ d ^= QRUtil.G15 << QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15);
477
+ }
478
+ return (data << 10 | d) ^ QRUtil.G15_MASK;
479
+ },
480
+ getBCHTypeNumber: function(data) {
481
+ var d = data << 12;
482
+ while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) >= 0) {
483
+ d ^= QRUtil.G18 << QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18);
484
+ }
485
+ return data << 12 | d;
486
+ },
487
+ getBCHDigit: function(data) {
488
+ var digit = 0;
489
+ while (data != 0) {
490
+ digit++;
491
+ data >>>= 1;
492
+ }
493
+ return digit;
494
+ },
495
+ getPatternPosition: function(typeNumber) {
496
+ return QRUtil.PATTERN_POSITION_TABLE[typeNumber - 1];
497
+ },
498
+ getMask: function(maskPattern, i2, j) {
499
+ switch (maskPattern) {
500
+ case 0:
501
+ return (i2 + j) % 2 == 0;
502
+ case 1:
503
+ return i2 % 2 == 0;
504
+ case 2:
505
+ return j % 3 == 0;
506
+ case 3:
507
+ return (i2 + j) % 3 == 0;
508
+ case 4:
509
+ return (Math.floor(i2 / 2) + Math.floor(j / 3)) % 2 == 0;
510
+ case 5:
511
+ return i2 * j % 2 + i2 * j % 3 == 0;
512
+ case 6:
513
+ return (i2 * j % 2 + i2 * j % 3) % 2 == 0;
514
+ case 7:
515
+ return (i2 * j % 3 + (i2 + j) % 2) % 2 == 0;
516
+ default:
517
+ throw new Error("bad maskPattern:" + maskPattern);
518
+ }
519
+ },
520
+ getErrorCorrectPolynomial: function(errorCorrectLength) {
521
+ var a = new QRPolynomial([1], 0);
522
+ for (var i2 = 0; i2 < errorCorrectLength; i2++) a = a.multiply(new QRPolynomial([1, QRMath.gexp(i2)], 0));
523
+ return a;
524
+ },
525
+ getLengthInBits: function(mode, type) {
526
+ if (1 <= type && type < 10) return 8;
527
+ else if (type < 27) return 16;
528
+ else if (type < 41) return 16;
529
+ else throw new Error("type:" + type);
530
+ },
531
+ getLostPoint: function(qrCode) {
532
+ var moduleCount = qrCode.getModuleCount(), lostPoint = 0;
533
+ for (var row = 0; row < moduleCount; row++) {
534
+ for (var col = 0; col < moduleCount; col++) {
535
+ var sameCount = 0, dark = qrCode.isDark(row, col);
536
+ for (var r = -1; r <= 1; r++) {
537
+ if (row + r < 0 || moduleCount <= row + r) continue;
538
+ for (var c = -1; c <= 1; c++) {
539
+ if (col + c < 0 || moduleCount <= col + c) continue;
540
+ if (r == 0 && c == 0) continue;
541
+ if (dark == qrCode.isDark(row + r, col + c)) sameCount++;
542
+ }
543
+ }
544
+ if (sameCount > 5) lostPoint += 3 + sameCount - 5;
545
+ }
546
+ }
547
+ return lostPoint;
548
+ }
549
+ };
550
+ var QRMath = {
551
+ glog: function(n) {
552
+ if (n < 1) throw new Error("glog(" + n + ")");
553
+ return QRMath.LOG_TABLE[n];
554
+ },
555
+ gexp: function(n) {
556
+ while (n < 0) n += 255;
557
+ while (n >= 256) n -= 255;
558
+ return QRMath.EXP_TABLE[n];
559
+ },
560
+ EXP_TABLE: new Array(256),
561
+ LOG_TABLE: new Array(256)
562
+ };
563
+ for (var i = 0; i < 8; i++) QRMath.EXP_TABLE[i] = 1 << i;
564
+ for (var i = 8; i < 256; i++) QRMath.EXP_TABLE[i] = QRMath.EXP_TABLE[i - 4] ^ QRMath.EXP_TABLE[i - 5] ^ QRMath.EXP_TABLE[i - 6] ^ QRMath.EXP_TABLE[i - 8];
565
+ for (var i = 0; i < 255; i++) QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]] = i;
566
+ function QRPolynomial(num, shift) {
567
+ if (num.length == void 0) throw new Error(num.length + "/" + shift);
568
+ var offset = 0;
569
+ while (offset < num.length && num[offset] == 0) offset++;
570
+ this.num = new Array(num.length - offset + shift);
571
+ for (var i2 = 0; i2 < num.length - offset; i2++) this.num[i2] = num[i2 + offset];
572
+ }
573
+ QRPolynomial.prototype = {
574
+ get: function(index) {
575
+ return this.num[index];
576
+ },
577
+ getLength: function() {
578
+ return this.num.length;
579
+ },
580
+ multiply: function(e) {
581
+ var num = new Array(this.getLength() + e.getLength() - 1);
582
+ for (var i2 = 0; i2 < this.getLength(); i2++) {
583
+ for (var j = 0; j < e.getLength(); j++) num[i2 + j] ^= QRMath.gexp(QRMath.glog(this.get(i2)) + QRMath.glog(e.get(j)));
584
+ }
585
+ return new QRPolynomial(num, 0);
586
+ },
587
+ mod: function(e) {
588
+ if (this.getLength() - e.getLength() < 0) return this;
589
+ var ratio = QRMath.glog(this.get(0)) - QRMath.glog(e.get(0));
590
+ var num = new Array(this.getLength());
591
+ for (var i2 = 0; i2 < this.getLength(); i2++) num[i2] = this.get(i2);
592
+ for (var i2 = 0; i2 < e.getLength(); i2++) num[i2] ^= QRMath.gexp(QRMath.glog(e.get(i2)) + ratio);
593
+ return new QRPolynomial(num, 0).mod(e);
594
+ }
595
+ };
596
+ function QRRSBlock(totalCount, dataCount) {
597
+ this.totalCount = totalCount;
598
+ this.dataCount = dataCount;
599
+ }
600
+ QRRSBlock.RS_BLOCK_TABLE = [
601
+ [1, 26, 19],
602
+ [1, 26, 16],
603
+ [1, 26, 13],
604
+ [1, 26, 9],
605
+ [1, 44, 34],
606
+ [1, 44, 28],
607
+ [1, 44, 22],
608
+ [1, 44, 16],
609
+ [1, 70, 55],
610
+ [1, 70, 44],
611
+ [2, 35, 17],
612
+ [2, 35, 13],
613
+ [1, 100, 80],
614
+ [2, 50, 32],
615
+ [2, 50, 24],
616
+ [4, 25, 9]
617
+ ];
618
+ QRRSBlock.getRSBlocks = function(typeNumber, errorCorrectLevel) {
619
+ var rsBlock = QRRSBlock.getRsBlockTable(typeNumber, errorCorrectLevel);
620
+ if (rsBlock == void 0) throw new Error("bad rs block @ typeNumber:" + typeNumber + "/errorCorrectLevel:" + errorCorrectLevel);
621
+ var length = rsBlock.length / 3, list = [];
622
+ for (var i2 = 0; i2 < length; i2++) {
623
+ var count = rsBlock[i2 * 3 + 0], totalCount = rsBlock[i2 * 3 + 1], dataCount = rsBlock[i2 * 3 + 2];
624
+ for (var j = 0; j < count; j++) list.push(new QRRSBlock(totalCount, dataCount));
625
+ }
626
+ return list;
627
+ };
628
+ QRRSBlock.getRsBlockTable = function(typeNumber, errorCorrectLevel) {
629
+ switch (errorCorrectLevel) {
630
+ case 1:
631
+ return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0];
632
+ case 0:
633
+ return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1];
634
+ case 3:
635
+ return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2];
636
+ case 2:
637
+ return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3];
638
+ default:
639
+ return void 0;
640
+ }
641
+ };
642
+ function QRBitBuffer() {
643
+ this.buffer = [];
644
+ this.length = 0;
645
+ }
646
+ QRBitBuffer.prototype = {
647
+ get: function(index) {
648
+ return (this.buffer[Math.floor(index / 8)] >>> 7 - index % 8 & 1) == 1;
649
+ },
650
+ put: function(num, length) {
651
+ for (var i2 = 0; i2 < length; i2++) this.putBit((num >>> length - i2 - 1 & 1) == 1);
652
+ },
653
+ getLengthInBits: function() {
654
+ return this.length;
655
+ },
656
+ putBit: function(bit) {
657
+ var bufIndex = Math.floor(this.length / 8);
658
+ if (this.buffer.length <= bufIndex) this.buffer.push(0);
659
+ if (bit) this.buffer[bufIndex] |= 128 >>> this.length % 8;
660
+ this.length++;
661
+ }
662
+ };
663
+ return function(text, size) {
664
+ size = size || 180;
665
+ var qr = new QRCodeModel(0, 1);
666
+ qr.addData(text);
667
+ qr.make();
668
+ var count = qr.getModuleCount();
669
+ var margin = 2;
670
+ var total = count + margin * 2;
671
+ var cellSize = size / total;
672
+ var svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + size + " " + size + '" width="' + size + '" height="' + size + '" style="border-radius:12px;background:#ffffff;">';
673
+ svg += '<rect width="100%" height="100%" fill="#ffffff"/>';
674
+ svg += '<g fill="#0f172a">';
675
+ for (var r = 0; r < count; r++) {
676
+ for (var c = 0; c < count; c++) {
677
+ if (qr.isDark(r, c)) {
678
+ var x = ((c + margin) * cellSize).toFixed(2);
679
+ var y = ((r + margin) * cellSize).toFixed(2);
680
+ var w = (cellSize + 0.3).toFixed(2);
681
+ svg += '<rect x="' + x + '" y="' + y + '" width="' + w + '" height="' + w + '"/>';
682
+ }
683
+ }
684
+ }
685
+ svg += "</g></svg>";
686
+ return svg;
687
+ };
688
+ })();
689
+
690
+ // src/client/components/NetworkCard.jsx
691
+ var import_react = __toESM(require("react"), 1);
692
+
693
+ // src/client/i18n.js
694
+ var translations = {
695
+ zh: {
696
+ // 主面板
697
+ title: "\u8FDC\u7A0B\u4E0E\u79FB\u52A8\u7AEF",
698
+ subtitle: "\u652F\u6301 Tailscale \u865A\u62DF\u79C1\u7F51\u4E0E\u672C\u5730\u5C40\u57DF\u7F51 (Wi-Fi) \u63A5\u5165\uFF0C\u63D0\u4F9B\u626B\u7801\u5FEB\u901F\u914D\u5BF9\u4E0E\u957F\u671F\u8BBF\u95EE\u5BC6\u7801\u3002",
699
+ // 卡片 1: 网络与直连
700
+ netCardTitle: "\u7F51\u7EDC\u63A5\u5165\u5730\u5740\u4E0E\u514D\u5BC6\u76F4\u8FDE\u7BA1\u7406",
701
+ netCardDesc: "\u5728\u624B\u673A\u6D4F\u89C8\u5668\u4E2D\u76F4\u63A5\u8F93\u5165\u4E0B\u5217\u5730\u5740\uFF0C\u6216\u901A\u8FC7\u4E0B\u65B9\u4E8C\u7EF4\u7801\u626B\u7801\u914D\u5BF9\u3002\u82E5\u60A8\u5904\u4E8E\u53EF\u4FE1\u5B89\u5168\u7F51\u7EDC\u73AF\u5883\u4E2D\uFF0C\u53EF\u5F00\u542F\u514D\u5BC6\u76F4\u8FDE\u529F\u80FD\u3002",
702
+ tailscaleSectionTitle: "Tailscale \u865A\u62DF\u79C1\u7F51\u63A5\u5165 (\u63A8\u8350)",
703
+ tailscaleBadge: "Tailscale IP",
704
+ tailscaleNotConnected: "\u672A\u68C0\u6D4B\u5230 Tailscale \u865A\u62DF\u79C1\u7F51 IP",
705
+ tailscaleGuide: "\u8BF7\u786E\u4FDD\u672C\u673A\u5DF2\u5B89\u88C5\u5E76\u767B\u5F55 Tailscale\u3002\u901A\u8FC7\u865A\u62DF\u79C1\u7F51\u8BBF\u95EE\u65E2\u5B89\u5168\u53C8\u65E0\u9700\u516C\u7F51\u6620\u5C04\u3002",
706
+ lanSectionTitle: "\u672C\u5730\u5C40\u57DF\u7F51\u63A5\u5165 (LAN / Wi-Fi)",
707
+ lanBadge: "\u5C40\u57DF\u7F51 IP",
708
+ lanNotConnected: "\u672A\u68C0\u6D4B\u5230\u53EF\u7528\u7684\u672C\u5730\u5C40\u57DF\u7F51 IP",
709
+ copyUrlBtn: "\u{1F4CB} \u590D\u5236\u94FE\u63A5",
710
+ copiedTip: "\u5DF2\u6210\u529F\u590D\u5236\u94FE\u63A5\u5230\u526A\u8D34\u677F\uFF01",
711
+ directBypassToggle: "\u514D\u5BC6\u76F4\u8FDE\u6A21\u5F0F (\u76F4\u8FBE\u9875\u9762)",
712
+ directBypassOn: "\u5DF2\u5F00\u542F\u514D\u5BC6\u76F4\u8FDE",
713
+ directBypassOff: "\u5DF2\u5173\u95ED\u514D\u5BC6\u76F4\u8FDE",
714
+ tailscaleBypassDesc: "\u5F00\u542F\u540E\uFF0C\u6240\u6709\u901A\u8FC7 Tailscale \u79C1\u7F51\u8BBF\u95EE\u7684\u8BBE\u5907\u65E0\u9700\u8F93\u5165\u5BC6\u7801\u6216\u914D\u5BF9\u7801\u5373\u53EF\u76F4\u63A5\u8FDB\u5165\u63A7\u5236\u53F0\uFF08\u63A8\u8350\u5728\u4E2A\u4EBA Tailnet \u79C1\u7F51\u4E2D\u5F00\u542F\uFF09\u3002",
715
+ lanBypassWarningTitle: "\u{1F6A8} \u6781\u9AD8\u98CE\u9669\u8B66\u793A\uFF1A\u5C40\u57DF\u7F51\u514D\u5BC6\u63A7\u5236\u98CE\u9669",
716
+ lanBypassDesc: "\u5F00\u542F\u5C40\u57DF\u7F51\u514D\u5BC6\u540E\uFF0C\u8FDE\u63A5\u540C\u4E00 Wi-Fi \u8DEF\u7531\u5668\u7684\u4EFB\u610F\u8BBE\u5907\uFF08\u5305\u62EC\u5BB6\u5EAD\u8BBF\u5BA2\u3001\u516C\u5171\u8E6D\u7F51\u8005\uFF09\u5747\u65E0\u9700\u5BC6\u7801\u5373\u53EF\u76F4\u63A5\u63A7\u5236\u548C\u6267\u884C\u672C\u670D\u52A1\uFF01\u5F3A\u70C8\u5EFA\u8BAE\u4EC5\u5728\u5B8C\u5168\u79C1\u6709\u53EF\u4FE1\u7684\u7F51\u7EDC\u4E2D\u8C28\u614E\u5F00\u542F\u3002",
717
+ lanBypassConfirm: "\u3010\u{1F6A8} \u6781\u9AD8\u5B89\u5168\u98CE\u9669\u64CD\u4F5C\u786E\u8BA4\u3011\n\n\u786E\u5B9A\u8981\u5F00\u542F\u300C\u5C40\u57DF\u7F51\u514D\u5BC6\u76F4\u8FDE\u300D\u5417\uFF1F\n\n\u26A0\uFE0F \u5F00\u542F\u540E\u679C\uFF1A\n\u540C\u4E00 Wi-Fi \u4E0B\u7684\u4EFB\u4F55\u5176\u4ED6\u8BBE\u5907\uFF08\u624B\u673A/\u7535\u8111/\u8BBF\u5BA2\u8BBE\u5907\uFF09\u65E0\u9700\u8F93\u5165\u4EFB\u4F55\u5BC6\u7801\u6216\u914D\u5BF9\u7801\uFF0C\u5C31\u80FD\u76F4\u63A5\u63A5\u7BA1\u5E76\u63A7\u5236\u60A8\u7684 DSH \u5DE5\u4F5C\u533A\u4E0E\u7EC8\u7AEF\u670D\u52A1\uFF01\n\n\u8BF7\u52A1\u5FC5\u786E\u8BA4\u60A8\u5F53\u524D\u8FDE\u63A5\u7684\u662F\u5B8C\u5168\u53EF\u4FE1\u7684\u5BB6\u5EAD\u79C1\u6709 Wi-Fi\uFF0C\u5207\u52FF\u5728\u516C\u5171\u7F51\u7EDC\uFF08\u5496\u5561\u5385/\u9152\u5E97/\u516C\u53F8 Wi-Fi\uFF09\u4E2D\u5F00\u542F\uFF01",
718
+ // 卡片 2: 配对码与二维码
719
+ pairingCardTitle: "\u624B\u673A\u5FEB\u901F\u914D\u5BF9\u4E0E\u626B\u7801\u63A5\u5165",
720
+ pairingCardDesc: "\u624B\u673A\u4E0E\u7535\u8111\u8FDE\u63A5\u540C\u4E00\u7F51\u7EDC\u540E\uFF0C\u4F7F\u7528\u624B\u673A\u5FAE\u4FE1\u6216\u76F8\u673A\u76F4\u63A5\u626B\u63CF\u4E0B\u65B9\u4E8C\u7EF4\u7801\uFF0C\u6216\u624B\u52A8\u8F93\u5165 6 \u4F4D\u77ED\u671F\u914D\u5BF9\u7801\u5373\u53EF\u5FEB\u901F\u5B8C\u6210\u8EAB\u4EFD\u6388\u6743\u4E0E\u8BBE\u5907\u7ED1\u5B9A\u3002",
721
+ shortCodeTitle: "6 \u4F4D\u77ED\u671F\u52A8\u6001\u914D\u5BF9\u7801",
722
+ codeValidUntil: "\u6709\u6548\u671F\u5269\u4F59\uFF1A",
723
+ codeExpired: "\u914D\u5BF9\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u70B9\u51FB\u5237\u65B0\u91CD\u65B0\u751F\u6210",
724
+ clickToReveal: "\u70B9\u51FB\u67E5\u770B 6 \u4F4D\u914D\u5BF9\u7801",
725
+ clickToHide: "\u70B9\u51FB\u9690\u85CF",
726
+ refreshCodeBtn: "\u{1F504} \u5237\u65B0\u914D\u5BF9\u7801",
727
+ qrTitle: "\u626B\u7801\u514D\u8F93\u5BC6\u7801\u76F4\u8FDE",
728
+ toggleQrShow: "\u663E\u793A\u914D\u5BF9\u4E8C\u7EF4\u7801",
729
+ toggleQrHide: "\u6536\u8D77\u4E8C\u7EF4\u7801",
730
+ switchTailscaleTab: "Tailscale \u626B\u7801",
731
+ switchLanTab: "\u5C40\u57DF\u7F51 (Wi-Fi) \u626B\u7801",
732
+ copyDirectAuthLink: "\u{1F4CB} \u590D\u5236\u5E26\u914D\u5BF9\u7801\u76F4\u94FE",
733
+ // 卡片 3: 长期密码
734
+ secretCardTitle: "\u8BBE\u7F6E\u957F\u671F\u8BBF\u95EE\u5BC6\u7801 (\u53EF\u9009)",
735
+ secretCardDesc: "\u5982\u679C\u4E0D\u5E0C\u671B\u6BCF\u6B21\u4F7F\u7528 6 \u4F4D\u77ED\u671F\u914D\u5BF9\u7801\uFF0C\u53EF\u5728\u6B64\u8BBE\u7F6E\u4E00\u4E2A\u56FA\u5B9A\u5BC6\u7801\u3002\u5728\u624B\u673A\u7AEF\u8F93\u5165\u6B64\u5BC6\u7801\u4E5F\u53EF\u5B8C\u6210\u9A8C\u8BC1\u5E76\u6362\u53D6\u6301\u4E45\u6388\u6743\u3002",
736
+ secretConfigured: "\u2705 \u5F53\u524D\u5DF2\u914D\u7F6E\u957F\u671F\u8BBF\u95EE\u5BC6\u7801 (\u5DF2\u5B89\u5168\u52A0\u76D0\u52A0\u5BC6\u843D\u76D8\u5B58\u50A8)",
737
+ secretNotConfigured: "\u26AA \u5F53\u524D\u672A\u914D\u7F6E\u957F\u671F\u8BBF\u95EE\u5BC6\u7801 (\u4EC5\u80FD\u901A\u8FC7\u4E34\u65F6\u914D\u5BF9\u7801\u6388\u6743)",
738
+ secretInputPlaceholder: "\u8BF7\u8F93\u5165 6 \u4F4D\u4EE5\u4E0A\u5B57\u6BCD\u4E0E\u6570\u5B57\u7EC4\u5408\u5BC6\u7801",
739
+ secretInputLabel: "\u957F\u671F\u5BC6\u7801\uFF1A",
740
+ showPassword: "\u{1F441}\uFE0F \u663E\u793A\u5BC6\u7801",
741
+ hidePassword: "\u{1F648} \u9690\u85CF\u5BC6\u7801",
742
+ saveSecretBtn: "\u{1F4BE} \u4FDD\u5B58\u5BC6\u7801",
743
+ clearSecretBtn: "\u{1F5D1}\uFE0F \u6E05\u9664\u5BC6\u7801",
744
+ secretWeakTip: "\u5BC6\u7801\u5FC5\u987B\u81F3\u5C11 6 \u4F4D\uFF0C\u4E14\u540C\u65F6\u5305\u542B\u5B57\u6BCD\u548C\u6570\u5B57",
745
+ clearSecretConfirm: "\u786E\u5B9A\u8981\u6E05\u9664\u5DF2\u8BBE\u7F6E\u7684\u957F\u671F\u5BC6\u7801\u5417\uFF1F\u6E05\u9664\u540E\u5C06\u65E0\u6CD5\u518D\u4F7F\u7528\u56FA\u5B9A\u5BC6\u7801\u767B\u5F55\u3002",
746
+ // 卡片 4: 设备列表
747
+ deviceCardTitle: "\u5DF2\u6388\u6743\u63A5\u5165\u8BBE\u5907\u5217\u8868",
748
+ deviceCardDesc: "\u6240\u6709\u5DF2\u901A\u8FC7\u77ED\u671F\u914D\u5BF9\u7801\u3001\u957F\u671F\u5BC6\u7801\u6216\u514D\u5BC6\u76F4\u8FDE\u6210\u529F\u63A5\u5165\u7684\u79FB\u52A8\u8BBE\u5907\u3002\u60A8\u53EF\u4EE5\u968F\u65F6\u64A4\u9500\u5355\u4E2A\u8BBE\u5907\uFF0C\u6216\u4E00\u952E\u6CE8\u9500\u5168\u90E8\u8BBE\u5907\u3002",
749
+ authDevicesCount: "\u53F0\u8BBE\u5907\u5DF2\u6388\u6743",
750
+ revokeAllBtn: "\u{1F5D1}\uFE0F \u4E00\u952E\u6CE8\u9500\u6240\u6709\u8BBE\u5907",
751
+ revokeAllConfirm: "\u786E\u5B9A\u8981\u64A4\u9500\u5E76\u8E22\u4E0B\u7EBF\u6240\u6709\u5DF2\u6388\u6743\u7684\u8BBE\u5907\u5417\uFF1F",
752
+ noDevices: "\u6682\u65E0\u5DF2\u6388\u6743\u8BBE\u5907\uFF0C\u4F7F\u7528\u624B\u673A\u626B\u7801\u914D\u5BF9\u540E\u5C06\u5728\u6B64\u81EA\u52A8\u5217\u51FA\u3002",
753
+ deviceOnlineNow: "\u5728\u7EBF",
754
+ deviceOffline: "\u79BB\u7EBF",
755
+ deviceBypassLabel: "\u514D\u5BC6\u76F4\u8FDE",
756
+ devicePasscodeLabel: "\u914D\u5BF9\u7801\u6388\u6743",
757
+ deviceSecretLabel: "\u957F\u671F\u5BC6\u7801",
758
+ deviceFirstSeen: "\u9996\u6B21\u6388\u6743\uFF1A",
759
+ deviceLastSeen: "\u6700\u8FD1\u6D3B\u8DC3\uFF1A",
760
+ revokeDeviceBtn: "\u8E22\u4E0B\u7EBF",
761
+ revokeDeviceConfirm: "\u786E\u5B9A\u8981\u64A4\u9500\u8FD9\u53F0\u8BBE\u5907\u7684\u8BBF\u95EE\u6743\u9650\u5417\uFF1F",
762
+ // 卡片 5: 防暴力破解与审计
763
+ securityCardTitle: "\u9632\u66B4\u529B\u7834\u89E3\u4E0E IP \u5B89\u5168\u5BA1\u8BA1",
764
+ securityCardDesc: "\u5B9E\u65F6\u76D1\u63A7\u6240\u6709\u5916\u90E8\u8BBF\u95EE IP \u7684\u8BF7\u6C42\u9891\u7387\u4E0E\u8BA4\u8BC1\u5931\u8D25\u6B21\u6570\u3002\u8FDE\u7EED\u8F93\u9519 5 \u6B21\u5C06\u81EA\u52A8\u9501\u5B9A IP\uFF0C\u9632\u6B62\u5BC6\u7801\u88AB\u6076\u610F\u66B4\u529B\u679A\u4E3E\u3002",
765
+ securityNormal: "\u2705 \u6240\u6709\u5916\u90E8\u8BBF\u95EE IP \u72B6\u6001\u6B63\u5E38\uFF0C\u672A\u68C0\u6D4B\u5230\u66B4\u529B\u7834\u89E3\u6216\u5F02\u5E38\u7206\u7834\u98CE\u9669\u3002",
766
+ unlockIpBtn: "\u{1F513} \u89E3\u9664\u9501\u5B9A",
767
+ unlockIpConfirm: "\u786E\u5B9A\u8981\u4E3A IP {ip} \u89E3\u9664\u5C01\u9501\u5417\uFF1F",
768
+ clearSecurityLogBtn: "\u{1F5D1}\uFE0F \u6E05\u7A7A\u5BA1\u8BA1\u65E5\u5FD7",
769
+ clearSecurityLogConfirm: "\u786E\u5B9A\u8981\u6E05\u7A7A\u6240\u6709 IP \u8BBF\u95EE\u4E0E\u5B89\u5168\u5BA1\u8BA1\u65E5\u5FD7\u5417\uFF1F",
770
+ thIp: "\u8BBF\u95EE\u6765\u6E90 IP",
771
+ thStatus: "\u5F53\u524D\u72B6\u6001",
772
+ thFailed: "\u5931\u8D25/\u603B\u6B21\u6570",
773
+ thLastSeen: "\u6700\u8FD1\u8BBF\u95EE",
774
+ thAction: "\u64CD\u4F5C",
775
+ statusLocked: "\u{1F6AB} \u5DF2\u88AB\u9501\u5B9A",
776
+ statusNormal: "\u{1F7E2} \u6B63\u5E38",
777
+ // 卡片 6: 本地存储说明
778
+ storageCardTitle: "\u672C\u5730\u6570\u636E\u4E0E\u5BC6\u94A5\u6301\u4E45\u5316\u5B58\u50A8\u8BF4\u660E",
779
+ storageCardDesc: "\u6240\u6709\u654F\u611F\u51ED\u8BC1\u5747\u5728\u672C\u5730\u7ECF\u8FC7\u52A0\u76D0\u54C8\u5E0C\u6216\u975E\u5BF9\u79F0\u52A0\u5BC6\u4FDD\u62A4\uFF0C\u4E0D\u4F9D\u8D56\u4EFB\u4F55\u7B2C\u4E09\u65B9\u4E91\u7AEF\u9274\u6743\u4E2D\u5FC3\u3002",
780
+ storageDeviceFile: "\u5DF2\u6388\u6743\u8BBE\u5907\u4E0E\u4F1A\u8BDD\u7F13\u5B58\u6587\u4EF6\uFF1A",
781
+ storageRsaFile: "\u670D\u52A1\u7AEF RSA \u79C1\u94A5\u6587\u4EF6 (0o600 \u6743\u9650\u4FDD\u62A4)\uFF1A",
782
+ storageStylesFile: "\u79FB\u52A8\u7AEF\u6837\u5F0F\u7247\u6BB5\u6587\u4EF6 (\u53EF\u72EC\u7ACB\u542F\u505C\u7684 CSS \u5C0F\u63D2\u4EF6)\uFF1A",
783
+ // 卡片 7: 全局高级配置
784
+ configCardTitle: "\u5168\u5C40\u9AD8\u7EA7\u5B89\u5168\u53C2\u6570\u914D\u7F6E",
785
+ configCardDesc: "\u8C03\u6574\u5355 IP \u8BBF\u95EE\u9891\u7387\u9650\u5236\u3001\u8F93\u9519\u5C01\u7981\u9608\u503C\u4E0E\u9501\u5B9A\u6301\u7EED\u65F6\u95F4\u3002\u70B9\u51FB\u4FDD\u5B58\u5C06\u7ACB\u5373\u751F\u6548\u5E76\u4FDD\u5B58\u5230 ~/.dsh/remote-mobile/settings.json\u3002",
786
+ copySettingsPathBtn: "\u{1F4CB} \u590D\u5236 settings.json \u8DEF\u5F84",
787
+ fieldVisitsLimit: "\u23F1\uFE0F \u6BCF\u5206\u949F\u8BBF\u95EE\u4E0A\u9650 (\u6B21/\u5206)",
788
+ fieldVisitsLimitHint: "\u5355 IP 1\u5206\u949F\u5185\u6253\u5F00\u767B\u5F55\u9875\u8D85\u8FC7\u6B64\u503C\u5C06\u89E6\u53D1\u9650\u9891",
789
+ fieldFailedLimit: "\u{1F6E1}\uFE0F \u8F93\u9519\u5BC6\u7801\u5C01\u7981\u9608\u503C (\u6B21)",
790
+ fieldFailedLimitHint: "\u8FDE\u7EED\u8F93\u9519\u914D\u5BF9\u7801/\u5BC6\u7801\u8FBE\u5230\u6B64\u503C\u5C06\u5C01\u9501\u8BE5 IP",
791
+ fieldLockDuration: "\u{1F6AB} \u89E6\u53D1\u5C01\u9501\u6301\u7EED\u65F6\u95F4 (\u5206\u949F)",
792
+ fieldLockDurationHint: "IP \u88AB\u9501\u5B9A\u540E\u7684\u51B7\u5374\u7B49\u5F85\u65F6\u957F (\u9ED8\u8BA4 15 \u5206\u949F)",
793
+ restoreDefaultsBtn: "\u{1F504} \u6062\u590D\u9ED8\u8BA4\u53C2\u6570",
794
+ saveConfigBtn: "\u{1F4BE} \u4FDD\u5B58\u53C2\u6570\u914D\u7F6E",
795
+ savingConfigBtn: "\u6B63\u5728\u4FDD\u5B58...",
796
+ saveConfigConfirm: "\u786E\u5B9A\u8981\u4FDD\u5B58\u5F53\u524D\u9AD8\u7EA7\u5B89\u5168\u53C2\u6570\u914D\u7F6E\u5417\uFF1F\u4FDD\u5B58\u540E\u5C06\u7ACB\u5373\u751F\u6548\u5E76\u4FDD\u5B58\u5230 ~/.dsh/remote-mobile/settings.json\u3002",
797
+ restoreDefaultsConfirm: "\u786E\u5B9A\u8981\u5C06\u5B89\u5168\u53C2\u6570\u6062\u590D\u4E3A\u7CFB\u7EDF\u9ED8\u8BA4\u503C\uFF0860\u6B21/\u5206\u30015\u6B21\u5931\u8D25\u3001\u9501\u5B9A15\u5206\u949F\uFF09\u5417\uFF1F\u786E\u8BA4\u540E\u5C06\u76F4\u63A5\u751F\u6548\u5E76\u81EA\u52A8\u540C\u6B65\u3002",
798
+ saveConfigSuccessToast: "\u9AD8\u7EA7\u5B89\u5168\u53C2\u6570\u5DF2\u6210\u529F\u4FDD\u5B58\u5E76\u540C\u6B65\u5230 settings.json\uFF01",
799
+ restoreDefaultsSuccessToast: "\u5DF2\u6210\u529F\u6062\u590D\u9ED8\u8BA4\u5B89\u5168\u53C2\u6570\u5E76\u7ACB\u5373\u751F\u6548\uFF01",
800
+ saveConfigFailTip: "\u53C2\u6570\u5FC5\u987B\u4E3A\u5927\u4E8E 0 \u7684\u6709\u6548\u6574\u6570",
801
+ // 卡片 8: 移动端样式片段(样式小插件)
802
+ tabAccess: "\u63A5\u5165",
803
+ tabDevices: "\u8BBE\u5907\u4E0E\u5B89\u5168",
804
+ tabStyle: "\u6837\u5F0F\u8986\u5199",
805
+ tabStorage: "\u672C\u5730\u6570\u636E",
806
+ stylesCardTitle: "\u79FB\u52A8\u7AEF\u6837\u5F0F\u7247\u6BB5\uFF08\u6837\u5F0F\u5C0F\u63D2\u4EF6\uFF09",
807
+ stylesCardDesc: "\u4EE5\u53EF\u72EC\u7ACB\u542F\u505C\u7684 CSS \u7247\u6BB5\u65B9\u5F0F\u9002\u914D DSH Web \u754C\u9762\u3002\u5185\u7F6E\u9884\u8BBE\u6309\u754C\u9762\u533A\u57DF\u5206\u4E3A\u4FA7\u8FB9\u680F\u3001\u8BBE\u7F6E\u9762\u677F\u4E0E\u6B63\u6587\u4E09\u6BB5\uFF0C\u6BCF\u6BB5\u5747\u53EF\u5206\u522B\u8BBE\u7F6E PC / \u79FB\u52A8\u7AEF\u662F\u5426\u542F\u7528\uFF1B\u4F60\u4E5F\u53EF\u4EE5\u7C98\u8D34\u81EA\u5DF1\u7684 CSS \u5C0F\u63D2\u4EF6\uFF0C\u4FDD\u5B58\u540E\u4E0B\u4E00\u6B21\u9875\u9762\u52A0\u8F7D\u7ACB\u5373\u751F\u6548\uFF0C\u65E0\u9700\u91CD\u542F\u3002",
808
+ stylesPresetsTitle: "\u{1F9E9} \u5185\u7F6E\u9884\u8BBE\u7247\u6BB5",
809
+ stylesCustomTitle: "\u270D\uFE0F \u81EA\u5B9A\u4E49\u7247\u6BB5",
810
+ stylesPcShort: "PC",
811
+ stylesMobileShort: "\u79FB\u52A8\u7AEF",
812
+ stylesBothDevices: "PC + \u79FB\u52A8\u7AEF",
813
+ stylesMobileOnlyBadge: "\u4EC5\u79FB\u52A8\u7AEF",
814
+ stylesPcOnlyBadge: "\u4EC5 PC",
815
+ stylesDisabled: "\u5DF2\u505C\u7528",
816
+ stylesViewCss: "\u67E5\u770B CSS",
817
+ stylesHideCss: "\u6536\u8D77 CSS",
818
+ stylesCopyCss: "\u{1F4CB} \u590D\u5236\u6837\u5F0F",
819
+ stylesCopyCssDone: "\u6837\u5F0F\u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F\uFF01",
820
+ stylesNamePlaceholder: "\u7247\u6BB5\u540D\u79F0\uFF08\u5FC5\u586B\uFF09",
821
+ stylesCssPlaceholder: "\u7C98\u8D34\u6216\u7F16\u5199 CSS \u4EE3\u7801\u7247\u6BB5\u2026\uFF08\u79FB\u52A8\u7AEF\u53EF\u7528 html[data-dsh-mobile] \u4F5C\u4E3A\u4F5C\u7528\u57DF\u9009\u62E9\u5668\uFF09",
822
+ stylesAddNew: "\uFF0B \u65B0\u589E\u81EA\u5B9A\u4E49\u7247\u6BB5",
823
+ stylesSave: "\u{1F4BE} \u4FDD\u5B58\u7247\u6BB5",
824
+ stylesCancelEdit: "\u53D6\u6D88",
825
+ stylesEditBtn: "\u7F16\u8F91",
826
+ stylesDeleteBtn: "\u5220\u9664",
827
+ stylesResetAll: "\u{1F504} \u6062\u590D\u9ED8\u8BA4\u542F\u505C",
828
+ stylesEmptyCustom: "\u6682\u65E0\u81EA\u5B9A\u4E49\u7247\u6BB5\uFF0C\u70B9\u51FB\u300C\u65B0\u589E\u81EA\u5B9A\u4E49\u7247\u6BB5\u300D\u5F00\u59CB\u3002",
829
+ stylesLoadFail: "\u52A0\u8F7D\u6837\u5F0F\u7247\u6BB5\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
830
+ stylesSaveSuccess: "\u6837\u5F0F\u7247\u6BB5\u5DF2\u4FDD\u5B58\uFF0C\u5237\u65B0\u9875\u9762\u5373\u53EF\u770B\u5230\u6548\u679C",
831
+ stylesDeleteConfirm: "\u786E\u5B9A\u8981\u5220\u9664\u8BE5\u81EA\u5B9A\u4E49\u6837\u5F0F\u7247\u6BB5\u5417\uFF1F",
832
+ stylesResetConfirm: "\u786E\u5B9A\u8981\u6062\u590D\u6240\u6709\u7247\u6BB5\u7684\u9ED8\u8BA4\u542F\u505C\u72B6\u6001\u5417\uFF1F\uFF08\u5185\u7F6E\u9884\u8BBE\uFF1APC \u5173\u95ED\u3001\u79FB\u52A8\u7AEF\u542F\u7528\uFF1B\u81EA\u5B9A\u4E49\u7247\u6BB5\uFF1APC \u5173\u95ED\u3001\u79FB\u52A8\u7AEF\u542F\u7528\uFF09",
833
+ stylesFieldsRequired: "\u8BF7\u586B\u5199\u7247\u6BB5\u540D\u79F0\u4E0E CSS \u5185\u5BB9",
834
+ stylesHint: "\u{1F4A1} \u63D0\u793A\uFF1A\u6BCF\u6BB5\u9884\u8BBE\u4E0E\u81EA\u5B9A\u4E49\u7247\u6BB5\u90FD\u652F\u6301 PC / \u79FB\u52A8\u7AEF\u5206\u522B\u542F\u7528\u3002\u79FB\u52A8\u7AEF\u8BF7\u6C42\u4F1A\u7ED9 <html> \u6253\u4E0A data-dsh-mobile \u6807\u8BB0\uFF1B\u5185\u7F6E\u9884\u8BBE\u9762\u5411\u79FB\u52A8\u7AEF\u4F18\u5316\uFF08\u9ED8\u8BA4 PC \u5173\u95ED\uFF09\uFF0C\u5728 PC \u4E0A\u542F\u7528\u65F6\u8BF7\u786E\u4FDD\u7247\u6BB5\u4E0D\u4F9D\u8D56 data-dsh-mobile \u4F5C\u7528\u57DF\u3002",
835
+ // 插件共存警示横幅
836
+ conflictBannerTitle: "\u68C0\u6D4B\u5230\u5176\u4ED6\u8FDC\u7A0B\u63A5\u5165\u63D2\u4EF6",
837
+ conflictUnknownPlugin: "\u53E6\u4E00\u6B3E\u8FDC\u7A0B\u63A5\u5165\u7C7B\u63D2\u4EF6",
838
+ conflictBannerBody: "\u68C0\u6D4B\u5230 {plugin} \u5DF2\u5360\u7528\u914D\u5BF9\u5171\u4EAB\u670D\u52A1\uFF08remoteWebUiPairing\uFF09\uFF0C\u672C\u63D2\u4EF6\u5DF2\u81EA\u52A8\u8BA9\u51FA\u4EE5\u4FDD\u8BC1\u6B63\u5E38\u542F\u52A8\u3002\u4E24\u5957\u8FDC\u7A0B\u63A5\u5165\u529F\u80FD\u5E76\u5B58\u53EF\u80FD\u51FA\u73B0\u91CD\u590D\u5165\u53E3\u4E0E\u72B6\u6001\u4E0D\u4E00\u81F4\uFF0C\u5EFA\u8BAE\u53EA\u4FDD\u7559\u5176\u4E00\u3002\u53EF\u590D\u5236\u4E0B\u65B9\u8BCA\u65AD\u4FE1\u606F\uFF0C\u7C98\u8D34\u7ED9 AI \u52A9\u624B\u6309\u6B65\u9AA4\u5904\u7406\u3002",
839
+ conflictCopyBtn: "\u{1F4CB} \u590D\u5236\u8BCA\u65AD\u4E0E\u4FEE\u590D\u4FE1\u606F",
840
+ conflictCopiedTip: "\u8BCA\u65AD\u4FE1\u606F\u5DF2\u590D\u5236\uFF0C\u53EF\u76F4\u63A5\u7C98\u8D34\u7ED9 AI \u52A9\u624B\u534F\u52A9\u5904\u7406",
841
+ conflictReportText: "[DSH \u63D2\u4EF6\u51B2\u7A81\u8BCA\u65AD\u62A5\u544A]\n\u73B0\u8C61\uFF1Adsh-remote-mobile \u8BBE\u7F6E\u9762\u677F\u63D0\u793A\u300C\u68C0\u6D4B\u5230\u5176\u4ED6\u8FDC\u7A0B\u63A5\u5165\u63D2\u4EF6\u300D\u3002\n\u539F\u56E0\uFF1A\u53E6\u4E00\u6B3E\u8FDC\u7A0B/Web \u63A5\u5165\u7C7B\u63D2\u4EF6\u5360\u7528\u4E86 Cordis \u5171\u4EAB\u670D\u52A1 remoteWebUiPairing\uFF0Cdsh-remote-mobile \u4E3A\u4FDD\u8BC1\u6B63\u5E38\u542F\u52A8\u5DF2\u81EA\u52A8\u8BA9\u51FA\u8BE5\u670D\u52A1\uFF0C\u5F53\u524D\u4E24\u5957\u8FDC\u7A0B\u63A5\u5165\u529F\u80FD\u5E76\u5B58\u3002\n\u5360\u7528\u65B9\u63D2\u4EF6\u5305\u540D\uFF1A{plugin}\n\u5360\u7528\u65B9 loader entry id\uFF1A{entryId}\n\u5904\u7406\u65B9\u6848\uFF08\u4E8C\u9009\u4E00\uFF09\uFF1A\n\u65B9\u6848 A\uFF08\u4FDD\u7559 dsh-remote-mobile\uFF0C\u7981\u7528\u5360\u7528\u65B9\uFF09\uFF1A\n1. \u6253\u5F00\u914D\u7F6E\u6587\u4EF6 ~/.dsh/profiles/web/cordis.patch.yml\n2. \u5728\u6587\u4EF6\u672B\u5C3E\u8FFD\u52A0\u4EE5\u4E0B\u4E24\u884C\uFF08\u6CE8\u610F YAML \u7F29\u8FDB\uFF0C\u7B2C\u4E8C\u884C\u524D\u6709\u4E24\u4E2A\u7A7A\u683C\uFF09\uFF1A\n- id: {entryId}\n disabled: true\n3. \u7ED3\u675F\u5F53\u524D\u6B63\u5728\u8FD0\u884C\u7684 dsh web \u8FDB\u7A0B\uFF0C\u91CD\u65B0\u6267\u884C dsh web --no-open\n\u65B9\u6848 B\uFF08\u4FDD\u7559\u5360\u7528\u65B9\uFF0C\u5378\u8F7D dsh-remote-mobile\uFF09\uFF1A\n\u6267\u884C\u547D\u4EE4 dsh plugin --profile web remove dsh-remote-mobile \u540E\u91CD\u542F dsh web",
842
+ conflictReportNoEntryIdText: "[DSH \u63D2\u4EF6\u51B2\u7A81\u8BCA\u65AD\u62A5\u544A]\n\u73B0\u8C61\uFF1Adsh-remote-mobile \u8BBE\u7F6E\u9762\u677F\u63D0\u793A\u300C\u68C0\u6D4B\u5230\u5176\u4ED6\u8FDC\u7A0B\u63A5\u5165\u63D2\u4EF6\u300D\u3002\n\u539F\u56E0\uFF1A\u53E6\u4E00\u6B3E\u8FDC\u7A0B/Web \u63A5\u5165\u7C7B\u63D2\u4EF6\u5360\u7528\u4E86 Cordis \u5171\u4EAB\u670D\u52A1 remoteWebUiPairing\uFF0Cdsh-remote-mobile \u4E3A\u4FDD\u8BC1\u6B63\u5E38\u542F\u52A8\u5DF2\u81EA\u52A8\u8BA9\u51FA\u8BE5\u670D\u52A1\uFF0C\u5F53\u524D\u4E24\u5957\u8FDC\u7A0B\u63A5\u5165\u529F\u80FD\u5E76\u5B58\u3002\n\u5360\u7528\u65B9\u63D2\u4EF6\u5305\u540D\uFF1A{plugin}\n\u5360\u7528\u65B9 loader entry id\uFF1A\u672A\u80FD\u81EA\u52A8\u8BC6\u522B\n\u5904\u7406\u65B9\u6848\uFF08\u4E8C\u9009\u4E00\uFF09\uFF1A\n\u65B9\u6848 A\uFF08\u4FDD\u7559 dsh-remote-mobile\uFF0C\u7981\u7528\u5360\u7528\u65B9\uFF09\uFF1A\n1. \u6267\u884C dsh --dump-config \u67E5\u770B\u5168\u90E8 loader entry\uFF0C\u627E\u5230\u5360\u7528\u65B9\u63D2\u4EF6\u5BF9\u5E94\u7684\u6761\u76EE id\n2. \u6253\u5F00\u914D\u7F6E\u6587\u4EF6 ~/.dsh/profiles/web/cordis.patch.yml\uFF0C\u5728\u6587\u4EF6\u672B\u5C3E\u8FFD\u52A0\uFF08\u6CE8\u610F YAML \u7F29\u8FDB\uFF09\uFF1A\n- id: <\u5360\u7528\u65B9\u6761\u76EEid>\n disabled: true\n3. \u7ED3\u675F\u5F53\u524D\u6B63\u5728\u8FD0\u884C\u7684 dsh web \u8FDB\u7A0B\uFF0C\u91CD\u65B0\u6267\u884C dsh web --no-open\n\u65B9\u6848 B\uFF08\u4FDD\u7559\u5360\u7528\u65B9\uFF0C\u5378\u8F7D dsh-remote-mobile\uFF09\uFF1A\n\u6267\u884C\u547D\u4EE4 dsh plugin --profile web remove dsh-remote-mobile \u540E\u91CD\u542F dsh web",
843
+ conflictBannerDismiss: "\u77E5\u9053\u4E86",
844
+ // Toast 提示
845
+ toastDeviceConnected: "\u{1F389} \u65B0\u8BBE\u5907\u63A5\u5165\u6210\u529F\uFF1A{name} ({ip})",
846
+ toastDeviceOnline: "\u{1F4F6} \u8BBE\u5907\u91CD\u65B0\u4E0A\u7EBF\uFF1A{name} ({ip})",
847
+ toastIpLocked: "\u{1F6AB} \u89E6\u53D1\u5B89\u5168\u8B66\u62A5\uFF1AIP {ip} \u8FDE\u7EED\u8BA4\u8BC1\u5931\u8D25\u5DF2\u88AB\u9501\u5B9A\uFF01"
848
+ },
849
+ en: {
850
+ // Main
851
+ title: "Remote & Mobile Access",
852
+ subtitle: "Enable access via Tailscale private network and local Wi-Fi, featuring QR code pairing and persistent passwords.",
853
+ // Card 1: Network & Bypass
854
+ netCardTitle: "Network Access & Bypass Management",
855
+ netCardDesc: "Enter the address below in your mobile browser, or scan the QR code to pair. You can enable direct bypass if you are in a trusted private network.",
856
+ tailscaleSectionTitle: "Tailscale Private Network (Recommended)",
857
+ tailscaleBadge: "Tailscale IP",
858
+ tailscaleNotConnected: "No Tailscale private IP detected",
859
+ tailscaleGuide: "Please ensure Tailscale is installed and logged in on this machine for secure access without port forwarding.",
860
+ lanSectionTitle: "Local Area Network (LAN / Wi-Fi)",
861
+ lanBadge: "LAN IP",
862
+ lanNotConnected: "No available local LAN IP detected",
863
+ copyUrlBtn: "\u{1F4CB} Copy Link",
864
+ copiedTip: "Link copied to clipboard!",
865
+ directBypassToggle: "Direct Bypass Mode (Passwordless)",
866
+ directBypassOn: "Bypass Enabled",
867
+ directBypassOff: "Bypass Disabled",
868
+ tailscaleBypassDesc: "When enabled, any device connecting via Tailscale can directly access the console without passwords or pairing codes.",
869
+ lanBypassWarningTitle: "\u{1F6A8} HIGH RISK SECURITY WARNING: LAN BYPASS",
870
+ lanBypassDesc: "Enabling LAN Bypass allows ANY device connected to the same Wi-Fi router (including guest/shared users) to execute commands and control DSH without any authentication. ONLY enable this in completely trusted home private networks!",
871
+ lanBypassConfirm: "[\u{1F6A8} CRITICAL SECURITY RISK CONFIRMATION]\n\nAre you sure you want to enable LAN Passwordless Bypass?\n\n\u26A0\uFE0F Consequences:\nANY device on the same Wi-Fi network (smartphones, PCs, guests) will gain FULL access and control over your DSH workspace without passwords or codes!\n\nDo NOT enable this on public networks (Cafes, Hotels, Office Wi-Fi)!",
872
+ // Card 2: Pairing & QR
873
+ pairingCardTitle: "Quick Pairing & QR Code Access",
874
+ pairingCardDesc: "When both devices are on the same network, scan the QR code or enter the 6-digit dynamic pairing code to authenticate.",
875
+ shortCodeTitle: "6-Digit Dynamic Pairing Code",
876
+ codeValidUntil: "Expires in: ",
877
+ codeExpired: "Pairing code expired. Click refresh to generate a new one.",
878
+ clickToReveal: "Click to reveal 6-digit code",
879
+ clickToHide: "Click to hide",
880
+ refreshCodeBtn: "\u{1F504} Refresh Code",
881
+ qrTitle: "Scan QR Code for Instant Access",
882
+ toggleQrShow: "Show QR Code",
883
+ toggleQrHide: "Hide QR Code",
884
+ switchTailscaleTab: "Tailscale QR",
885
+ switchLanTab: "LAN (Wi-Fi) QR",
886
+ copyDirectAuthLink: "\u{1F4CB} Copy Link with Code",
887
+ // Card 3: Persistent Password
888
+ secretCardTitle: "Configure Persistent Password (Optional)",
889
+ secretCardDesc: "If you prefer not using 6-digit short codes every time, set a fixed password here. Entering this password on mobile grants persistent access.",
890
+ secretConfigured: "\u2705 Persistent password is configured (securely salted & encrypted)",
891
+ secretNotConfigured: "\u26AA No persistent password set (authentication via temporary codes only)",
892
+ secretInputPlaceholder: "Enter a password with at least 6 alphanumeric characters",
893
+ secretInputLabel: "Password:",
894
+ showPassword: "\u{1F441}\uFE0F Show Password",
895
+ hidePassword: "\u{1F648} Hide Password",
896
+ saveSecretBtn: "\u{1F4BE} Save Password",
897
+ clearSecretBtn: "\u{1F5D1}\uFE0F Clear Password",
898
+ secretWeakTip: "Password must be at least 6 characters and contain both letters and numbers",
899
+ clearSecretConfirm: "Are you sure you want to clear the persistent password? You will no longer be able to log in with a fixed password.",
900
+ // Card 4: Device List
901
+ deviceCardTitle: "Authorized Devices List",
902
+ deviceCardDesc: "All mobile devices successfully authorized via pairing code, persistent password, or bypass. You can revoke access at any time.",
903
+ authDevicesCount: "device(s) authorized",
904
+ revokeAllBtn: "\u{1F5D1}\uFE0F Revoke All Devices",
905
+ revokeAllConfirm: "Are you sure you want to disconnect and revoke all authorized devices?",
906
+ noDevices: "No authorized devices yet. Scan the QR code with your mobile device to connect.",
907
+ deviceOnlineNow: "Online",
908
+ deviceOffline: "Offline",
909
+ deviceBypassLabel: "Bypass",
910
+ devicePasscodeLabel: "Code",
911
+ deviceSecretLabel: "Password",
912
+ deviceFirstSeen: "Authorized: ",
913
+ deviceLastSeen: "Last active: ",
914
+ revokeDeviceBtn: "Revoke",
915
+ revokeDeviceConfirm: "Are you sure you want to revoke access for this device?",
916
+ // Card 5: Security & Audit
917
+ securityCardTitle: "Brute-Force Defense & IP Audit",
918
+ securityCardDesc: "Real-time monitoring of external access IP request frequencies and failure rates. Automatically locks IPs after 5 consecutive failures.",
919
+ securityNormal: "\u2705 All external IPs are in good standing. No brute-force attacks detected.",
920
+ unlockIpBtn: "\u{1F513} Unlock IP",
921
+ unlockIpConfirm: "Are you sure you want to unlock IP {ip}?",
922
+ clearSecurityLogBtn: "\u{1F5D1}\uFE0F Clear Audit Logs",
923
+ clearSecurityLogConfirm: "Are you sure you want to clear all IP access and security audit logs?",
924
+ thIp: "Client IP",
925
+ thStatus: "Status",
926
+ thFailed: "Fail / Total",
927
+ thLastSeen: "Last Seen",
928
+ thAction: "Actions",
929
+ statusLocked: "\u{1F6AB} Locked",
930
+ statusNormal: "\u{1F7E2} Normal",
931
+ // Card 6: Storage
932
+ storageCardTitle: "Local Storage & Key Persistence",
933
+ storageCardDesc: "All sensitive credentials are protected locally via slow salt hashing and asymmetric RSA cryptography, without cloud dependencies.",
934
+ storageDeviceFile: "Authorized devices & session cache:",
935
+ storageRsaFile: "Server RSA private key (0o600 protected):",
936
+ storageStylesFile: "Mobile style snippets file (toggleable CSS mini-plugins):",
937
+ // Card 7: Advanced Security Config
938
+ configCardTitle: "Global Security Policy Configuration",
939
+ configCardDesc: "Configure per-IP rate limits, brute-force failure thresholds, and lockout durations. Changes take effect immediately and are saved to ~/.dsh/remote-mobile/settings.json.",
940
+ copySettingsPathBtn: "\u{1F4CB} Copy settings.json Path",
941
+ fieldVisitsLimit: "\u23F1\uFE0F Max Visits per Minute (visits/min)",
942
+ fieldVisitsLimitHint: "Rate limit triggered when a single IP opens login page more than this value",
943
+ fieldFailedLimit: "\u{1F6E1}\uFE0F Max Failed Password Attempts (times)",
944
+ fieldFailedLimitHint: "IP locked after consecutive incorrect pairing codes or passwords",
945
+ fieldLockDuration: "\u{1F6AB} Lockout Duration (minutes)",
946
+ fieldLockDurationHint: "Cool-down lock period for blocked IPs (default: 15 minutes)",
947
+ restoreDefaultsBtn: "\u{1F504} Restore Defaults",
948
+ saveConfigBtn: "\u{1F4BE} Save Configuration",
949
+ savingConfigBtn: "Saving...",
950
+ saveConfigConfirm: "Are you sure you want to save the current security policy? It will take effect immediately and be saved to ~/.dsh/remote-mobile/settings.json.",
951
+ restoreDefaultsConfirm: "Are you sure you want to restore security policies to default values (60 visits/min, 5 failed attempts, 15m lockout)? It will take effect immediately.",
952
+ saveConfigSuccessToast: "Security policy successfully saved and synced to settings.json!",
953
+ restoreDefaultsSuccessToast: "Default security policies restored and applied immediately!",
954
+ saveConfigFailTip: "Parameters must be valid integers greater than 0",
955
+ // Card 8: Mobile Style Snippets (style mini-plugin)
956
+ tabAccess: "Access",
957
+ tabDevices: "Devices & Security",
958
+ tabStyle: "Style Overrides",
959
+ tabStorage: "Local Data",
960
+ stylesCardTitle: "Mobile Style Snippets",
961
+ stylesCardDesc: "Adapt the DSH web UI via toggleable CSS snippets. Built-in presets are split by UI area: sidebar, settings panel and conversation. Each can be enabled per device (PC / Mobile). You can also add your own CSS mini-plugins; changes take effect on the next page load without restart.",
962
+ stylesPresetsTitle: "\u{1F9E9} Built-in Presets",
963
+ stylesCustomTitle: "\u270D\uFE0F Custom Snippets",
964
+ stylesPcShort: "PC",
965
+ stylesMobileShort: "Mobile",
966
+ stylesBothDevices: "PC + Mobile",
967
+ stylesMobileOnlyBadge: "Mobile only",
968
+ stylesPcOnlyBadge: "PC only",
969
+ stylesDisabled: "Disabled",
970
+ stylesViewCss: "View CSS",
971
+ stylesHideCss: "Hide CSS",
972
+ stylesCopyCss: "\u{1F4CB} Copy CSS",
973
+ stylesCopyCssDone: "CSS copied to clipboard!",
974
+ stylesNamePlaceholder: "Snippet name (required)",
975
+ stylesCssPlaceholder: "Paste or write CSS here\u2026 (html[data-dsh-mobile] can be used as a mobile scope selector)",
976
+ stylesAddNew: "\uFF0B Add Custom Snippet",
977
+ stylesSave: "\u{1F4BE} Save Snippet",
978
+ stylesCancelEdit: "Cancel",
979
+ stylesEditBtn: "Edit",
980
+ stylesDeleteBtn: "Delete",
981
+ stylesResetAll: "\u{1F504} Reset Defaults",
982
+ stylesEmptyCustom: 'No custom snippets yet. Click "Add Custom Snippet" to start.',
983
+ stylesLoadFail: "Failed to load style snippets, please try again later",
984
+ stylesSaveSuccess: "Style snippet saved. Refresh to see the effect.",
985
+ stylesDeleteConfirm: "Are you sure you want to delete this custom style snippet?",
986
+ stylesResetConfirm: "Reset all snippets to default states? (Built-ins: PC off / Mobile on. Custom: PC off / Mobile on.)",
987
+ stylesFieldsRequired: "Please fill in both the snippet name and CSS content",
988
+ stylesHint: "\u{1F4A1} Tip: every preset and custom snippet has separate PC / Mobile toggles. Mobile requests add a data-dsh-mobile attribute to <html>. Built-ins target mobile (PC off by default); when enabling them on PC, make sure they do not depend on the data-dsh-mobile scope.",
989
+ // Plugin coexistence banner
990
+ conflictBannerTitle: "Another remote-access plugin detected",
991
+ conflictUnknownPlugin: "another remote-access plugin",
992
+ conflictBannerBody: "{plugin} has already registered the shared pairing service (remoteWebUiPairing); this plugin has automatically yielded it to keep startup safe. Running two remote-access features side by side may cause duplicated entries and inconsistent state \u2014 keeping only one is recommended. Copy the diagnosis below and paste it to an AI assistant for step-by-step help.",
993
+ conflictCopyBtn: "\u{1F4CB} Copy diagnosis & fix",
994
+ conflictCopiedTip: "Diagnosis copied! Paste it to your AI assistant",
995
+ conflictReportText: '[DSH plugin conflict diagnosis]\nSymptom: the dsh-remote-mobile settings panel shows "Another remote-access plugin detected".\nCause: another remote/Web access plugin registered the shared Cordis service remoteWebUiPairing first; dsh-remote-mobile yielded it to keep startup safe, so both remote features now run in parallel.\nConflicting plugin package: {plugin}\nConflicting loader entry id: {entryId}\nFix (choose ONE):\nOption A (keep dsh-remote-mobile, disable the other):\n1. Open ~/.dsh/profiles/web/cordis.patch.yml\n2. Append these two lines at the end (mind YAML indentation, two spaces before disabled):\n- id: {entryId}\n disabled: true\n3. Stop the running dsh web process, then run dsh web --no-open again\nOption B (keep the other, remove dsh-remote-mobile):\nRun dsh plugin --profile web remove dsh-remote-mobile and restart dsh web',
996
+ conflictReportNoEntryIdText: '[DSH plugin conflict diagnosis]\nSymptom: the dsh-remote-mobile settings panel shows "Another remote-access plugin detected".\nCause: another remote/Web access plugin registered the shared Cordis service remoteWebUiPairing first; dsh-remote-mobile yielded it to keep startup safe, so both remote features now run in parallel.\nConflicting plugin package: {plugin}\nConflicting loader entry id: could not be detected automatically\nFix (choose ONE):\nOption A (keep dsh-remote-mobile, disable the other):\n1. Run dsh --dump-config to list all loader entries and find the entry id of the conflicting plugin\n2. Open ~/.dsh/profiles/web/cordis.patch.yml and append at the end (mind YAML indentation):\n- id: <the conflicting entry id>\n disabled: true\n3. Stop the running dsh web process, then run dsh web --no-open again\nOption B (keep the other, remove dsh-remote-mobile):\nRun dsh plugin --profile web remove dsh-remote-mobile and restart dsh web',
997
+ conflictBannerDismiss: "Got it",
998
+ // Toast
999
+ toastDeviceConnected: "\u{1F389} New device connected: {name} ({ip})",
1000
+ toastDeviceOnline: "\u{1F4F6} Device reconnected: {name} ({ip})",
1001
+ toastIpLocked: "\u{1F6AB} Security Alert: IP {ip} locked due to consecutive failures!"
1002
+ }
1003
+ };
1004
+ function resolveLocale(ctx, status) {
1005
+ if (typeof document !== "undefined" && document.documentElement && document.documentElement.lang) {
1006
+ return document.documentElement.lang.toLowerCase().startsWith("en") ? "en" : "zh";
1007
+ }
1008
+ if (status && typeof status.locale === "string") {
1009
+ return status.locale.toLowerCase().startsWith("en") ? "en" : "zh";
1010
+ }
1011
+ if (typeof navigator !== "undefined" && navigator.language) {
1012
+ return navigator.language.toLowerCase().startsWith("en") ? "en" : "zh";
1013
+ }
1014
+ return "zh";
1015
+ }
1016
+ function t(key, lang, params) {
1017
+ var currentLang = lang && (lang.startsWith("en") || lang === "en") ? "en" : "zh";
1018
+ var dict = translations[currentLang] || translations.zh;
1019
+ var text = dict[key] || translations.zh[key] || key;
1020
+ if (params && typeof params === "object") {
1021
+ for (var p in params) {
1022
+ if (Object.prototype.hasOwnProperty.call(params, p)) {
1023
+ text = text.replace(new RegExp("\\{" + p + "\\}", "g"), String(params[p]));
1024
+ }
1025
+ }
1026
+ }
1027
+ return text;
1028
+ }
1029
+ function formatDeviceName(name, lang) {
1030
+ if (!name) return lang === "en" ? "Mobile Device" : "\u79FB\u52A8\u7AEF\u8BBE\u5907";
1031
+ if (lang !== "en") return name;
1032
+ return name.replace(/手机/g, "Phone").replace(/移动设备/g, "Mobile Device").replace(/微信内置浏览器/g, "WeChat Browser").replace(/局域网免密临时设备/g, "LAN Bypass Device").replace(/Tailscale免密临时设备/g, "Tailscale Bypass Device");
1033
+ }
1034
+ function formatAuthType(authType, lang) {
1035
+ if (!authType) return lang === "en" ? "\u26AA Pending" : "\u26AA \u5F85\u8BA4\u8BC1";
1036
+ if (lang !== "en") return authType;
1037
+ if (authType.includes("\u626B\u7801\u914D\u5BF9") || authType.includes("\u914D\u5BF9\u7801")) {
1038
+ if (authType.includes("365")) return "\u{1F4F1} QR Pairing (365d)";
1039
+ return "\u{1F4F1} QR Authenticated";
1040
+ }
1041
+ if (authType.includes("\u957F\u671F\u5BC6\u7801") || authType.includes("\u5BC6\u7801\u9A8C\u8BC1")) {
1042
+ if (authType.includes("365")) return "\u{1F511} Password Auth (365d)";
1043
+ return "\u{1F511} Password Auth";
1044
+ }
1045
+ if (authType.includes("\u5C40\u57DF\u7F51") || authType.includes("LAN")) {
1046
+ return "\u26A1 LAN Direct Bypass";
1047
+ }
1048
+ if (authType.includes("Tailscale")) {
1049
+ return "\u26A1 Tailscale Bypass";
1050
+ }
1051
+ if (authType.includes("\u5F85\u8BA4\u8BC1") || authType.includes("\u672A\u8BA4\u8BC1")) {
1052
+ return "\u26AA Pending";
1053
+ }
1054
+ return authType;
1055
+ }
1056
+ function formatFailedAttempts(count, lang) {
1057
+ if (lang === "en") {
1058
+ return count === 1 ? "\u26A0\uFE0F 1 failed attempt" : `\u26A0\uFE0F ${count} failed attempts`;
1059
+ }
1060
+ return `\u26A0\uFE0F \u66FE\u5931\u8D25 ${count} \u6B21`;
1061
+ }
1062
+
1063
+ // src/client/components/NetworkCard.jsx
1064
+ var import_jsx_runtime = require("react/jsx-runtime");
1065
+ function NetworkCard({ status, toggleTailscale, toggleLan, copyText, lang }) {
1066
+ const tsHost = status.tailscaleIp || "100.x.y.z";
1067
+ const lanHost = status.lanIp || "127.0.0.1";
1068
+ const port = typeof window !== "undefined" && window.location.port || "3080";
1069
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1070
+ "div",
1071
+ {
1072
+ style: {
1073
+ background: "var(--dsw-alias-bg-layer-3, rgba(128,128,128,0.06))",
1074
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.15))",
1075
+ borderRadius: "12px",
1076
+ padding: "14px 14px"
1077
+ },
1078
+ children: [
1079
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1080
+ "div",
1081
+ {
1082
+ style: {
1083
+ fontSize: "15px",
1084
+ fontWeight: "600",
1085
+ color: "var(--dsw-alias-label-primary, inherit)",
1086
+ marginBottom: "4px"
1087
+ },
1088
+ children: t("netCardTitle", lang)
1089
+ }
1090
+ ),
1091
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1092
+ "div",
1093
+ {
1094
+ style: {
1095
+ fontSize: "12px",
1096
+ color: "var(--dsw-alias-label-tertiary, #888)",
1097
+ marginBottom: "16px"
1098
+ },
1099
+ children: t("netCardDesc", lang)
1100
+ }
1101
+ ),
1102
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1103
+ "div",
1104
+ {
1105
+ style: {
1106
+ paddingBottom: "16px",
1107
+ borderBottom: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.12))",
1108
+ marginBottom: "16px",
1109
+ display: "flex",
1110
+ flexDirection: "column",
1111
+ gap: "10px"
1112
+ },
1113
+ children: [
1114
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1115
+ "div",
1116
+ {
1117
+ style: {
1118
+ display: "flex",
1119
+ alignItems: "center",
1120
+ justifyContent: "space-between",
1121
+ flexWrap: "wrap",
1122
+ gap: "8px 12px"
1123
+ },
1124
+ children: [
1125
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" }, children: [
1126
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: "16px", flexShrink: 0 }, children: "\u{1F512}" }),
1127
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: "14px", fontWeight: "700", color: "var(--dsw-alias-label-primary, inherit)" }, children: t("tailscaleSectionTitle", lang) }),
1128
+ status.tailscaleIp ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1129
+ "span",
1130
+ {
1131
+ style: {
1132
+ fontSize: "11px",
1133
+ padding: "2px 6px",
1134
+ borderRadius: "4px",
1135
+ background: "rgba(16,185,129,0.15)",
1136
+ color: "#10b981",
1137
+ fontWeight: "600",
1138
+ whiteSpace: "nowrap",
1139
+ flexShrink: 0
1140
+ },
1141
+ children: lang === "en" ? "Connected" : "\u5DF2\u8FDE\u63A5"
1142
+ }
1143
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1144
+ "span",
1145
+ {
1146
+ style: {
1147
+ fontSize: "11px",
1148
+ padding: "2px 6px",
1149
+ borderRadius: "4px",
1150
+ background: "rgba(239,68,68,0.15)",
1151
+ color: "#ef4444",
1152
+ fontWeight: "600",
1153
+ whiteSpace: "nowrap",
1154
+ flexShrink: 0
1155
+ },
1156
+ children: lang === "en" ? "Not Connected" : "\u672A\u8FDE\u63A5"
1157
+ }
1158
+ )
1159
+ ] }),
1160
+ status.tailscaleIp ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px", flexShrink: 0, marginLeft: "auto" }, children: [
1161
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1162
+ "span",
1163
+ {
1164
+ style: {
1165
+ fontSize: "12px",
1166
+ color: status.allowTailscale ? "var(--dsw-alias-brand-primary, #3b82f6)" : "var(--dsw-alias-label-secondary, #888)",
1167
+ fontWeight: "600",
1168
+ whiteSpace: "nowrap"
1169
+ },
1170
+ children: status.allowTailscale ? t("directBypassOn", lang) : t("directBypassOff", lang)
1171
+ }
1172
+ ),
1173
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1174
+ "label",
1175
+ {
1176
+ style: {
1177
+ position: "relative",
1178
+ display: "inline-block",
1179
+ width: "44px",
1180
+ height: "24px",
1181
+ cursor: "pointer",
1182
+ userSelect: "none",
1183
+ flexShrink: 0
1184
+ },
1185
+ children: [
1186
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1187
+ "input",
1188
+ {
1189
+ type: "checkbox",
1190
+ checked: !!status.allowTailscale,
1191
+ onChange: toggleTailscale,
1192
+ style: { opacity: 0, width: 0, height: 0, margin: 0 }
1193
+ }
1194
+ ),
1195
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1196
+ "span",
1197
+ {
1198
+ style: {
1199
+ position: "absolute",
1200
+ top: 0,
1201
+ left: 0,
1202
+ right: 0,
1203
+ bottom: 0,
1204
+ backgroundColor: status.allowTailscale ? "var(--dsw-alias-brand-primary, #3b82f6)" : "var(--dsw-alias-border-l2, rgba(128,128,128,0.3))",
1205
+ transition: "background-color 0.25s ease",
1206
+ borderRadius: "24px"
1207
+ }
1208
+ }
1209
+ ),
1210
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1211
+ "span",
1212
+ {
1213
+ style: {
1214
+ position: "absolute",
1215
+ top: "3px",
1216
+ left: "3px",
1217
+ width: "18px",
1218
+ height: "18px",
1219
+ backgroundColor: "#ffffff",
1220
+ borderRadius: "50%",
1221
+ boxShadow: "0 2px 4px rgba(0, 0, 0, 0.25)",
1222
+ transform: status.allowTailscale ? "translateX(20px)" : "translateX(0px)",
1223
+ transition: "transform 0.25s cubic-bezier(0.4, 0, 0.2, 1)"
1224
+ }
1225
+ }
1226
+ )
1227
+ ]
1228
+ }
1229
+ )
1230
+ ] }) : null
1231
+ ]
1232
+ }
1233
+ ),
1234
+ status.tailscaleIp ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1235
+ "div",
1236
+ {
1237
+ style: {
1238
+ fontSize: "13px",
1239
+ fontFamily: "monospace",
1240
+ color: "var(--dsw-alias-brand-primary, #3b82f6)",
1241
+ display: "flex",
1242
+ alignItems: "center",
1243
+ justifyContent: "space-between",
1244
+ gap: "8px",
1245
+ padding: "6px 10px",
1246
+ background: "var(--dsw-alias-bg-layer-2, rgba(128, 128, 128, 0.08))",
1247
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.15))",
1248
+ borderRadius: "8px"
1249
+ },
1250
+ children: [
1251
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1252
+ "span",
1253
+ {
1254
+ style: {
1255
+ overflow: "hidden",
1256
+ textOverflow: "ellipsis",
1257
+ whiteSpace: "nowrap",
1258
+ flex: 1,
1259
+ minWidth: 0
1260
+ },
1261
+ children: `http://${tsHost}:${port}`
1262
+ }
1263
+ ),
1264
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1265
+ "button",
1266
+ {
1267
+ type: "button",
1268
+ onClick: () => copyText(`http://${tsHost}:${port}`, t("copiedTip", lang)),
1269
+ style: {
1270
+ padding: "3px 10px",
1271
+ background: "var(--dsw-alias-bg-layer-3, rgba(255, 255, 255, 0.08))",
1272
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))",
1273
+ borderRadius: "4px",
1274
+ fontSize: "11px",
1275
+ cursor: "pointer",
1276
+ color: "var(--dsw-alias-label-secondary, inherit)",
1277
+ whiteSpace: "nowrap",
1278
+ flexShrink: 0
1279
+ },
1280
+ children: t("copyUrlBtn", lang)
1281
+ }
1282
+ )
1283
+ ]
1284
+ }
1285
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1286
+ "div",
1287
+ {
1288
+ style: {
1289
+ fontSize: "12px",
1290
+ color: "var(--dsw-alias-label-tertiary, #888)",
1291
+ lineHeight: "1.4"
1292
+ },
1293
+ children: t("tailscaleGuide", lang)
1294
+ }
1295
+ ),
1296
+ status.tailscaleIp ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1297
+ "div",
1298
+ {
1299
+ style: {
1300
+ fontSize: "12px",
1301
+ color: "var(--dsw-alias-label-tertiary, #888)",
1302
+ lineHeight: "1.4"
1303
+ },
1304
+ children: t("tailscaleBypassDesc", lang)
1305
+ }
1306
+ ) : null
1307
+ ]
1308
+ }
1309
+ ),
1310
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "10px" }, children: [
1311
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1312
+ "div",
1313
+ {
1314
+ style: {
1315
+ display: "flex",
1316
+ alignItems: "center",
1317
+ justifyContent: "space-between",
1318
+ flexWrap: "wrap",
1319
+ gap: "8px 12px"
1320
+ },
1321
+ children: [
1322
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" }, children: [
1323
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: "16px", flexShrink: 0 }, children: "\u{1F3E0}" }),
1324
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: "14px", fontWeight: "700", color: "var(--dsw-alias-label-primary, inherit)" }, children: t("lanSectionTitle", lang) }),
1325
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1326
+ "span",
1327
+ {
1328
+ style: {
1329
+ fontSize: "11px",
1330
+ padding: "2px 6px",
1331
+ borderRadius: "4px",
1332
+ background: "rgba(16,185,129,0.15)",
1333
+ color: "#10b981",
1334
+ fontWeight: "600",
1335
+ whiteSpace: "nowrap",
1336
+ flexShrink: 0
1337
+ },
1338
+ children: lang === "en" ? "Ready" : "\u5DF2\u5C31\u7EEA"
1339
+ }
1340
+ )
1341
+ ] }),
1342
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px", flexShrink: 0, marginLeft: "auto" }, children: [
1343
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1344
+ "span",
1345
+ {
1346
+ style: {
1347
+ fontSize: "12px",
1348
+ color: status.allowLan ? "#ef4444" : "var(--dsw-alias-label-secondary, #888)",
1349
+ fontWeight: status.allowLan ? "700" : "500",
1350
+ display: "flex",
1351
+ alignItems: "center",
1352
+ gap: "4px",
1353
+ whiteSpace: "nowrap"
1354
+ },
1355
+ children: status.allowLan ? lang === "en" ? "\u{1F6A8} LAN Bypass (High Risk)" : "\u{1F6A8} \u5C40\u57DF\u7F51\u514D\u5BC6\uFF08\u9AD8\u5371\uFF09" : t("directBypassOff", lang)
1356
+ }
1357
+ ),
1358
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1359
+ "label",
1360
+ {
1361
+ style: {
1362
+ position: "relative",
1363
+ display: "inline-block",
1364
+ width: "44px",
1365
+ height: "24px",
1366
+ cursor: "pointer",
1367
+ userSelect: "none",
1368
+ flexShrink: 0
1369
+ },
1370
+ children: [
1371
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1372
+ "input",
1373
+ {
1374
+ type: "checkbox",
1375
+ checked: !!status.allowLan,
1376
+ onChange: toggleLan,
1377
+ style: { opacity: 0, width: 0, height: 0, margin: 0 }
1378
+ }
1379
+ ),
1380
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1381
+ "span",
1382
+ {
1383
+ style: {
1384
+ position: "absolute",
1385
+ top: 0,
1386
+ left: 0,
1387
+ right: 0,
1388
+ bottom: 0,
1389
+ backgroundColor: status.allowLan ? "#ef4444" : "var(--dsw-alias-border-l2, rgba(128,128,128,0.3))",
1390
+ boxShadow: status.allowLan ? "0 0 10px rgba(239, 68, 68, 0.45)" : "none",
1391
+ transition: "all 0.25s ease",
1392
+ borderRadius: "24px"
1393
+ }
1394
+ }
1395
+ ),
1396
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1397
+ "span",
1398
+ {
1399
+ style: {
1400
+ position: "absolute",
1401
+ top: "3px",
1402
+ left: "3px",
1403
+ width: "18px",
1404
+ height: "18px",
1405
+ backgroundColor: "#ffffff",
1406
+ borderRadius: "50%",
1407
+ boxShadow: "0 2px 4px rgba(0, 0, 0, 0.25)",
1408
+ transform: status.allowLan ? "translateX(20px)" : "translateX(0px)",
1409
+ transition: "transform 0.25s cubic-bezier(0.4, 0, 0.2, 1)"
1410
+ }
1411
+ }
1412
+ )
1413
+ ]
1414
+ }
1415
+ )
1416
+ ] })
1417
+ ]
1418
+ }
1419
+ ),
1420
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1421
+ "div",
1422
+ {
1423
+ style: {
1424
+ fontSize: "13px",
1425
+ fontFamily: "monospace",
1426
+ color: "var(--dsw-alias-brand-primary, #3b82f6)",
1427
+ display: "flex",
1428
+ alignItems: "center",
1429
+ justifyContent: "space-between",
1430
+ gap: "8px",
1431
+ padding: "6px 10px",
1432
+ background: "var(--dsw-alias-bg-layer-2, rgba(128, 128, 128, 0.08))",
1433
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.15))",
1434
+ borderRadius: "8px"
1435
+ },
1436
+ children: [
1437
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1438
+ "span",
1439
+ {
1440
+ style: {
1441
+ overflow: "hidden",
1442
+ textOverflow: "ellipsis",
1443
+ whiteSpace: "nowrap",
1444
+ flex: 1,
1445
+ minWidth: 0
1446
+ },
1447
+ children: `http://${lanHost}:${port}`
1448
+ }
1449
+ ),
1450
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1451
+ "button",
1452
+ {
1453
+ type: "button",
1454
+ onClick: () => copyText(`http://${lanHost}:${port}`, t("copiedTip", lang)),
1455
+ style: {
1456
+ padding: "3px 10px",
1457
+ background: "var(--dsw-alias-bg-layer-3, rgba(255, 255, 255, 0.08))",
1458
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))",
1459
+ borderRadius: "4px",
1460
+ fontSize: "11px",
1461
+ cursor: "pointer",
1462
+ color: "var(--dsw-alias-label-secondary, inherit)",
1463
+ whiteSpace: "nowrap",
1464
+ flexShrink: 0
1465
+ },
1466
+ children: t("copyUrlBtn", lang)
1467
+ }
1468
+ )
1469
+ ]
1470
+ }
1471
+ ),
1472
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1473
+ "div",
1474
+ {
1475
+ style: {
1476
+ marginTop: "4px",
1477
+ padding: "10px 14px",
1478
+ borderRadius: "8px",
1479
+ background: status.allowLan ? "rgba(239, 68, 68, 0.12)" : "rgba(245, 158, 11, 0.08)",
1480
+ border: status.allowLan ? "1px solid rgba(239, 68, 68, 0.4)" : "1px solid rgba(245, 158, 11, 0.25)",
1481
+ borderLeft: status.allowLan ? "4px solid #ef4444" : "4px solid #f59e0b",
1482
+ boxShadow: status.allowLan ? "0 2px 8px rgba(239, 68, 68, 0.15)" : "none"
1483
+ },
1484
+ children: [
1485
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1486
+ "div",
1487
+ {
1488
+ style: {
1489
+ display: "flex",
1490
+ alignItems: "center",
1491
+ gap: "6px",
1492
+ fontSize: "12px",
1493
+ fontWeight: "700",
1494
+ color: status.allowLan ? "#ef4444" : "#f59e0b",
1495
+ marginBottom: "4px"
1496
+ },
1497
+ children: [
1498
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: status.allowLan ? "\u{1F6A8}" : "\u26A0\uFE0F" }),
1499
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("lanBypassWarningTitle", lang) })
1500
+ ]
1501
+ }
1502
+ ),
1503
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1504
+ "div",
1505
+ {
1506
+ style: {
1507
+ fontSize: "12px",
1508
+ lineHeight: "1.55",
1509
+ color: status.allowLan ? "var(--dsw-alias-label-primary, #fca5a5)" : "var(--dsw-alias-label-secondary, #cbd5e1)"
1510
+ },
1511
+ children: t("lanBypassDesc", lang)
1512
+ }
1513
+ )
1514
+ ]
1515
+ }
1516
+ )
1517
+ ] })
1518
+ ]
1519
+ }
1520
+ );
1521
+ }
1522
+
1523
+ // src/client/components/QrPairingCard.jsx
1524
+ var import_react2 = __toESM(require("react"), 1);
1525
+
1526
+ // src/client/utils/format.js
1527
+ function formatTime(ts, lang) {
1528
+ if (!ts) return lang === "en" ? "Unknown" : "\u672A\u77E5\u65F6\u95F4";
1529
+ var date = new Date(ts);
1530
+ var now = /* @__PURE__ */ new Date();
1531
+ var isToday = date.toDateString() === now.toDateString();
1532
+ var pad = function(n) {
1533
+ return n < 10 ? "0" + n : n;
1534
+ };
1535
+ var timeStr = pad(date.getHours()) + ":" + pad(date.getMinutes()) + ":" + pad(date.getSeconds());
1536
+ if (isToday) {
1537
+ return (lang === "en" ? "Today " : "\u4ECA\u5929 ") + timeStr;
1538
+ }
1539
+ if (lang === "en") {
1540
+ var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1541
+ return months[date.getMonth()] + " " + date.getDate() + " " + timeStr;
1542
+ }
1543
+ return date.getMonth() + 1 + "\u6708" + date.getDate() + "\u65E5 " + timeStr;
1544
+ }
1545
+ function formatCountdown(sec, lang) {
1546
+ if (sec <= 0) return lang === "en" ? "Expired" : "\u5DF2\u8FC7\u671F";
1547
+ var m = Math.floor(sec / 60);
1548
+ var s = sec % 60;
1549
+ if (lang === "en") {
1550
+ return (m > 0 ? m + "m " : "") + (s < 10 ? "0" + s : s) + "s";
1551
+ }
1552
+ return (m > 0 ? m + " \u5206 " : "") + (s < 10 ? "0" + s : s) + " \u79D2";
1553
+ }
1554
+
1555
+ // src/client/components/QrPairingCard.jsx
1556
+ var import_jsx_runtime2 = require("react/jsx-runtime");
1557
+ function QrPairingCard({
1558
+ status,
1559
+ showCode,
1560
+ setShowCode,
1561
+ timeLeft,
1562
+ showQr,
1563
+ setShowQr,
1564
+ selectedTab,
1565
+ setSelectedTab,
1566
+ generateQrSvg: generateQrSvg2,
1567
+ refreshStatusAndCode,
1568
+ copyDirectLink,
1569
+ lang
1570
+ }) {
1571
+ const port = typeof window !== "undefined" && window.location.port || "3080";
1572
+ const isExpired = timeLeft <= 0;
1573
+ const currentHost = selectedTab === "lan" && status.lanIp ? status.lanIp : status.tailscaleIp || status.lanIp || typeof window !== "undefined" && window.location.hostname || "127.0.0.1";
1574
+ const directLink = `http://${currentHost}:${port}/auth?token=${status.code || ""}`;
1575
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1576
+ "div",
1577
+ {
1578
+ style: {
1579
+ background: "var(--dsw-alias-bg-layer-3, rgba(128,128,128,0.06))",
1580
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.15))",
1581
+ borderRadius: "12px",
1582
+ padding: "14px 14px",
1583
+ textAlign: "center"
1584
+ },
1585
+ children: [
1586
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1587
+ "div",
1588
+ {
1589
+ style: {
1590
+ fontSize: "15px",
1591
+ fontWeight: "600",
1592
+ marginBottom: "4px",
1593
+ color: "var(--dsw-alias-label-primary, inherit)"
1594
+ },
1595
+ children: `\u{1F4F1} ${t("shortCodeTitle", lang)}`
1596
+ }
1597
+ ),
1598
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1599
+ "div",
1600
+ {
1601
+ style: {
1602
+ fontSize: "12px",
1603
+ color: "var(--dsw-alias-label-tertiary, #888)",
1604
+ marginBottom: "14px"
1605
+ },
1606
+ children: t("pairingCardDesc", lang)
1607
+ }
1608
+ ),
1609
+ showQr ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1610
+ "div",
1611
+ {
1612
+ style: {
1613
+ background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.06))",
1614
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.12))",
1615
+ borderRadius: "10px",
1616
+ padding: "16px",
1617
+ maxWidth: "320px",
1618
+ margin: "0 auto 16px auto",
1619
+ display: "flex",
1620
+ flexDirection: "column",
1621
+ alignItems: "center",
1622
+ gap: "10px"
1623
+ },
1624
+ children: [
1625
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1626
+ "div",
1627
+ {
1628
+ style: {
1629
+ display: "flex",
1630
+ width: "100%",
1631
+ background: "var(--dsw-alias-bg-layer-3, rgba(128,128,128,0.1))",
1632
+ padding: "3px",
1633
+ borderRadius: "8px"
1634
+ },
1635
+ children: [
1636
+ status.tailscaleIp ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1637
+ "button",
1638
+ {
1639
+ type: "button",
1640
+ onClick: () => setSelectedTab("tailscale"),
1641
+ style: {
1642
+ flex: 1,
1643
+ padding: "5px 8px",
1644
+ borderRadius: "6px",
1645
+ border: "none",
1646
+ background: selectedTab === "tailscale" ? "var(--dsw-alias-brand-primary, #3b82f6)" : "transparent",
1647
+ color: selectedTab === "tailscale" ? "#ffffff" : "var(--dsw-alias-label-secondary, inherit)",
1648
+ fontSize: "12px",
1649
+ fontWeight: "600",
1650
+ cursor: "pointer",
1651
+ transition: "all 0.2s"
1652
+ },
1653
+ children: `\u{1F680} ${t("switchTailscaleTab", lang)}`
1654
+ }
1655
+ ) : null,
1656
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1657
+ "button",
1658
+ {
1659
+ type: "button",
1660
+ onClick: () => setSelectedTab("lan"),
1661
+ style: {
1662
+ flex: 1,
1663
+ padding: "5px 8px",
1664
+ borderRadius: "6px",
1665
+ border: "none",
1666
+ background: selectedTab === "lan" ? "var(--dsw-alias-brand-primary, #3b82f6)" : "transparent",
1667
+ color: selectedTab === "lan" ? "#ffffff" : "var(--dsw-alias-label-secondary, inherit)",
1668
+ fontSize: "12px",
1669
+ fontWeight: "600",
1670
+ cursor: "pointer",
1671
+ transition: "all 0.2s"
1672
+ },
1673
+ children: `\u{1F3E0} ${t("switchLanTab", lang)}`
1674
+ }
1675
+ )
1676
+ ]
1677
+ }
1678
+ ),
1679
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1680
+ "div",
1681
+ {
1682
+ style: {
1683
+ display: "flex",
1684
+ justifyContent: "center",
1685
+ opacity: isExpired ? 0.3 : 1
1686
+ },
1687
+ dangerouslySetInnerHTML: {
1688
+ __html: generateQrSvg2(directLink, 170)
1689
+ }
1690
+ }
1691
+ ),
1692
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1693
+ "div",
1694
+ {
1695
+ style: {
1696
+ width: "100%",
1697
+ padding: "6px 10px",
1698
+ background: "var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.12))",
1699
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.15))",
1700
+ borderRadius: "6px",
1701
+ fontFamily: "monospace",
1702
+ fontSize: "11px",
1703
+ color: isExpired ? "var(--dsw-alias-label-error, #ef4444)" : "var(--dsw-alias-label-secondary, inherit)",
1704
+ wordBreak: "break-all",
1705
+ userSelect: "all",
1706
+ lineHeight: "1.4",
1707
+ textAlign: "center"
1708
+ },
1709
+ children: [
1710
+ `http://${currentHost}:${port}/auth?token=`,
1711
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1712
+ "span",
1713
+ {
1714
+ style: {
1715
+ fontWeight: "700",
1716
+ color: showCode ? "var(--dsw-alias-brand-primary, #3b82f6)" : "inherit",
1717
+ letterSpacing: showCode ? "1px" : "2px"
1718
+ },
1719
+ children: showCode ? status.code || "" : "\u2022\u2022\u2022\u2022\u2022\u2022"
1720
+ }
1721
+ )
1722
+ ]
1723
+ }
1724
+ ),
1725
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1726
+ "button",
1727
+ {
1728
+ type: "button",
1729
+ onClick: copyDirectLink,
1730
+ style: {
1731
+ padding: "6px 16px",
1732
+ background: "#2563eb",
1733
+ color: "#ffffff",
1734
+ border: "none",
1735
+ borderRadius: "6px",
1736
+ fontSize: "12px",
1737
+ fontWeight: "600",
1738
+ cursor: "pointer",
1739
+ boxShadow: "0 2px 6px rgba(37, 99, 235, 0.3)",
1740
+ transition: "background-color 0.2s"
1741
+ },
1742
+ children: status.copied ? `\u2705 ${lang === "en" ? "Copied!" : "\u5DF2\u590D\u5236\uFF01"}` : t("copyDirectAuthLink", lang)
1743
+ }
1744
+ )
1745
+ ]
1746
+ }
1747
+ ) : null,
1748
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1749
+ "div",
1750
+ {
1751
+ style: {
1752
+ display: "inline-flex",
1753
+ alignItems: "center",
1754
+ justifyContent: "center",
1755
+ gap: "12px",
1756
+ background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.1))",
1757
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.2))",
1758
+ borderRadius: "10px",
1759
+ padding: "6px 20px",
1760
+ margin: "4px auto 8px auto"
1761
+ },
1762
+ children: [
1763
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1764
+ "span",
1765
+ {
1766
+ style: {
1767
+ fontSize: "32px",
1768
+ fontWeight: "800",
1769
+ letterSpacing: showCode ? "6px" : "4px",
1770
+ color: isExpired ? "var(--dsw-alias-label-error, #ef4444)" : "var(--dsw-alias-brand-primary, #3b82f6)",
1771
+ fontFamily: "monospace",
1772
+ userSelect: "all"
1773
+ },
1774
+ children: isExpired ? lang === "en" ? "EXPIRED" : "\u5DF2\u8FC7\u671F" : showCode ? status.code : "\u2022\u2022\u2022\u2022\u2022\u2022"
1775
+ }
1776
+ ),
1777
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1778
+ "button",
1779
+ {
1780
+ type: "button",
1781
+ onClick: () => setShowCode(!showCode),
1782
+ title: showCode ? t("clickToHide", lang) : t("clickToReveal", lang),
1783
+ style: {
1784
+ background: "transparent",
1785
+ border: "none",
1786
+ color: "var(--dsw-alias-label-secondary, #888)",
1787
+ cursor: "pointer",
1788
+ display: "flex",
1789
+ alignItems: "center",
1790
+ justifyContent: "center",
1791
+ padding: "4px"
1792
+ },
1793
+ children: showCode ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { style: { width: "20px", height: "20px", fill: "currentColor" }, viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" }) }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { style: { width: "20px", height: "20px", fill: "currentColor" }, viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z" }) })
1794
+ }
1795
+ )
1796
+ ]
1797
+ }
1798
+ ),
1799
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1800
+ "div",
1801
+ {
1802
+ style: {
1803
+ fontSize: "12px",
1804
+ color: isExpired ? "var(--dsw-alias-label-error, #ef4444)" : "var(--dsw-alias-label-secondary, #888)",
1805
+ margin: "4px 0 16px 0",
1806
+ display: "flex",
1807
+ alignItems: "center",
1808
+ justifyContent: "center",
1809
+ gap: "6px"
1810
+ },
1811
+ children: [
1812
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1813
+ "span",
1814
+ {
1815
+ style: {
1816
+ display: "inline-block",
1817
+ width: "8px",
1818
+ height: "8px",
1819
+ borderRadius: "50%",
1820
+ background: isExpired ? "#ef4444" : "#10b981"
1821
+ }
1822
+ }
1823
+ ),
1824
+ isExpired ? `\u26A0\uFE0F ${t("codeExpired", lang)}` : `${t("codeValidUntil", lang)}${formatCountdown(timeLeft, lang)}`
1825
+ ]
1826
+ }
1827
+ ),
1828
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1829
+ "div",
1830
+ {
1831
+ style: {
1832
+ display: "flex",
1833
+ gap: "10px",
1834
+ justifyContent: "center",
1835
+ flexWrap: "wrap"
1836
+ },
1837
+ children: [
1838
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1839
+ "button",
1840
+ {
1841
+ type: "button",
1842
+ onClick: () => setShowQr(!showQr),
1843
+ style: {
1844
+ padding: "7px 16px",
1845
+ background: showQr ? "rgba(59,130,246,0.15)" : "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.1))",
1846
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.2))",
1847
+ color: showQr ? "var(--dsw-alias-brand-primary, #3b82f6)" : "var(--dsw-alias-label-primary, inherit)",
1848
+ borderRadius: "8px",
1849
+ fontSize: "13px",
1850
+ fontWeight: "600",
1851
+ cursor: "pointer",
1852
+ whiteSpace: "nowrap",
1853
+ flexShrink: 0
1854
+ },
1855
+ children: showQr ? `\u{1F4F7} ${t("toggleQrHide", lang)}` : `\u{1F4F7} ${t("toggleQrShow", lang)}`
1856
+ }
1857
+ ),
1858
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1859
+ "button",
1860
+ {
1861
+ type: "button",
1862
+ onClick: refreshStatusAndCode,
1863
+ style: {
1864
+ padding: "7px 16px",
1865
+ background: "rgba(59,130,246,0.12)",
1866
+ border: "1px solid rgba(59,130,246,0.3)",
1867
+ color: "var(--dsw-alias-brand-primary, #3b82f6)",
1868
+ borderRadius: "8px",
1869
+ fontSize: "13px",
1870
+ fontWeight: "600",
1871
+ cursor: "pointer",
1872
+ whiteSpace: "nowrap",
1873
+ flexShrink: 0
1874
+ },
1875
+ children: t("refreshCodeBtn", lang)
1876
+ }
1877
+ )
1878
+ ]
1879
+ }
1880
+ )
1881
+ ]
1882
+ }
1883
+ );
1884
+ }
1885
+
1886
+ // src/client/components/SecretCard.jsx
1887
+ var import_react3 = __toESM(require("react"), 1);
1888
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1889
+ function SecretCard({
1890
+ status,
1891
+ secretInput,
1892
+ setSecretInput,
1893
+ showSecret,
1894
+ setShowSecret,
1895
+ saveSecret,
1896
+ clearSecret,
1897
+ lang
1898
+ }) {
1899
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1900
+ "div",
1901
+ {
1902
+ style: {
1903
+ background: "var(--dsw-alias-bg-layer-3, rgba(128,128,128,0.06))",
1904
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.15))",
1905
+ borderRadius: "12px",
1906
+ padding: "14px 14px"
1907
+ },
1908
+ children: [
1909
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1910
+ "style",
1911
+ {
1912
+ dangerouslySetInnerHTML: {
1913
+ __html: "input.dsh-secret-pwd-input::-ms-reveal, input.dsh-secret-pwd-input::-ms-clear, input.dsh-secret-pwd-input::-webkit-credentials-auto-fill-button, input.dsh-secret-pwd-input::-webkit-contacts-auto-fill-button { display: none !important; width: 0 !important; height: 0 !important; visibility: hidden !important; pointer-events: none !important; }"
1914
+ }
1915
+ }
1916
+ ),
1917
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1918
+ "div",
1919
+ {
1920
+ style: {
1921
+ display: "flex",
1922
+ alignItems: "center",
1923
+ justifyContent: "space-between",
1924
+ marginBottom: "4px"
1925
+ },
1926
+ children: [
1927
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1928
+ "div",
1929
+ {
1930
+ style: {
1931
+ fontSize: "15px",
1932
+ fontWeight: "600",
1933
+ color: "var(--dsw-alias-label-primary, inherit)"
1934
+ },
1935
+ children: t("secretCardTitle", lang)
1936
+ }
1937
+ ),
1938
+ status.hasSecret ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1939
+ "span",
1940
+ {
1941
+ style: {
1942
+ fontSize: "12px",
1943
+ color: "#10b981",
1944
+ background: "rgba(16,185,129,0.12)",
1945
+ padding: "2px 8px",
1946
+ borderRadius: "12px",
1947
+ fontWeight: "500"
1948
+ },
1949
+ children: t("secretConfigured", lang)
1950
+ }
1951
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1952
+ "span",
1953
+ {
1954
+ style: {
1955
+ fontSize: "12px",
1956
+ color: "var(--dsw-alias-label-tertiary, #888)"
1957
+ },
1958
+ children: t("secretNotConfigured", lang)
1959
+ }
1960
+ )
1961
+ ]
1962
+ }
1963
+ ),
1964
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1965
+ "div",
1966
+ {
1967
+ style: {
1968
+ fontSize: "12px",
1969
+ color: "var(--dsw-alias-label-tertiary, #888)",
1970
+ marginBottom: "12px"
1971
+ },
1972
+ children: t("secretCardDesc", lang)
1973
+ }
1974
+ ),
1975
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "10px", alignItems: "center", flexWrap: "wrap" }, children: [
1976
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1977
+ "div",
1978
+ {
1979
+ style: {
1980
+ position: "relative",
1981
+ flex: "1 1 200px",
1982
+ minWidth: "200px",
1983
+ display: "flex",
1984
+ alignItems: "center"
1985
+ },
1986
+ children: [
1987
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1988
+ "input",
1989
+ {
1990
+ className: "dsh-secret-pwd-input",
1991
+ type: showSecret ? "text" : "password",
1992
+ value: secretInput,
1993
+ placeholder: status.hasSecret ? lang === "en" ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 (Password configured, enter new to update)" : "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 (\u5DF2\u914D\u7F6E\u6709\u6548\u5BC6\u7801\uFF0C\u8F93\u5165\u65B0\u5BC6\u7801\u53EF\u8986\u76D6\u66F4\u65B0)" : t("secretInputPlaceholder", lang),
1994
+ onChange: (e) => setSecretInput(e.target.value),
1995
+ style: {
1996
+ width: "100%",
1997
+ height: "38px",
1998
+ background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.1))",
1999
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.2))",
2000
+ borderRadius: "8px",
2001
+ padding: "0 38px 0 12px",
2002
+ color: "inherit",
2003
+ fontSize: "14px",
2004
+ outline: "none"
2005
+ }
2006
+ }
2007
+ ),
2008
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2009
+ "button",
2010
+ {
2011
+ type: "button",
2012
+ onClick: () => setShowSecret(!showSecret),
2013
+ title: showSecret ? t("hidePassword", lang) : t("showPassword", lang),
2014
+ style: {
2015
+ position: "absolute",
2016
+ right: "8px",
2017
+ background: "transparent",
2018
+ border: "none",
2019
+ color: "var(--dsw-alias-label-secondary, #888)",
2020
+ cursor: "pointer",
2021
+ display: "flex",
2022
+ alignItems: "center",
2023
+ justifyContent: "center",
2024
+ padding: "4px"
2025
+ },
2026
+ children: showSecret ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { style: { width: "18px", height: "18px", fill: "currentColor" }, viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" }) }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { style: { width: "18px", height: "18px", fill: "currentColor" }, viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z" }) })
2027
+ }
2028
+ )
2029
+ ]
2030
+ }
2031
+ ),
2032
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2033
+ "button",
2034
+ {
2035
+ type: "button",
2036
+ onClick: saveSecret,
2037
+ style: {
2038
+ padding: "0 16px",
2039
+ height: "38px",
2040
+ background: "#2563eb",
2041
+ color: "#ffffff",
2042
+ border: "none",
2043
+ borderRadius: "8px",
2044
+ fontSize: "13px",
2045
+ fontWeight: "600",
2046
+ cursor: "pointer",
2047
+ whiteSpace: "nowrap",
2048
+ flexShrink: 0,
2049
+ boxShadow: "0 2px 6px rgba(37, 99, 235, 0.3)"
2050
+ },
2051
+ children: t("saveSecretBtn", lang)
2052
+ }
2053
+ ),
2054
+ status.hasSecret ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
2055
+ "button",
2056
+ {
2057
+ type: "button",
2058
+ onClick: clearSecret,
2059
+ title: t("clearSecretConfirm", lang),
2060
+ style: {
2061
+ padding: "0 12px",
2062
+ height: "38px",
2063
+ background: "rgba(239, 68, 68, 0.1)",
2064
+ color: "#ef4444",
2065
+ border: "1px solid rgba(239, 68, 68, 0.2)",
2066
+ borderRadius: "8px",
2067
+ fontSize: "13px",
2068
+ fontWeight: "500",
2069
+ cursor: "pointer",
2070
+ whiteSpace: "nowrap",
2071
+ flexShrink: 0
2072
+ },
2073
+ children: t("clearSecretBtn", lang)
2074
+ }
2075
+ ) : null
2076
+ ] })
2077
+ ]
2078
+ }
2079
+ );
2080
+ }
2081
+
2082
+ // src/client/components/DeviceListCard.jsx
2083
+ var import_react4 = __toESM(require("react"), 1);
2084
+ var import_jsx_runtime4 = require("react/jsx-runtime");
2085
+ function isCurrentLocalIp(ip, status) {
2086
+ if (!ip) return false;
2087
+ if (ip === "127.0.0.1" || ip === "::1" || ip === "localhost") return true;
2088
+ if (status && status.lanIp && ip === status.lanIp) return true;
2089
+ if (status && status.tailscaleIp && ip === status.tailscaleIp) return true;
2090
+ if (typeof window !== "undefined" && window.location && window.location.hostname && ip === window.location.hostname)
2091
+ return true;
2092
+ return false;
2093
+ }
2094
+ function DeviceListCard({ status, revokeDevice, revokeAll, lang }) {
2095
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2096
+ "div",
2097
+ {
2098
+ style: {
2099
+ background: "var(--dsw-alias-bg-layer-3, rgba(128,128,128,0.06))",
2100
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.15))",
2101
+ borderRadius: "12px",
2102
+ padding: "14px 14px"
2103
+ },
2104
+ children: [
2105
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2106
+ "div",
2107
+ {
2108
+ style: {
2109
+ display: "flex",
2110
+ alignItems: "center",
2111
+ justifyContent: "space-between",
2112
+ flexWrap: "wrap",
2113
+ gap: "8px 12px",
2114
+ marginBottom: "6px"
2115
+ },
2116
+ children: [
2117
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2118
+ "div",
2119
+ {
2120
+ style: {
2121
+ fontSize: "15px",
2122
+ fontWeight: "600",
2123
+ color: "var(--dsw-alias-label-primary, inherit)"
2124
+ },
2125
+ children: `\u{1F4F1} ${t("deviceCardTitle", lang)} (${status.devicesCount}${lang === "en" ? " devices" : " \u53F0"})`
2126
+ }
2127
+ ),
2128
+ status.devicesCount > 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2129
+ "button",
2130
+ {
2131
+ type: "button",
2132
+ onClick: revokeAll,
2133
+ style: {
2134
+ background: "transparent",
2135
+ border: "1px solid rgba(239,68,68,0.4)",
2136
+ color: "var(--dsw-alias-label-error, #ef4444)",
2137
+ padding: "4px 10px",
2138
+ borderRadius: "6px",
2139
+ fontSize: "11px",
2140
+ cursor: "pointer",
2141
+ whiteSpace: "nowrap",
2142
+ flexShrink: 0,
2143
+ marginLeft: "auto"
2144
+ },
2145
+ children: t("revokeAllBtn", lang)
2146
+ }
2147
+ ) : null
2148
+ ]
2149
+ }
2150
+ ),
2151
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2152
+ "div",
2153
+ {
2154
+ style: {
2155
+ fontSize: "12px",
2156
+ color: "var(--dsw-alias-label-tertiary, #888)",
2157
+ lineHeight: "1.45",
2158
+ marginBottom: "14px"
2159
+ },
2160
+ children: t("deviceCardDesc", lang)
2161
+ }
2162
+ ),
2163
+ status.devices && status.devices.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: "10px" }, children: status.devices.map((dev) => {
2164
+ const isLocal = isCurrentLocalIp(dev.ip, status);
2165
+ const ipLabel = `${dev.ip}${isLocal ? lang === "en" ? " (Local)" : " (\u672C\u673A)" : ""}`;
2166
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2167
+ "div",
2168
+ {
2169
+ style: {
2170
+ display: "flex",
2171
+ alignItems: "center",
2172
+ justifyContent: "space-between",
2173
+ gap: "12px",
2174
+ padding: "12px 14px",
2175
+ background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.08))",
2176
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.12))",
2177
+ borderRadius: "8px"
2178
+ },
2179
+ children: [
2180
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2181
+ "div",
2182
+ {
2183
+ style: {
2184
+ display: "flex",
2185
+ flexDirection: "column",
2186
+ gap: "4px",
2187
+ minWidth: 0,
2188
+ flex: 1
2189
+ },
2190
+ children: [
2191
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2192
+ "div",
2193
+ {
2194
+ style: {
2195
+ fontSize: "14px",
2196
+ fontWeight: "600",
2197
+ color: "var(--dsw-alias-label-primary, inherit)",
2198
+ display: "flex",
2199
+ alignItems: "center",
2200
+ flexWrap: "wrap",
2201
+ gap: "8px"
2202
+ },
2203
+ children: [
2204
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: formatDeviceName(dev.deviceName, lang) }),
2205
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2206
+ "span",
2207
+ {
2208
+ style: {
2209
+ fontSize: "11px",
2210
+ padding: "1px 6px",
2211
+ borderRadius: "4px",
2212
+ background: dev.isBypass ? "rgba(16,185,129,0.15)" : "rgba(59,130,246,0.15)",
2213
+ color: dev.isBypass ? "#10b981" : "var(--dsw-alias-brand-primary, #3b82f6)",
2214
+ fontWeight: "normal",
2215
+ whiteSpace: "nowrap"
2216
+ },
2217
+ children: formatAuthType(dev.authType, lang)
2218
+ }
2219
+ )
2220
+ ]
2221
+ }
2222
+ ),
2223
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2224
+ "div",
2225
+ {
2226
+ style: {
2227
+ fontSize: "12px",
2228
+ color: "var(--dsw-alias-label-tertiary, #888)",
2229
+ lineHeight: "1.4",
2230
+ wordBreak: "break-word"
2231
+ },
2232
+ children: [
2233
+ lang === "en" ? "IP: " : "\u6765\u6E90 IP: ",
2234
+ ipLabel,
2235
+ lang === "en" ? " \xB7 Authorized: " : " \xB7 \u6388\u6743\u4E8E ",
2236
+ formatTime(dev.createdAt, lang),
2237
+ lang === "en" ? " \xB7 Last seen: " : " \xB7 \u6700\u8FD1\u6D3B\u8DC3 ",
2238
+ formatTime(dev.lastSeenAt, lang)
2239
+ ]
2240
+ }
2241
+ )
2242
+ ]
2243
+ }
2244
+ ),
2245
+ !dev.isBypass ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2246
+ "button",
2247
+ {
2248
+ type: "button",
2249
+ onClick: () => revokeDevice(dev.token),
2250
+ style: {
2251
+ padding: "5px 12px",
2252
+ background: "transparent",
2253
+ border: "1px solid rgba(239,68,68,0.3)",
2254
+ color: "var(--dsw-alias-label-error, #ef4444)",
2255
+ borderRadius: "6px",
2256
+ fontSize: "12px",
2257
+ cursor: "pointer",
2258
+ whiteSpace: "nowrap",
2259
+ flexShrink: 0
2260
+ },
2261
+ children: t("revokeDeviceBtn", lang)
2262
+ }
2263
+ ) : null
2264
+ ]
2265
+ },
2266
+ dev.token
2267
+ );
2268
+ }) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2269
+ "div",
2270
+ {
2271
+ style: {
2272
+ textAlign: "center",
2273
+ padding: "16px",
2274
+ color: "var(--dsw-alias-label-tertiary, #888)",
2275
+ fontSize: "13px"
2276
+ },
2277
+ children: t("noDevices", lang)
2278
+ }
2279
+ )
2280
+ ]
2281
+ }
2282
+ );
2283
+ }
2284
+
2285
+ // src/client/components/SecurityCard.jsx
2286
+ var import_react5 = __toESM(require("react"), 1);
2287
+ var import_jsx_runtime5 = require("react/jsx-runtime");
2288
+ function isCurrentLocalIp2(ip, status) {
2289
+ if (!ip) return false;
2290
+ if (ip === "127.0.0.1" || ip === "::1" || ip === "localhost") return true;
2291
+ if (status && status.lanIp && ip === status.lanIp) return true;
2292
+ if (status && status.tailscaleIp && ip === status.tailscaleIp) return true;
2293
+ if (typeof window !== "undefined" && window.location && window.location.hostname && ip === window.location.hostname)
2294
+ return true;
2295
+ return false;
2296
+ }
2297
+ function SecurityCard({ status, unlockIp, clearIpStats, lang }) {
2298
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2299
+ "div",
2300
+ {
2301
+ style: {
2302
+ background: "var(--dsw-alias-bg-layer-3, rgba(128,128,128,0.06))",
2303
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.15))",
2304
+ borderRadius: "12px",
2305
+ padding: "14px 14px"
2306
+ },
2307
+ children: [
2308
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2309
+ "div",
2310
+ {
2311
+ style: {
2312
+ display: "flex",
2313
+ alignItems: "center",
2314
+ justifyContent: "space-between",
2315
+ flexWrap: "wrap",
2316
+ gap: "8px 12px",
2317
+ marginBottom: "8px"
2318
+ },
2319
+ children: [
2320
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px", minWidth: 0 }, children: [
2321
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { fontSize: "16px", flexShrink: 0 }, children: "\u{1F6E1}\uFE0F" }),
2322
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2323
+ "span",
2324
+ {
2325
+ style: {
2326
+ fontSize: "15px",
2327
+ fontWeight: "600",
2328
+ color: "var(--dsw-alias-label-primary, inherit)"
2329
+ },
2330
+ children: t("securityCardTitle", lang)
2331
+ }
2332
+ )
2333
+ ] }),
2334
+ status.ipSecurityStats && status.ipSecurityStats.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2335
+ "button",
2336
+ {
2337
+ type: "button",
2338
+ onClick: clearIpStats,
2339
+ style: {
2340
+ background: "transparent",
2341
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))",
2342
+ color: "var(--dsw-alias-label-secondary, inherit)",
2343
+ padding: "4px 10px",
2344
+ borderRadius: "6px",
2345
+ fontSize: "11px",
2346
+ cursor: "pointer",
2347
+ whiteSpace: "nowrap",
2348
+ flexShrink: 0,
2349
+ marginLeft: "auto"
2350
+ },
2351
+ children: t("clearSecurityLogBtn", lang)
2352
+ }
2353
+ ) : null
2354
+ ]
2355
+ }
2356
+ ),
2357
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2358
+ "div",
2359
+ {
2360
+ style: {
2361
+ display: "flex",
2362
+ alignItems: "center",
2363
+ gap: "8px",
2364
+ flexWrap: "wrap",
2365
+ marginBottom: "8px"
2366
+ },
2367
+ children: [
2368
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2369
+ "span",
2370
+ {
2371
+ style: {
2372
+ fontSize: "12px",
2373
+ color: "#10b981",
2374
+ background: "rgba(16,185,129,0.12)",
2375
+ padding: "2px 8px",
2376
+ borderRadius: "12px",
2377
+ fontWeight: "500"
2378
+ },
2379
+ children: lang === "en" ? `\u23F1\uFE0F IP Rate Limit: ${status.maxVisitsPerMinute || 60}/min` : `\u23F1\uFE0F \u5355 IP \u9650\u9891 ${status.maxVisitsPerMinute || 60}\u6B21/\u5206\u949F`
2380
+ }
2381
+ ),
2382
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2383
+ "span",
2384
+ {
2385
+ style: {
2386
+ fontSize: "12px",
2387
+ color: "#10b981",
2388
+ background: "rgba(16,185,129,0.12)",
2389
+ padding: "2px 8px",
2390
+ borderRadius: "12px",
2391
+ fontWeight: "500"
2392
+ },
2393
+ children: lang === "en" ? `\u{1F6E1}\uFE0F Lockout: ${status.maxFailedAttempts || 5} fails / ${Math.round(
2394
+ (status.lockDurationMs || 9e5) / 6e4
2395
+ )} mins` : `\u{1F6E1}\uFE0F \u8FDE\u7EED ${status.maxFailedAttempts || 5} \u6B21\u9519\u8BEF\u5C01\u9501 ${Math.round(
2396
+ (status.lockDurationMs || 9e5) / 6e4
2397
+ )} \u5206\u949F`
2398
+ }
2399
+ )
2400
+ ]
2401
+ }
2402
+ ),
2403
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2404
+ "div",
2405
+ {
2406
+ style: {
2407
+ fontSize: "12px",
2408
+ color: "var(--dsw-alias-label-tertiary, #888)",
2409
+ marginBottom: "12px",
2410
+ lineHeight: "1.5"
2411
+ },
2412
+ children: t("securityCardDesc", lang)
2413
+ }
2414
+ ),
2415
+ status.ipSecurityStats && status.ipSecurityStats.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: "8px" }, children: status.ipSecurityStats.map((stat) => {
2416
+ const isLocked = stat.lockedUntil && stat.lockedUntil > Date.now();
2417
+ const remMin = isLocked ? Math.ceil((stat.lockedUntil - Date.now()) / 6e4) : 0;
2418
+ const matchedDevice = status.devices && Array.isArray(status.devices) ? status.devices.find((d) => d.ip === stat.ip) : null;
2419
+ let effectiveAuthType = stat.authType;
2420
+ if (!effectiveAuthType || effectiveAuthType === "\u26AA \u5F85\u8BA4\u8BC1" || effectiveAuthType === "\u26AA Pending") {
2421
+ if (matchedDevice) {
2422
+ if (matchedDevice.authType && matchedDevice.authType.indexOf("\u514D\u5BC6") !== -1) {
2423
+ effectiveAuthType = lang === "en" ? "\u26A1 Bypass Access" : matchedDevice.authType;
2424
+ } else if (matchedDevice.authType && matchedDevice.authType.indexOf("\u5BC6\u7801") !== -1) {
2425
+ effectiveAuthType = lang === "en" ? "\u{1F511} Password Auth" : "\u{1F511} \u957F\u671F\u5BC6\u7801\u8BA4\u8BC1";
2426
+ } else {
2427
+ effectiveAuthType = lang === "en" ? "\u{1F4F1} QR Pairing Auth" : "\u{1F4F1} \u626B\u7801\u914D\u5BF9\u8BA4\u8BC1";
2428
+ }
2429
+ }
2430
+ }
2431
+ let effectiveDeviceName = stat.deviceName;
2432
+ if (!effectiveDeviceName && matchedDevice && matchedDevice.deviceName) {
2433
+ effectiveDeviceName = matchedDevice.deviceName;
2434
+ }
2435
+ const isLocal = isCurrentLocalIp2(stat.ip, status);
2436
+ const ipLabel = `${stat.ip}${isLocal ? lang === "en" ? " (Local)" : " (\u672C\u673A)" : ""}`;
2437
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2438
+ "div",
2439
+ {
2440
+ style: {
2441
+ display: "flex",
2442
+ alignItems: "center",
2443
+ justifyContent: "space-between",
2444
+ gap: "12px",
2445
+ padding: "10px 14px",
2446
+ background: isLocked ? "rgba(239,68,68,0.08)" : "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.08))",
2447
+ border: isLocked ? "1px solid rgba(239,68,68,0.3)" : "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.1))",
2448
+ borderRadius: "8px"
2449
+ },
2450
+ children: [
2451
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2452
+ "div",
2453
+ {
2454
+ style: {
2455
+ display: "flex",
2456
+ flexDirection: "column",
2457
+ gap: "3px",
2458
+ minWidth: 0,
2459
+ flex: 1
2460
+ },
2461
+ children: [
2462
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2463
+ "div",
2464
+ {
2465
+ style: {
2466
+ fontSize: "13px",
2467
+ fontWeight: "600",
2468
+ color: "var(--dsw-alias-label-primary, inherit)",
2469
+ display: "flex",
2470
+ alignItems: "center",
2471
+ gap: "8px",
2472
+ flexWrap: "wrap"
2473
+ },
2474
+ children: [
2475
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { fontWeight: "600" }, children: stat.deviceName ? `${formatDeviceName(stat.deviceName, lang)} \xB7 ${stat.ip}${isLocal ? lang === "en" ? " (Local)" : " (\u672C\u673A)" : ""}` : `${stat.ip}${isLocal ? lang === "en" ? " (Local)" : " (\u672C\u673A)" : ""}` }),
2476
+ effectiveAuthType ? (() => {
2477
+ const translatedAuth = formatAuthType(effectiveAuthType, lang);
2478
+ let bg = "rgba(59,130,246,0.12)";
2479
+ let color = "#3b82f6";
2480
+ if (effectiveAuthType.indexOf("\u514D\u5BC6") !== -1 || effectiveAuthType.indexOf("Bypass") !== -1) {
2481
+ bg = "rgba(16,185,129,0.15)";
2482
+ color = "#10b981";
2483
+ } else if (effectiveAuthType.indexOf("\u626B\u7801") !== -1 || effectiveAuthType.indexOf("Pairing") !== -1 || effectiveAuthType.indexOf("QR") !== -1) {
2484
+ bg = "rgba(59,130,246,0.15)";
2485
+ color = "#3b82f6";
2486
+ } else if (effectiveAuthType.indexOf("\u5BC6\u7801") !== -1 || effectiveAuthType.indexOf("Password") !== -1) {
2487
+ bg = "rgba(139,92,246,0.15)";
2488
+ color = "#a78bfa";
2489
+ }
2490
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2491
+ "span",
2492
+ {
2493
+ style: {
2494
+ fontSize: "11px",
2495
+ padding: "1px 6px",
2496
+ borderRadius: "4px",
2497
+ background: bg,
2498
+ color,
2499
+ fontWeight: "600",
2500
+ whiteSpace: "nowrap"
2501
+ },
2502
+ children: translatedAuth
2503
+ }
2504
+ );
2505
+ })() : null,
2506
+ isLocked ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2507
+ "span",
2508
+ {
2509
+ style: {
2510
+ fontSize: "11px",
2511
+ padding: "1px 6px",
2512
+ borderRadius: "4px",
2513
+ background: "rgba(239,68,68,0.2)",
2514
+ color: "#ef4444",
2515
+ fontWeight: "700",
2516
+ whiteSpace: "nowrap"
2517
+ },
2518
+ children: lang === "en" ? `\u{1F6AB} Locked (${remMin}m left)` : `\u{1F6AB} \u5DF2\u88AB\u9501\u5B9A (\u8FD8\u5269 ${remMin} \u5206\u949F)`
2519
+ }
2520
+ ) : stat.failedAttempts > 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2521
+ "span",
2522
+ {
2523
+ style: {
2524
+ fontSize: "11px",
2525
+ padding: "1px 6px",
2526
+ borderRadius: "4px",
2527
+ background: "rgba(245,158,11,0.2)",
2528
+ color: "#f59e0b",
2529
+ fontWeight: "600",
2530
+ whiteSpace: "nowrap"
2531
+ },
2532
+ children: formatFailedAttempts(stat.failedAttempts, lang)
2533
+ }
2534
+ ) : !effectiveAuthType ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2535
+ "span",
2536
+ {
2537
+ style: {
2538
+ fontSize: "11px",
2539
+ padding: "1px 6px",
2540
+ borderRadius: "4px",
2541
+ background: "rgba(148,163,184,0.15)",
2542
+ color: "#94a3b8",
2543
+ fontWeight: "600",
2544
+ whiteSpace: "nowrap"
2545
+ },
2546
+ children: lang === "en" ? "\u26AA Pending" : "\u26AA \u5F85\u8BA4\u8BC1"
2547
+ }
2548
+ ) : null
2549
+ ]
2550
+ }
2551
+ ),
2552
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2553
+ "div",
2554
+ {
2555
+ style: {
2556
+ fontSize: "11px",
2557
+ color: "var(--dsw-alias-label-tertiary, #888)",
2558
+ lineHeight: "1.4",
2559
+ wordBreak: "break-word"
2560
+ },
2561
+ children: lang === "en" ? `Visits: ${stat.authVisits} \xB7 Attempts: ${stat.totalAttempts} \xB7 Consecutive fails: ${stat.failedAttempts} \xB7 Last seen: ${formatTime(stat.lastSeenAt, lang)}` : `\u6253\u5F00\u767B\u5F55\u9875 ${stat.authVisits} \u6B21 \xB7 \u5C1D\u8BD5\u914D\u5BF9 ${stat.totalAttempts} \u6B21 \xB7 \u8FDE\u7EED\u5931\u8D25 ${stat.failedAttempts} \u6B21 \xB7 \u6700\u8FD1\u8BBF\u95EE ${formatTime(stat.lastSeenAt, lang)}`
2562
+ }
2563
+ )
2564
+ ]
2565
+ }
2566
+ ),
2567
+ isLocked ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2568
+ "button",
2569
+ {
2570
+ type: "button",
2571
+ onClick: () => unlockIp(stat.ip),
2572
+ style: {
2573
+ padding: "5px 12px",
2574
+ background: "rgba(59,130,246,0.12)",
2575
+ border: "1px solid rgba(59,130,246,0.3)",
2576
+ color: "var(--dsw-alias-brand-primary, #3b82f6)",
2577
+ borderRadius: "6px",
2578
+ fontSize: "12px",
2579
+ fontWeight: "600",
2580
+ cursor: "pointer",
2581
+ whiteSpace: "nowrap",
2582
+ flexShrink: 0
2583
+ },
2584
+ children: t("unlockIpBtn", lang)
2585
+ }
2586
+ ) : null
2587
+ ]
2588
+ },
2589
+ stat.ip
2590
+ );
2591
+ }) }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2592
+ "div",
2593
+ {
2594
+ style: {
2595
+ textAlign: "center",
2596
+ padding: "14px",
2597
+ color: "var(--dsw-alias-label-tertiary, #888)",
2598
+ fontSize: "13px"
2599
+ },
2600
+ children: t("securityNormal", lang)
2601
+ }
2602
+ )
2603
+ ]
2604
+ }
2605
+ );
2606
+ }
2607
+
2608
+ // src/client/components/StorageCard.jsx
2609
+ var import_react6 = __toESM(require("react"), 1);
2610
+ var import_jsx_runtime6 = require("react/jsx-runtime");
2611
+ function StorageCard({ status, lang, copyText }) {
2612
+ const handleCopy = (path, tip) => {
2613
+ if (!path) return;
2614
+ if (typeof copyText === "function") {
2615
+ copyText(path, tip);
2616
+ return;
2617
+ }
2618
+ if (typeof navigator !== "undefined" && navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
2619
+ navigator.clipboard.writeText(path).catch(() => {
2620
+ });
2621
+ }
2622
+ };
2623
+ const devicesPath = status.persistPath || "";
2624
+ const rsaPath = status.rsaKeyPath || "";
2625
+ const stylesPath = status.styleSnippetsPath || "";
2626
+ const items = [
2627
+ {
2628
+ icon: "\u{1F4C1}",
2629
+ title: t("storageDeviceFile", lang),
2630
+ path: devicesPath,
2631
+ tip: lang === "en" ? "Devices storage path copied!" : "\u5DF2\u6210\u529F\u590D\u5236\u8BBE\u5907\u5B58\u50A8\u6587\u4EF6\u8DEF\u5F84\uFF01"
2632
+ },
2633
+ {
2634
+ icon: "\u{1F511}",
2635
+ title: t("storageRsaFile", lang),
2636
+ path: rsaPath,
2637
+ tip: lang === "en" ? "RSA key path copied!" : "\u5DF2\u6210\u529F\u590D\u5236 RSA \u5BC6\u94A5\u6587\u4EF6\u8DEF\u5F84\uFF01"
2638
+ },
2639
+ {
2640
+ icon: "\u{1F3A8}",
2641
+ title: t("storageStylesFile", lang),
2642
+ path: stylesPath,
2643
+ tip: lang === "en" ? "Style snippets path copied!" : "\u5DF2\u6210\u529F\u590D\u5236\u6837\u5F0F\u7247\u6BB5\u6587\u4EF6\u8DEF\u5F84\uFF01"
2644
+ }
2645
+ ];
2646
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2647
+ "div",
2648
+ {
2649
+ style: {
2650
+ background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.04))",
2651
+ border: "1px dashed var(--dsw-alias-border-l2, rgba(128,128,128,0.2))",
2652
+ borderRadius: "10px",
2653
+ padding: "14px 14px",
2654
+ display: "flex",
2655
+ flexDirection: "column",
2656
+ gap: "10px"
2657
+ },
2658
+ children: items.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2659
+ "div",
2660
+ {
2661
+ style: {
2662
+ display: "flex",
2663
+ alignItems: "center",
2664
+ justifyContent: "space-between",
2665
+ gap: "12px",
2666
+ paddingTop: index > 0 ? "8px" : "0",
2667
+ borderTop: index > 0 ? "1px dashed var(--dsw-alias-border-l2, rgba(128,128,128,0.15))" : "none"
2668
+ },
2669
+ children: [
2670
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "10px", minWidth: 0, flex: 1 }, children: [
2671
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: { fontSize: "18px", flexShrink: 0 }, children: item.icon }),
2672
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "2px", minWidth: 0 }, children: [
2673
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2674
+ "div",
2675
+ {
2676
+ style: {
2677
+ fontSize: "12px",
2678
+ fontWeight: "600",
2679
+ color: "var(--dsw-alias-label-primary, inherit)"
2680
+ },
2681
+ children: item.title
2682
+ }
2683
+ ),
2684
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2685
+ "code",
2686
+ {
2687
+ style: {
2688
+ fontSize: "11px",
2689
+ color: "var(--dsw-alias-brand-primary, #3b82f6)",
2690
+ fontFamily: "monospace",
2691
+ wordBreak: "break-all"
2692
+ },
2693
+ children: item.path || "..."
2694
+ }
2695
+ )
2696
+ ] })
2697
+ ] }),
2698
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2699
+ "button",
2700
+ {
2701
+ type: "button",
2702
+ onClick: () => handleCopy(item.path, item.tip),
2703
+ style: {
2704
+ padding: "4px 10px",
2705
+ background: "var(--dsw-alias-bg-layer-3, rgba(128,128,128,0.1))",
2706
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.2))",
2707
+ color: "var(--dsw-alias-label-secondary, inherit)",
2708
+ borderRadius: "6px",
2709
+ fontSize: "11px",
2710
+ cursor: "pointer",
2711
+ whiteSpace: "nowrap",
2712
+ flexShrink: 0
2713
+ },
2714
+ children: t("copyUrlBtn", lang)
2715
+ }
2716
+ )
2717
+ ]
2718
+ },
2719
+ index
2720
+ ))
2721
+ }
2722
+ );
2723
+ }
2724
+
2725
+ // src/client/components/ConfigCard.jsx
2726
+ var import_react7 = __toESM(require("react"), 1);
2727
+ var import_jsx_runtime7 = require("react/jsx-runtime");
2728
+ function ConfigCard({
2729
+ maxVisitsInput,
2730
+ setMaxVisitsInput,
2731
+ maxFailedInput,
2732
+ setMaxFailedInput,
2733
+ lockDurationMinsInput,
2734
+ setLockDurationMinsInput,
2735
+ saveAdvancedConfig,
2736
+ resetAdvancedConfigDefaults,
2737
+ isSaving,
2738
+ lang,
2739
+ copyText
2740
+ }) {
2741
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2742
+ "div",
2743
+ {
2744
+ style: {
2745
+ background: "var(--dsw-alias-bg-layer-3, rgba(128,128,128,0.06))",
2746
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.15))",
2747
+ borderRadius: "12px",
2748
+ padding: "14px 14px",
2749
+ display: "flex",
2750
+ flexDirection: "column",
2751
+ gap: "14px"
2752
+ },
2753
+ children: [
2754
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2755
+ "div",
2756
+ {
2757
+ style: {
2758
+ display: "flex",
2759
+ alignItems: "center",
2760
+ justifyContent: "space-between",
2761
+ flexWrap: "wrap",
2762
+ gap: "8px 12px"
2763
+ },
2764
+ children: [
2765
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
2766
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { fontSize: "16px" }, children: "\u2699\uFE0F" }),
2767
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2768
+ "span",
2769
+ {
2770
+ style: {
2771
+ fontSize: "15px",
2772
+ fontWeight: "600",
2773
+ color: "var(--dsw-alias-label-primary, inherit)"
2774
+ },
2775
+ children: t("configCardTitle", lang)
2776
+ }
2777
+ )
2778
+ ] }),
2779
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2780
+ "button",
2781
+ {
2782
+ type: "button",
2783
+ onClick: () => {
2784
+ if (typeof copyText === "function") {
2785
+ copyText(
2786
+ "~/.dsh/remote-mobile/settings.json",
2787
+ lang === "en" ? "Settings path copied (~/.dsh/remote-mobile/settings.json)" : "\u5DF2\u590D\u5236\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84\uFF08~/.dsh/remote-mobile/settings.json\uFF09"
2788
+ );
2789
+ }
2790
+ },
2791
+ style: {
2792
+ padding: "4px 10px",
2793
+ background: "var(--dsw-alias-bg-layer-3, rgba(128,128,128,0.1))",
2794
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.2))",
2795
+ color: "var(--dsw-alias-label-secondary, inherit)",
2796
+ borderRadius: "6px",
2797
+ fontSize: "11px",
2798
+ cursor: "pointer",
2799
+ whiteSpace: "nowrap",
2800
+ flexShrink: 0,
2801
+ marginLeft: "auto"
2802
+ },
2803
+ children: t("copySettingsPathBtn", lang)
2804
+ }
2805
+ )
2806
+ ]
2807
+ }
2808
+ ),
2809
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2810
+ "div",
2811
+ {
2812
+ style: {
2813
+ fontSize: "12px",
2814
+ color: "var(--dsw-alias-label-tertiary, #888)",
2815
+ lineHeight: "1.5"
2816
+ },
2817
+ children: t("configCardDesc", lang)
2818
+ }
2819
+ ),
2820
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2821
+ "div",
2822
+ {
2823
+ style: {
2824
+ display: "grid",
2825
+ gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
2826
+ gap: "12px"
2827
+ },
2828
+ children: [
2829
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2830
+ "div",
2831
+ {
2832
+ style: {
2833
+ display: "flex",
2834
+ flexDirection: "column",
2835
+ gap: "6px",
2836
+ background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.06))",
2837
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.12))",
2838
+ borderRadius: "8px",
2839
+ padding: "10px 12px"
2840
+ },
2841
+ children: [
2842
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2843
+ "label",
2844
+ {
2845
+ style: {
2846
+ fontSize: "12px",
2847
+ fontWeight: "600",
2848
+ color: "var(--dsw-alias-label-primary, inherit)"
2849
+ },
2850
+ children: t("fieldVisitsLimit", lang)
2851
+ }
2852
+ ),
2853
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2854
+ "input",
2855
+ {
2856
+ type: "number",
2857
+ min: "1",
2858
+ max: "1000",
2859
+ value: maxVisitsInput,
2860
+ onChange: (e) => setMaxVisitsInput(e.target.value),
2861
+ placeholder: "60",
2862
+ style: {
2863
+ height: "34px",
2864
+ background: "var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.15))",
2865
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))",
2866
+ borderRadius: "6px",
2867
+ padding: "0 10px",
2868
+ color: "inherit",
2869
+ fontSize: "13px",
2870
+ outline: "none"
2871
+ }
2872
+ }
2873
+ ),
2874
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { fontSize: "11px", color: "var(--dsw-alias-label-tertiary, #888)" }, children: t("fieldVisitsLimitHint", lang) })
2875
+ ]
2876
+ }
2877
+ ),
2878
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2879
+ "div",
2880
+ {
2881
+ style: {
2882
+ display: "flex",
2883
+ flexDirection: "column",
2884
+ gap: "6px",
2885
+ background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.06))",
2886
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.12))",
2887
+ borderRadius: "8px",
2888
+ padding: "10px 12px"
2889
+ },
2890
+ children: [
2891
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2892
+ "label",
2893
+ {
2894
+ style: {
2895
+ fontSize: "12px",
2896
+ fontWeight: "600",
2897
+ color: "var(--dsw-alias-label-primary, inherit)"
2898
+ },
2899
+ children: t("fieldFailedLimit", lang)
2900
+ }
2901
+ ),
2902
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2903
+ "input",
2904
+ {
2905
+ type: "number",
2906
+ min: "1",
2907
+ max: "100",
2908
+ value: maxFailedInput,
2909
+ onChange: (e) => setMaxFailedInput(e.target.value),
2910
+ placeholder: "5",
2911
+ style: {
2912
+ height: "34px",
2913
+ background: "var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.15))",
2914
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))",
2915
+ borderRadius: "6px",
2916
+ padding: "0 10px",
2917
+ color: "inherit",
2918
+ fontSize: "13px",
2919
+ outline: "none"
2920
+ }
2921
+ }
2922
+ ),
2923
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { fontSize: "11px", color: "var(--dsw-alias-label-tertiary, #888)" }, children: t("fieldFailedLimitHint", lang) })
2924
+ ]
2925
+ }
2926
+ ),
2927
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2928
+ "div",
2929
+ {
2930
+ style: {
2931
+ display: "flex",
2932
+ flexDirection: "column",
2933
+ gap: "6px",
2934
+ background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.06))",
2935
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.12))",
2936
+ borderRadius: "8px",
2937
+ padding: "10px 12px"
2938
+ },
2939
+ children: [
2940
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2941
+ "label",
2942
+ {
2943
+ style: {
2944
+ fontSize: "12px",
2945
+ fontWeight: "600",
2946
+ color: "var(--dsw-alias-label-primary, inherit)"
2947
+ },
2948
+ children: t("fieldLockDuration", lang)
2949
+ }
2950
+ ),
2951
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2952
+ "input",
2953
+ {
2954
+ type: "number",
2955
+ min: "1",
2956
+ max: "1440",
2957
+ value: lockDurationMinsInput,
2958
+ onChange: (e) => setLockDurationMinsInput(e.target.value),
2959
+ placeholder: "15",
2960
+ style: {
2961
+ height: "34px",
2962
+ background: "var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.15))",
2963
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))",
2964
+ borderRadius: "6px",
2965
+ padding: "0 10px",
2966
+ color: "inherit",
2967
+ fontSize: "13px",
2968
+ outline: "none"
2969
+ }
2970
+ }
2971
+ ),
2972
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { fontSize: "11px", color: "var(--dsw-alias-label-tertiary, #888)" }, children: t("fieldLockDurationHint", lang) })
2973
+ ]
2974
+ }
2975
+ )
2976
+ ]
2977
+ }
2978
+ ),
2979
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2980
+ "div",
2981
+ {
2982
+ style: {
2983
+ display: "flex",
2984
+ alignItems: "center",
2985
+ justifyContent: "flex-end",
2986
+ gap: "10px",
2987
+ marginTop: "4px"
2988
+ },
2989
+ children: [
2990
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2991
+ "button",
2992
+ {
2993
+ type: "button",
2994
+ onClick: resetAdvancedConfigDefaults,
2995
+ disabled: isSaving,
2996
+ style: {
2997
+ padding: "6px 14px",
2998
+ background: "transparent",
2999
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))",
3000
+ color: "var(--dsw-alias-label-secondary, inherit)",
3001
+ borderRadius: "6px",
3002
+ fontSize: "12px",
3003
+ cursor: isSaving ? "not-allowed" : "pointer",
3004
+ opacity: isSaving ? 0.6 : 1,
3005
+ whiteSpace: "nowrap"
3006
+ },
3007
+ children: t("restoreDefaultsBtn", lang)
3008
+ }
3009
+ ),
3010
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3011
+ "button",
3012
+ {
3013
+ type: "button",
3014
+ onClick: saveAdvancedConfig,
3015
+ disabled: isSaving,
3016
+ style: {
3017
+ padding: "6px 18px",
3018
+ background: "#2563eb",
3019
+ border: "none",
3020
+ color: "#ffffff",
3021
+ borderRadius: "6px",
3022
+ fontSize: "12px",
3023
+ fontWeight: "600",
3024
+ cursor: isSaving ? "not-allowed" : "pointer",
3025
+ opacity: isSaving ? 0.7 : 1,
3026
+ whiteSpace: "nowrap",
3027
+ boxShadow: "0 2px 6px rgba(37, 99, 235, 0.3)"
3028
+ },
3029
+ children: isSaving ? t("savingConfigBtn", lang) : t("saveConfigBtn", lang)
3030
+ }
3031
+ )
3032
+ ]
3033
+ }
3034
+ )
3035
+ ]
3036
+ }
3037
+ );
3038
+ }
3039
+
3040
+ // src/client/components/StylesCard.jsx
3041
+ var import_react8 = __toESM(require("react"), 1);
3042
+ var import_jsx_runtime8 = require("react/jsx-runtime");
3043
+ function StylesCard({ lang, showToast }) {
3044
+ const [snippets, setSnippets] = (0, import_react8.useState)([]);
3045
+ const [loading, setLoading] = (0, import_react8.useState)(true);
3046
+ const [editingId, setEditingId] = (0, import_react8.useState)(null);
3047
+ const [showForm, setShowForm] = (0, import_react8.useState)(false);
3048
+ const [name, setName] = (0, import_react8.useState)("");
3049
+ const [css, setCss] = (0, import_react8.useState)("");
3050
+ const [pcEnabled, setPcEnabled] = (0, import_react8.useState)(false);
3051
+ const [mobileEnabled, setMobileEnabled] = (0, import_react8.useState)(true);
3052
+ const [expanded, setExpanded] = (0, import_react8.useState)({});
3053
+ const [feedback, setFeedback] = (0, import_react8.useState)("");
3054
+ const showFeedback = (0, import_react8.useCallback)((msg, type = "success") => {
3055
+ if (typeof showToast === "function") {
3056
+ showToast(msg, type);
3057
+ } else {
3058
+ setFeedback(msg);
3059
+ setTimeout(() => setFeedback(""), 3500);
3060
+ }
3061
+ }, [showToast]);
3062
+ const load = (0, import_react8.useCallback)(() => {
3063
+ fetchStyles().then((data) => {
3064
+ setSnippets(data.snippets || []);
3065
+ setLoading(false);
3066
+ }).catch(() => {
3067
+ setLoading(false);
3068
+ showFeedback(t("stylesLoadFail", lang), "danger");
3069
+ });
3070
+ }, [lang, showFeedback]);
3071
+ (0, import_react8.useEffect)(() => {
3072
+ load();
3073
+ }, [load]);
3074
+ const toggle = (id, scope, enabled) => {
3075
+ toggleStyleApi(id, scope, enabled).then((data) => {
3076
+ if (data.success) {
3077
+ load();
3078
+ const scopeLabel = scope === "pc" ? lang === "en" ? "PC" : "PC \u7AEF" : lang === "en" ? "Mobile" : "\u79FB\u52A8\u7AEF";
3079
+ showFeedback(
3080
+ enabled ? lang === "en" ? scopeLabel + " enabled!" : scopeLabel + " \u5DF2\u542F\u7528\uFF01" : lang === "en" ? scopeLabel + " disabled!" : scopeLabel + " \u5DF2\u505C\u7528\uFF01",
3081
+ "success"
3082
+ );
3083
+ } else {
3084
+ showFeedback((lang === "en" ? "Toggle failed: " : "\u5207\u6362\u5931\u8D25\uFF1A") + (data.reason || ""), "danger");
3085
+ }
3086
+ }).catch((err) => {
3087
+ showFeedback((lang === "en" ? "Toggle failed: " : "\u5207\u6362\u5931\u8D25\uFF1A") + (err?.message || err), "danger");
3088
+ });
3089
+ };
3090
+ const startEdit = (s) => {
3091
+ setEditingId(s.id);
3092
+ setShowForm(true);
3093
+ setName(s.name);
3094
+ setCss(s.css);
3095
+ setPcEnabled(s.pcEnabled !== false);
3096
+ setMobileEnabled(s.mobileEnabled !== false);
3097
+ setFeedback("");
3098
+ };
3099
+ const cancelEdit = () => {
3100
+ setEditingId(null);
3101
+ setShowForm(false);
3102
+ setName("");
3103
+ setCss("");
3104
+ setPcEnabled(false);
3105
+ setMobileEnabled(true);
3106
+ setFeedback("");
3107
+ };
3108
+ const save = () => {
3109
+ if (!name.trim() || !css.trim()) {
3110
+ showFeedback(t("stylesFieldsRequired", lang), "danger");
3111
+ return;
3112
+ }
3113
+ saveStyleSnippet({
3114
+ id: editingId || void 0,
3115
+ name: name.trim(),
3116
+ css,
3117
+ pcEnabled,
3118
+ mobileEnabled
3119
+ }).then((data) => {
3120
+ if (data.success) {
3121
+ load();
3122
+ cancelEdit();
3123
+ showFeedback(t("stylesSaveSuccess", lang));
3124
+ } else {
3125
+ showFeedback((lang === "en" ? "Save failed: " : "\u4FDD\u5B58\u5931\u8D25\uFF1A") + (data.reason || ""), "danger");
3126
+ }
3127
+ }).catch((err) => {
3128
+ showFeedback((lang === "en" ? "Save failed: " : "\u4FDD\u5B58\u5931\u8D25\uFF1A") + (err?.message || err), "danger");
3129
+ });
3130
+ };
3131
+ const remove = (s) => {
3132
+ if (s.builtin) return;
3133
+ if (confirm(t("stylesDeleteConfirm", lang))) {
3134
+ deleteStyleSnippetApi(s.id).then((data) => {
3135
+ if (data.success) {
3136
+ if (editingId === s.id) cancelEdit();
3137
+ load();
3138
+ showFeedback(lang === "en" ? "Style snippet deleted!" : "\u81EA\u5B9A\u4E49\u6837\u5F0F\u7247\u6BB5\u5DF2\u5220\u9664\uFF01");
3139
+ } else {
3140
+ showFeedback((lang === "en" ? "Delete failed: " : "\u5220\u9664\u5931\u8D25\uFF1A") + (data.reason || ""), "danger");
3141
+ }
3142
+ }).catch((err) => {
3143
+ showFeedback((lang === "en" ? "Delete failed: " : "\u5220\u9664\u5931\u8D25\uFF1A") + (err?.message || err), "danger");
3144
+ });
3145
+ }
3146
+ };
3147
+ const resetAll = () => {
3148
+ if (!confirm(t("stylesResetConfirm", lang))) return;
3149
+ resetStylesApi().then((data) => {
3150
+ if (data.success) {
3151
+ load();
3152
+ showFeedback(lang === "en" ? "Style snippets reset to defaults!" : "\u5DF2\u6062\u590D\u6240\u6709\u7247\u6BB5\u7684\u9ED8\u8BA4\u542F\u505C\u72B6\u6001\uFF01");
3153
+ } else {
3154
+ showFeedback((lang === "en" ? "Reset failed: " : "\u91CD\u7F6E\u5931\u8D25\uFF1A") + (data.reason || ""), "danger");
3155
+ }
3156
+ }).catch((err) => {
3157
+ showFeedback((lang === "en" ? "Reset failed: " : "\u91CD\u7F6E\u5931\u8D25\uFF1A") + (err?.message || err), "danger");
3158
+ });
3159
+ };
3160
+ const toggleExpand = (id) => {
3161
+ setExpanded((prev) => ({ ...prev, [id]: !prev[id] }));
3162
+ };
3163
+ const copyCss = (cssText) => {
3164
+ navigator.clipboard.writeText(cssText).then(() => showFeedback(t("stylesCopyCssDone", lang))).catch(() => {
3165
+ const ta = document.createElement("textarea");
3166
+ ta.value = cssText;
3167
+ document.body.appendChild(ta);
3168
+ ta.select();
3169
+ try {
3170
+ document.execCommand("copy");
3171
+ showFeedback(t("stylesCopyCssDone", lang));
3172
+ } catch {
3173
+ showFeedback(lang === "en" ? "Copy failed, please copy manually." : "\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u590D\u5236\u3002", "danger");
3174
+ }
3175
+ document.body.removeChild(ta);
3176
+ });
3177
+ };
3178
+ const scopeBadge = (s) => {
3179
+ if (s.pcEnabled && s.mobileEnabled) return t("stylesBothDevices", lang);
3180
+ if (s.mobileEnabled) return "\u{1F4F1} " + t("stylesMobileOnlyBadge", lang);
3181
+ if (s.pcEnabled) return "\u{1F5A5}\uFE0F " + t("stylesPcOnlyBadge", lang);
3182
+ return t("stylesDisabled", lang);
3183
+ };
3184
+ const rowStyle = {
3185
+ display: "flex",
3186
+ flexDirection: "column",
3187
+ gap: "8px",
3188
+ background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.06))",
3189
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.12))",
3190
+ borderRadius: "8px",
3191
+ padding: "10px 12px",
3192
+ boxSizing: "border-box",
3193
+ maxWidth: "100%",
3194
+ overflow: "hidden"
3195
+ };
3196
+ const miniBtnStyle = {
3197
+ padding: "4px 8px",
3198
+ background: "var(--dsw-alias-bg-layer-3, rgba(128,128,128,0.1))",
3199
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.2))",
3200
+ color: "var(--dsw-alias-label-secondary, inherit)",
3201
+ borderRadius: "6px",
3202
+ fontSize: "11px",
3203
+ cursor: "pointer",
3204
+ whiteSpace: "nowrap",
3205
+ flexShrink: 0
3206
+ };
3207
+ const primaryBtnStyle = {
3208
+ ...miniBtnStyle,
3209
+ background: "rgba(59,130,246,0.15)",
3210
+ color: "var(--dsw-alias-brand-primary, #3b82f6)",
3211
+ border: "1px solid rgba(59,130,246,0.3)"
3212
+ };
3213
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
3214
+ "div",
3215
+ {
3216
+ style: {
3217
+ background: "var(--dsw-alias-bg-layer-3, rgba(128,128,128,0.06))",
3218
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.15))",
3219
+ borderRadius: "12px",
3220
+ padding: "14px 14px",
3221
+ display: "flex",
3222
+ flexDirection: "column",
3223
+ gap: "14px"
3224
+ },
3225
+ children: [
3226
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "10px", flexWrap: "wrap" }, children: [
3227
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
3228
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { fontSize: "16px" }, children: "\u{1F3A8}" }),
3229
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { fontSize: "15px", fontWeight: "600", color: "var(--dsw-alias-label-primary, inherit)" }, children: t("stylesCardTitle", lang) })
3230
+ ] }),
3231
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: resetAll, style: miniBtnStyle, children: t("stylesResetAll", lang) })
3232
+ ] }),
3233
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { fontSize: "12px", color: "var(--dsw-alias-label-tertiary, #888)", lineHeight: "1.5" }, children: t("stylesCardDesc", lang) }),
3234
+ feedback && typeof showToast !== "function" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3235
+ "div",
3236
+ {
3237
+ style: {
3238
+ fontSize: "12px",
3239
+ padding: "8px 12px",
3240
+ borderRadius: "8px",
3241
+ background: "rgba(16,185,129,0.12)",
3242
+ color: "var(--dsw-alias-success, #10b981)",
3243
+ border: "1px solid rgba(16,185,129,0.25)"
3244
+ },
3245
+ children: feedback
3246
+ }
3247
+ ) : null,
3248
+ loading ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { fontSize: "12px", color: "var(--dsw-alias-label-tertiary, #888)" }, children: lang === "en" ? "Loading style snippets..." : "\u6B63\u5728\u52A0\u8F7D\u6837\u5F0F\u7247\u6BB5..." }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
3249
+ snippets.some((s) => s.builtin) ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
3250
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { fontSize: "13px", fontWeight: "600", color: "var(--dsw-alias-label-secondary, inherit)" }, children: t("stylesPresetsTitle", lang) }),
3251
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: "8px" }, children: snippets.filter((s) => s.builtin).map((s) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: rowStyle, children: [
3252
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "8px", flexWrap: "wrap" }, children: [
3253
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { flex: "1 1 180px", minWidth: 0, maxWidth: "100%" }, children: [
3254
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "6px", flexWrap: "wrap" }, children: [
3255
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { fontSize: "12px", fontWeight: "600", color: "var(--dsw-alias-label-primary, inherit)" }, children: s.name }),
3256
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3257
+ "span",
3258
+ {
3259
+ style: {
3260
+ fontSize: "10px",
3261
+ padding: "1px 6px",
3262
+ borderRadius: "8px",
3263
+ background: "rgba(139,92,246,0.15)",
3264
+ color: "#8b5cf6",
3265
+ fontWeight: "600"
3266
+ },
3267
+ children: "Preset"
3268
+ }
3269
+ ),
3270
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3271
+ "span",
3272
+ {
3273
+ style: {
3274
+ fontSize: "10px",
3275
+ padding: "1px 6px",
3276
+ borderRadius: "8px",
3277
+ background: "rgba(59,130,246,0.12)",
3278
+ color: "#3b82f6",
3279
+ fontWeight: "600"
3280
+ },
3281
+ children: scopeBadge(s)
3282
+ }
3283
+ )
3284
+ ] }),
3285
+ s.description ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { fontSize: "11px", color: "var(--dsw-alias-label-tertiary, #888)", marginTop: "3px" }, children: s.description }) : null
3286
+ ] }),
3287
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "6px 8px", flexWrap: "wrap", maxWidth: "100%" }, children: [
3288
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { style: { display: "inline-flex", alignItems: "center", gap: "4px", fontSize: "11px", cursor: "pointer", whiteSpace: "nowrap" }, children: [
3289
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3290
+ "input",
3291
+ {
3292
+ type: "checkbox",
3293
+ checked: s.pcEnabled,
3294
+ onChange: (e) => toggle(s.id, "pc", e.target.checked),
3295
+ style: { accentColor: "#3b82f6", width: "16px", height: "16px", cursor: "pointer" }
3296
+ }
3297
+ ),
3298
+ "\u{1F5A5}\uFE0F ",
3299
+ t("stylesPcShort", lang)
3300
+ ] }),
3301
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { style: { display: "inline-flex", alignItems: "center", gap: "4px", fontSize: "11px", cursor: "pointer", whiteSpace: "nowrap" }, children: [
3302
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3303
+ "input",
3304
+ {
3305
+ type: "checkbox",
3306
+ checked: s.mobileEnabled,
3307
+ onChange: (e) => toggle(s.id, "mobile", e.target.checked),
3308
+ style: { accentColor: "#3b82f6", width: "16px", height: "16px", cursor: "pointer" }
3309
+ }
3310
+ ),
3311
+ "\u{1F4F1} ",
3312
+ t("stylesMobileShort", lang)
3313
+ ] }),
3314
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { display: "inline-flex", alignItems: "center", gap: "6px", flexWrap: "wrap" }, children: [
3315
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: () => toggleExpand(s.id), style: miniBtnStyle, children: expanded[s.id] ? t("stylesHideCss", lang) : t("stylesViewCss", lang) }),
3316
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: () => copyCss(s.css), style: miniBtnStyle, children: t("stylesCopyCss", lang) })
3317
+ ] })
3318
+ ] })
3319
+ ] }),
3320
+ expanded[s.id] ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3321
+ "pre",
3322
+ {
3323
+ style: {
3324
+ maxHeight: "180px",
3325
+ overflow: "auto",
3326
+ fontSize: "11px",
3327
+ lineHeight: "1.5",
3328
+ margin: 0,
3329
+ padding: "8px 10px",
3330
+ background: "var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.25))",
3331
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.15))",
3332
+ borderRadius: "6px",
3333
+ color: "var(--dsw-alias-label-secondary, inherit)",
3334
+ whiteSpace: "pre-wrap",
3335
+ wordBreak: "break-word",
3336
+ fontFamily: "var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, monospace)"
3337
+ },
3338
+ children: s.css
3339
+ }
3340
+ ) : null
3341
+ ] }, s.id)) })
3342
+ ] }) : null,
3343
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { fontSize: "13px", fontWeight: "600", color: "var(--dsw-alias-label-secondary, inherit)", marginTop: "4px" }, children: t("stylesCustomTitle", lang) }),
3344
+ !showForm ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3345
+ "button",
3346
+ {
3347
+ type: "button",
3348
+ onClick: () => setShowForm(true),
3349
+ style: {
3350
+ width: "100%",
3351
+ padding: "12px",
3352
+ background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.06))",
3353
+ border: "1px dashed var(--dsw-alias-border-l2, rgba(128,128,128,0.25))",
3354
+ borderRadius: "8px",
3355
+ color: "var(--dsw-alias-brand-primary, #3b82f6)",
3356
+ fontSize: "12px",
3357
+ fontWeight: "600",
3358
+ cursor: "pointer"
3359
+ },
3360
+ children: t("stylesAddNew", lang)
3361
+ }
3362
+ ) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
3363
+ "div",
3364
+ {
3365
+ style: {
3366
+ display: "flex",
3367
+ flexDirection: "column",
3368
+ gap: "8px",
3369
+ background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.06))",
3370
+ border: "1px dashed var(--dsw-alias-border-l2, rgba(128,128,128,0.25))",
3371
+ borderRadius: "8px",
3372
+ padding: "12px"
3373
+ },
3374
+ children: [
3375
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { display: "flex", gap: "8px", flexWrap: "wrap" }, children: [
3376
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3377
+ "input",
3378
+ {
3379
+ type: "text",
3380
+ value: name,
3381
+ onChange: (e) => setName(e.target.value),
3382
+ placeholder: t("stylesNamePlaceholder", lang),
3383
+ style: {
3384
+ flex: 1,
3385
+ minWidth: "160px",
3386
+ padding: "8px 10px",
3387
+ fontSize: "12px",
3388
+ borderRadius: "6px",
3389
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))",
3390
+ background: "var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.2))",
3391
+ color: "var(--dsw-alias-label-primary, inherit)",
3392
+ outline: "none"
3393
+ }
3394
+ }
3395
+ ),
3396
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { style: { display: "inline-flex", alignItems: "center", gap: "4px", fontSize: "11px", cursor: "pointer", whiteSpace: "nowrap" }, children: [
3397
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3398
+ "input",
3399
+ {
3400
+ type: "checkbox",
3401
+ checked: pcEnabled,
3402
+ onChange: (e) => setPcEnabled(e.target.checked),
3403
+ style: { accentColor: "#3b82f6", width: "16px", height: "16px", cursor: "pointer" }
3404
+ }
3405
+ ),
3406
+ "\u{1F5A5}\uFE0F ",
3407
+ t("stylesPcShort", lang)
3408
+ ] }),
3409
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { style: { display: "inline-flex", alignItems: "center", gap: "4px", fontSize: "11px", cursor: "pointer", whiteSpace: "nowrap" }, children: [
3410
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3411
+ "input",
3412
+ {
3413
+ type: "checkbox",
3414
+ checked: mobileEnabled,
3415
+ onChange: (e) => setMobileEnabled(e.target.checked),
3416
+ style: { accentColor: "#3b82f6", width: "16px", height: "16px", cursor: "pointer" }
3417
+ }
3418
+ ),
3419
+ "\u{1F4F1} ",
3420
+ t("stylesMobileShort", lang)
3421
+ ] })
3422
+ ] }),
3423
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3424
+ "textarea",
3425
+ {
3426
+ value: css,
3427
+ onChange: (e) => setCss(e.target.value),
3428
+ placeholder: t("stylesCssPlaceholder", lang),
3429
+ rows: 5,
3430
+ style: {
3431
+ width: "100%",
3432
+ boxSizing: "border-box",
3433
+ padding: "8px 10px",
3434
+ fontSize: "12px",
3435
+ lineHeight: "1.5",
3436
+ borderRadius: "6px",
3437
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))",
3438
+ background: "var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.2))",
3439
+ color: "var(--dsw-alias-label-primary, inherit)",
3440
+ fontFamily: "var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, monospace)",
3441
+ outline: "none",
3442
+ resize: "vertical"
3443
+ }
3444
+ }
3445
+ ),
3446
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { display: "flex", gap: "8px", flexWrap: "wrap" }, children: [
3447
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: save, style: primaryBtnStyle, children: t("stylesSave", lang) }),
3448
+ editingId !== null ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: cancelEdit, style: miniBtnStyle, children: t("stylesCancelEdit", lang) }) : null,
3449
+ showForm && !editingId ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: cancelEdit, style: miniBtnStyle, children: t("stylesCancelEdit", lang) }) : null
3450
+ ] })
3451
+ ]
3452
+ }
3453
+ ),
3454
+ snippets.filter((s) => !s.builtin).length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { fontSize: "12px", color: "var(--dsw-alias-label-tertiary, #888)" }, children: t("stylesEmptyCustom", lang) }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: "8px" }, children: snippets.filter((s) => !s.builtin).map((s) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: rowStyle, children: [
3455
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "8px", flexWrap: "wrap" }, children: [
3456
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { flex: "1 1 180px", minWidth: 0, maxWidth: "100%" }, children: [
3457
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "6px", flexWrap: "wrap" }, children: [
3458
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { fontSize: "12px", fontWeight: "600", color: "var(--dsw-alias-label-primary, inherit)" }, children: s.name }),
3459
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3460
+ "span",
3461
+ {
3462
+ style: {
3463
+ fontSize: "10px",
3464
+ padding: "1px 6px",
3465
+ borderRadius: "8px",
3466
+ background: "rgba(59,130,246,0.12)",
3467
+ color: "#3b82f6",
3468
+ fontWeight: "600"
3469
+ },
3470
+ children: scopeBadge(s)
3471
+ }
3472
+ )
3473
+ ] }),
3474
+ s.description ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { fontSize: "11px", color: "var(--dsw-alias-label-tertiary, #888)", marginTop: "3px" }, children: s.description }) : null
3475
+ ] }),
3476
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "6px 8px", flexWrap: "wrap", maxWidth: "100%" }, children: [
3477
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { style: { display: "inline-flex", alignItems: "center", gap: "4px", fontSize: "11px", cursor: "pointer", whiteSpace: "nowrap" }, children: [
3478
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3479
+ "input",
3480
+ {
3481
+ type: "checkbox",
3482
+ checked: s.pcEnabled,
3483
+ onChange: (e) => toggle(s.id, "pc", e.target.checked),
3484
+ style: { accentColor: "#3b82f6", width: "16px", height: "16px", cursor: "pointer" }
3485
+ }
3486
+ ),
3487
+ "\u{1F5A5}\uFE0F ",
3488
+ t("stylesPcShort", lang)
3489
+ ] }),
3490
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { style: { display: "inline-flex", alignItems: "center", gap: "4px", fontSize: "11px", cursor: "pointer", whiteSpace: "nowrap" }, children: [
3491
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3492
+ "input",
3493
+ {
3494
+ type: "checkbox",
3495
+ checked: s.mobileEnabled,
3496
+ onChange: (e) => toggle(s.id, "mobile", e.target.checked),
3497
+ style: { accentColor: "#3b82f6", width: "16px", height: "16px", cursor: "pointer" }
3498
+ }
3499
+ ),
3500
+ "\u{1F4F1} ",
3501
+ t("stylesMobileShort", lang)
3502
+ ] }),
3503
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { display: "inline-flex", alignItems: "center", gap: "6px", flexWrap: "wrap" }, children: [
3504
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: () => startEdit(s), style: miniBtnStyle, children: t("stylesEditBtn", lang) }),
3505
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: () => remove(s), style: { ...miniBtnStyle, color: "#ef4444" }, children: t("stylesDeleteBtn", lang) }),
3506
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: () => toggleExpand(s.id), style: miniBtnStyle, children: expanded[s.id] ? t("stylesHideCss", lang) : t("stylesViewCss", lang) }),
3507
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: () => copyCss(s.css), style: miniBtnStyle, children: t("stylesCopyCss", lang) })
3508
+ ] })
3509
+ ] })
3510
+ ] }),
3511
+ expanded[s.id] ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3512
+ "pre",
3513
+ {
3514
+ style: {
3515
+ maxHeight: "180px",
3516
+ overflow: "auto",
3517
+ fontSize: "11px",
3518
+ lineHeight: "1.5",
3519
+ margin: 0,
3520
+ padding: "8px 10px",
3521
+ background: "var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.25))",
3522
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.15))",
3523
+ borderRadius: "6px",
3524
+ color: "var(--dsw-alias-label-secondary, inherit)",
3525
+ whiteSpace: "pre-wrap",
3526
+ wordBreak: "break-word",
3527
+ fontFamily: "var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, monospace)"
3528
+ },
3529
+ children: s.css
3530
+ }
3531
+ ) : null
3532
+ ] }, s.id)) }),
3533
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { fontSize: "11px", color: "var(--dsw-alias-label-tertiary, #888)", lineHeight: "1.6" }, children: t("stylesHint", lang) })
3534
+ ] })
3535
+ ]
3536
+ }
3537
+ );
3538
+ }
3539
+
3540
+ // src/client/version.js
3541
+ var PLUGIN_VERSION = "v1.9.1";
3542
+ var BUILD_TIME = "2026-10-13 12:07:35";
3543
+
3544
+ // src/client/components/MainSection.jsx
3545
+ var import_jsx_runtime9 = require("react/jsx-runtime");
3546
+ function TailscaleMobileSection(props) {
3547
+ const ctx = props && props.ctx || typeof window !== "undefined" && window.__DSH_CLIENT_CTX__ || null;
3548
+ const [status, setStatus] = (0, import_react9.useState)({
3549
+ tailscaleIp: "",
3550
+ lanIp: "",
3551
+ code: "",
3552
+ expiresAt: 0,
3553
+ hasSecret: false,
3554
+ allowTailscale: false,
3555
+ allowLan: false,
3556
+ maxVisitsPerMinute: 60,
3557
+ maxFailedAttempts: 5,
3558
+ lockDurationMs: 9e5,
3559
+ devicesCount: 0,
3560
+ devices: [],
3561
+ ipSecurityStats: [],
3562
+ copied: false,
3563
+ loading: true,
3564
+ persistPath: "",
3565
+ rsaKeyPath: "",
3566
+ styleSnippetsPath: "",
3567
+ locale: "zh",
3568
+ pairingBridgeMode: "pending",
3569
+ pairingBridgeConflict: "",
3570
+ pairingBridgeConflictId: ""
3571
+ });
3572
+ const [secretInput, setSecretInput] = (0, import_react9.useState)("");
3573
+ const [showSecret, setShowSecret] = (0, import_react9.useState)(false);
3574
+ const [showCode, setShowCode] = (0, import_react9.useState)(false);
3575
+ const [showQr, setShowQr] = (0, import_react9.useState)(false);
3576
+ const [selectedTab, setSelectedTab] = (0, import_react9.useState)("lan");
3577
+ const [timeLeft, setTimeLeft] = (0, import_react9.useState)(300);
3578
+ const [toast, setToast] = (0, import_react9.useState)(null);
3579
+ const [maxVisitsInput, setMaxVisitsInput] = (0, import_react9.useState)("60");
3580
+ const [maxFailedInput, setMaxFailedInput] = (0, import_react9.useState)("5");
3581
+ const [lockDurationMinsInput, setLockDurationMinsInput] = (0, import_react9.useState)("15");
3582
+ const [isSaving, setIsSaving] = (0, import_react9.useState)(false);
3583
+ const [showVersionTip, setShowVersionTip] = (0, import_react9.useState)(false);
3584
+ const [currentLang, setCurrentLang] = (0, import_react9.useState)(resolveLocale(ctx, status));
3585
+ const [npmLatestVersion, setNpmLatestVersion] = (0, import_react9.useState)("");
3586
+ const [hasUpdate, setHasUpdate] = (0, import_react9.useState)(false);
3587
+ const [conflictBannerDismissed, setConflictBannerDismissed] = (0, import_react9.useState)(false);
3588
+ const qrTabTouchedRef = (0, import_react9.useRef)(false);
3589
+ const toastTimerRef = (0, import_react9.useRef)(null);
3590
+ const showToast = (0, import_react9.useCallback)((message, type = "success", duration = 2500) => {
3591
+ if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
3592
+ setToast({ message, type });
3593
+ toastTimerRef.current = setTimeout(() => setToast(null), duration);
3594
+ }, []);
3595
+ const updateQrTab = (0, import_react9.useCallback)((tab) => {
3596
+ qrTabTouchedRef.current = true;
3597
+ setSelectedTab(tab);
3598
+ }, []);
3599
+ const [activeTab, setActiveTab] = (0, import_react9.useState)(() => {
3600
+ try {
3601
+ const saved = localStorage.getItem("dsh-rm-settings-tab");
3602
+ if (saved === "access" || saved === "devices" || saved === "style" || saved === "storage") return saved;
3603
+ } catch (e) {
3604
+ }
3605
+ return "access";
3606
+ });
3607
+ const switchTab = (tab) => {
3608
+ setActiveTab(tab);
3609
+ try {
3610
+ localStorage.setItem("dsh-rm-settings-tab", tab);
3611
+ } catch (e) {
3612
+ }
3613
+ };
3614
+ const refreshStatusOnly = (0, import_react9.useCallback)(() => {
3615
+ fetchStatus().then((data) => {
3616
+ if (data.tailscaleIp && !qrTabTouchedRef.current) {
3617
+ setSelectedTab("tailscale");
3618
+ }
3619
+ if (data.maxVisitsPerMinute) setMaxVisitsInput(String(data.maxVisitsPerMinute));
3620
+ if (data.maxFailedAttempts) setMaxFailedInput(String(data.maxFailedAttempts));
3621
+ if (data.lockDurationMs)
3622
+ setLockDurationMinsInput(String(Math.round(data.lockDurationMs / 6e4)));
3623
+ if (data.locale) {
3624
+ setCurrentLang(resolveLocale(ctx, data));
3625
+ }
3626
+ setStatus((prev) => ({
3627
+ ...prev,
3628
+ tailscaleIp: data.tailscaleIp || "",
3629
+ lanIp: data.lanIp || "",
3630
+ hasSecret: data.hasSecret,
3631
+ allowTailscale: data.allowTailscale,
3632
+ allowLan: data.allowLan,
3633
+ maxVisitsPerMinute: data.maxVisitsPerMinute || 60,
3634
+ maxFailedAttempts: data.maxFailedAttempts || 5,
3635
+ lockDurationMs: data.lockDurationMs || 9e5,
3636
+ devicesCount: data.devicesCount || 0,
3637
+ devices: data.devices || [],
3638
+ ipSecurityStats: data.ipSecurityStats || [],
3639
+ persistPath: data.persistPath || "",
3640
+ rsaKeyPath: data.rsaKeyPath || "",
3641
+ styleSnippetsPath: data.styleSnippetsPath || "",
3642
+ locale: data.locale || "zh",
3643
+ pairingBridgeMode: data.pairingBridgeMode || "pending",
3644
+ pairingBridgeConflict: data.pairingBridgeConflict || "",
3645
+ pairingBridgeConflictId: data.pairingBridgeConflictId || "",
3646
+ loading: false
3647
+ }));
3648
+ }).catch(() => {
3649
+ });
3650
+ }, [ctx]);
3651
+ const refreshStatusAndCode = (0, import_react9.useCallback)(() => {
3652
+ refreshStatusOnly();
3653
+ generatePairCode().then((data) => {
3654
+ if (data.success) {
3655
+ setStatus((prev) => ({
3656
+ ...prev,
3657
+ code: data.code,
3658
+ expiresAt: data.expiresAt
3659
+ }));
3660
+ const remain = Math.max(0, Math.floor((data.expiresAt - Date.now()) / 1e3));
3661
+ setTimeLeft(remain);
3662
+ }
3663
+ }).catch(() => {
3664
+ });
3665
+ }, [refreshStatusOnly]);
3666
+ (0, import_react9.useEffect)(() => {
3667
+ let cancelled = false;
3668
+ fetch("https://registry.npmjs.org/dsh-remote-mobile/latest", { cache: "no-store" }).then((res) => res.ok ? res.json() : Promise.reject(new Error("npm registry error"))).then((data) => {
3669
+ if (cancelled) return;
3670
+ const v = data && data.version ? String(data.version) : "";
3671
+ if (!v) return;
3672
+ setNpmLatestVersion(v);
3673
+ const installed = String(PLUGIN_VERSION).replace(/^v/i, "");
3674
+ setHasUpdate(compareVersions(v, installed) > 0);
3675
+ }).catch(() => {
3676
+ if (cancelled) return;
3677
+ setHasUpdate(false);
3678
+ });
3679
+ return () => {
3680
+ cancelled = true;
3681
+ };
3682
+ }, []);
3683
+ const compareVersions = (a, b) => {
3684
+ const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
3685
+ const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
3686
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
3687
+ const da = pa[i] || 0;
3688
+ const db = pb[i] || 0;
3689
+ if (da > db) return 1;
3690
+ if (da < db) return -1;
3691
+ }
3692
+ return 0;
3693
+ };
3694
+ (0, import_react9.useEffect)(() => {
3695
+ refreshStatusAndCode();
3696
+ const localeUnsubs = [];
3697
+ try {
3698
+ const loc = ctx && (typeof ctx.get === "function" ? ctx.get("locale") : ctx.locale);
3699
+ if (loc && typeof loc.subscribe === "function") {
3700
+ const unsub = loc.subscribe(() => {
3701
+ setCurrentLang(resolveLocale(ctx, status));
3702
+ });
3703
+ if (typeof unsub === "function") localeUnsubs.push(unsub);
3704
+ }
3705
+ } catch (e) {
3706
+ }
3707
+ const handleDeviceUpdate = (e) => {
3708
+ if (!e || !e.detail) return;
3709
+ const detail = e.detail;
3710
+ if (detail.stats && Array.isArray(detail.stats)) {
3711
+ setStatus((prev) => ({ ...prev, ipSecurityStats: detail.stats }));
3712
+ }
3713
+ if (detail.device) {
3714
+ const dev = detail.device;
3715
+ setStatus((prev) => {
3716
+ const prevList = prev.devices || [];
3717
+ const nextList = [dev].concat(prevList.filter((d) => d.token !== dev.token));
3718
+ return {
3719
+ ...prev,
3720
+ devicesCount: nextList.length,
3721
+ devices: nextList
3722
+ };
3723
+ });
3724
+ const msg = detail.type === "device-connected" ? t("toastDeviceConnected", currentLang, {
3725
+ name: dev.deviceName || "Device",
3726
+ ip: dev.ip
3727
+ }) : t("toastDeviceOnline", currentLang, {
3728
+ name: dev.deviceName || "Device",
3729
+ ip: dev.ip
3730
+ });
3731
+ showToast(msg, "info", 4e3);
3732
+ }
3733
+ if (detail.type === "device-revoked") {
3734
+ setStatus((prev) => {
3735
+ const prevList = prev.devices || [];
3736
+ const nextList = detail.all ? [] : prevList.filter((d) => d.token !== detail.token);
3737
+ return {
3738
+ ...prev,
3739
+ devicesCount: nextList.length,
3740
+ devices: nextList
3741
+ };
3742
+ });
3743
+ }
3744
+ if (detail.type === "ip-security-alert") {
3745
+ const alertMsg = t("toastIpLocked", currentLang, { ip: detail.ip });
3746
+ showToast(alertMsg, "danger", 5e3);
3747
+ }
3748
+ refreshStatusOnly();
3749
+ };
3750
+ window.addEventListener("dsh-device-updated", handleDeviceUpdate);
3751
+ const pollTimer = setInterval(() => {
3752
+ refreshStatusOnly();
3753
+ }, 3e3);
3754
+ return () => {
3755
+ clearInterval(pollTimer);
3756
+ window.removeEventListener("dsh-device-updated", handleDeviceUpdate);
3757
+ for (let i = 0; i < localeUnsubs.length; i++) {
3758
+ localeUnsubs[i]();
3759
+ }
3760
+ };
3761
+ }, [refreshStatusAndCode, refreshStatusOnly, currentLang, ctx]);
3762
+ (0, import_react9.useEffect)(() => {
3763
+ if (timeLeft <= 0) return;
3764
+ const timer = setInterval(() => {
3765
+ setTimeLeft((prev) => {
3766
+ if (prev <= 1) {
3767
+ clearInterval(timer);
3768
+ return 0;
3769
+ }
3770
+ return prev - 1;
3771
+ });
3772
+ }, 1e3);
3773
+ return () => clearInterval(timer);
3774
+ }, [timeLeft]);
3775
+ const toggleTailscale = () => {
3776
+ const nextVal = !status.allowTailscale;
3777
+ updateBypassConfig(nextVal, status.allowLan).then(() => {
3778
+ setStatus((prev) => ({ ...prev, allowTailscale: nextVal }));
3779
+ }).catch((err) => {
3780
+ showToast((currentLang === "en" ? "Operation failed: " : "\u64CD\u4F5C\u5931\u8D25\uFF1A") + (err?.message || err), "danger", 3500);
3781
+ });
3782
+ };
3783
+ const toggleLan = () => {
3784
+ const nextVal = !status.allowLan;
3785
+ if (nextVal) {
3786
+ const warnMsg = t("lanBypassConfirm", currentLang);
3787
+ if (!confirm(warnMsg)) return;
3788
+ }
3789
+ updateBypassConfig(status.allowTailscale, nextVal).then(() => {
3790
+ setStatus((prev) => ({ ...prev, allowLan: nextVal }));
3791
+ }).catch((err) => {
3792
+ showToast((currentLang === "en" ? "Operation failed: " : "\u64CD\u4F5C\u5931\u8D25\uFF1A") + (err?.message || err), "danger", 3500);
3793
+ });
3794
+ };
3795
+ const saveSecret = () => {
3796
+ const trimmed = (secretInput || "").trim();
3797
+ if (!trimmed) {
3798
+ alert(t("secretInputPlaceholder", currentLang));
3799
+ return;
3800
+ }
3801
+ const hasLetter = /[a-zA-Z]/.test(trimmed);
3802
+ const hasNumber = /[0-9]/.test(trimmed);
3803
+ if (trimmed.length < 6 || !hasLetter || !hasNumber) {
3804
+ alert(t("secretWeakTip", currentLang));
3805
+ return;
3806
+ }
3807
+ updateSecret(trimmed).then((data) => {
3808
+ if (data.success) {
3809
+ setStatus((prev) => ({ ...prev, hasSecret: true }));
3810
+ setSecretInput("");
3811
+ showToast(
3812
+ currentLang === "en" ? "Persistent password saved!" : "\u957F\u671F\u8BBF\u95EE\u5BC6\u7801\u5DF2\u6210\u529F\u4FDD\u5B58\u5E76\u542F\u7528\uFF01",
3813
+ "success",
3814
+ 3e3
3815
+ );
3816
+ } else {
3817
+ showToast((currentLang === "en" ? "Save failed: " : "\u4FDD\u5B58\u5931\u8D25\uFF1A") + (data.reason || ""), "danger", 3500);
3818
+ }
3819
+ }).catch((err) => {
3820
+ showToast((currentLang === "en" ? "Save failed: " : "\u4FDD\u5B58\u5931\u8D25\uFF1A") + (err?.message || err), "danger", 3500);
3821
+ });
3822
+ };
3823
+ const clearSecret = () => {
3824
+ if (confirm(t("clearSecretConfirm", currentLang))) {
3825
+ clearSecretApi().then(() => {
3826
+ setStatus((prev) => ({ ...prev, hasSecret: false }));
3827
+ setSecretInput("");
3828
+ showToast(
3829
+ currentLang === "en" ? "Password cleared!" : "\u5DF2\u6210\u529F\u6E05\u9664\u957F\u671F\u8BBF\u95EE\u5BC6\u7801\uFF01",
3830
+ "info",
3831
+ 3e3
3832
+ );
3833
+ }).catch((err) => {
3834
+ showToast((currentLang === "en" ? "Clear failed: " : "\u6E05\u9664\u5931\u8D25\uFF1A") + (err?.message || err), "danger", 3500);
3835
+ });
3836
+ }
3837
+ };
3838
+ const revokeDevice = (token) => {
3839
+ if (confirm(t("revokeDeviceConfirm", currentLang))) {
3840
+ revokeDeviceApi(token).then(() => {
3841
+ refreshStatusAndCode();
3842
+ showToast(
3843
+ currentLang === "en" ? "Device disconnected!" : "\u5DF2\u6210\u529F\u8E22\u51FA\u8BE5\u8BBE\u5907\uFF01",
3844
+ "info",
3845
+ 2500
3846
+ );
3847
+ }).catch((err) => {
3848
+ showToast((currentLang === "en" ? "Revoke failed: " : "\u65AD\u5F00\u5931\u8D25\uFF1A") + (err?.message || err), "danger", 3500);
3849
+ });
3850
+ }
3851
+ };
3852
+ const revokeAll = () => {
3853
+ if (confirm(t("revokeAllConfirm", currentLang))) {
3854
+ revokeAllDevicesApi().then(() => {
3855
+ refreshStatusAndCode();
3856
+ showToast(
3857
+ currentLang === "en" ? "All devices disconnected!" : "\u5DF2\u6210\u529F\u6CE8\u9500\u5E76\u8E22\u4E0B\u7EBF\u6240\u6709\u8BBE\u5907\uFF01",
3858
+ "info",
3859
+ 2500
3860
+ );
3861
+ }).catch((err) => {
3862
+ showToast((currentLang === "en" ? "Revoke failed: " : "\u65AD\u5F00\u5931\u8D25\uFF1A") + (err?.message || err), "danger", 3500);
3863
+ });
3864
+ }
3865
+ };
3866
+ const unlockIp = (ip) => {
3867
+ if (confirm(t("unlockIpConfirm", currentLang, { ip }))) {
3868
+ unlockIpApi(ip).then(() => {
3869
+ refreshStatusAndCode();
3870
+ showToast(
3871
+ currentLang === "en" ? `IP ${ip} unlocked!` : `\u5DF2\u6210\u529F\u4E3A IP ${ip} \u89E3\u9664\u9501\u5B9A\uFF01`,
3872
+ "success",
3873
+ 3e3
3874
+ );
3875
+ }).catch((err) => {
3876
+ showToast((currentLang === "en" ? "Unlock failed: " : "\u89E3\u9501\u5931\u8D25\uFF1A") + (err?.message || err), "danger", 3500);
3877
+ });
3878
+ }
3879
+ };
3880
+ const clearIpStats = () => {
3881
+ if (confirm(t("clearSecurityLogConfirm", currentLang))) {
3882
+ clearIpStatsApi().then(() => {
3883
+ refreshStatusAndCode();
3884
+ showToast(
3885
+ currentLang === "en" ? "Audit logs cleared!" : "\u5DF2\u6210\u529F\u6E05\u7A7A\u6240\u6709\u5B89\u5168\u5BA1\u8BA1\u65E5\u5FD7\uFF01",
3886
+ "success",
3887
+ 2500
3888
+ );
3889
+ }).catch((err) => {
3890
+ showToast((currentLang === "en" ? "Clear failed: " : "\u6E05\u7A7A\u5931\u8D25\uFF1A") + (err?.message || err), "danger", 3500);
3891
+ });
3892
+ }
3893
+ };
3894
+ const saveAdvancedConfig = () => {
3895
+ const visits = parseInt(maxVisitsInput, 10);
3896
+ const failed = parseInt(maxFailedInput, 10);
3897
+ const mins = parseInt(lockDurationMinsInput, 10);
3898
+ if (isNaN(visits) || visits <= 0 || isNaN(failed) || failed <= 0 || isNaN(mins) || mins <= 0) {
3899
+ alert(t("saveConfigFailTip", currentLang));
3900
+ return;
3901
+ }
3902
+ if (!confirm(t("saveConfigConfirm", currentLang))) {
3903
+ return;
3904
+ }
3905
+ setIsSaving(true);
3906
+ updateAdvancedSecurityOptions({
3907
+ maxVisitsPerMinute: visits,
3908
+ maxFailedAttempts: failed,
3909
+ lockDurationMs: mins * 60 * 1e3
3910
+ }).then((data) => {
3911
+ setIsSaving(false);
3912
+ if (data.success) {
3913
+ refreshStatusAndCode();
3914
+ showToast(t("saveConfigSuccessToast", currentLang), "success", 3e3);
3915
+ } else {
3916
+ showToast((currentLang === "en" ? "Save failed: " : "\u4FDD\u5B58\u5931\u8D25\uFF1A") + (data.reason || ""), "danger", 3500);
3917
+ }
3918
+ }).catch((err) => {
3919
+ setIsSaving(false);
3920
+ showToast((currentLang === "en" ? "Save failed: " : "\u4FDD\u5B58\u5931\u8D25\uFF1A") + (err?.message || err), "danger", 3500);
3921
+ });
3922
+ };
3923
+ const resetAdvancedConfigDefaults = () => {
3924
+ if (!confirm(t("restoreDefaultsConfirm", currentLang))) {
3925
+ return;
3926
+ }
3927
+ setMaxVisitsInput("60");
3928
+ setMaxFailedInput("5");
3929
+ setLockDurationMinsInput("15");
3930
+ setIsSaving(true);
3931
+ updateAdvancedSecurityOptions({
3932
+ maxVisitsPerMinute: 60,
3933
+ maxFailedAttempts: 5,
3934
+ lockDurationMs: 15 * 60 * 1e3
3935
+ }).then((data) => {
3936
+ setIsSaving(false);
3937
+ if (data.success) {
3938
+ refreshStatusAndCode();
3939
+ showToast(t("restoreDefaultsSuccessToast", currentLang), "success", 3e3);
3940
+ } else {
3941
+ showToast((currentLang === "en" ? "Reset failed: " : "\u91CD\u7F6E\u5931\u8D25\uFF1A") + (data.reason || ""), "danger", 3500);
3942
+ }
3943
+ }).catch((err) => {
3944
+ setIsSaving(false);
3945
+ showToast((currentLang === "en" ? "Reset failed: " : "\u91CD\u7F6E\u5931\u8D25\uFF1A") + (err?.message || err), "danger", 3500);
3946
+ });
3947
+ };
3948
+ const copyDirectLink = () => {
3949
+ if (timeLeft <= 0) {
3950
+ alert(t("codeExpired", currentLang));
3951
+ return;
3952
+ }
3953
+ const effectiveQrIp = selectedTab === "tailscale" && status.tailscaleIp ? status.tailscaleIp : status.lanIp || "127.0.0.1";
3954
+ const port = typeof window !== "undefined" && window.location.port || "3080";
3955
+ const directLink = `http://${effectiveQrIp}:${port}/auth?token=${status.code || ""}`;
3956
+ copyText(directLink, t("copiedTip", currentLang));
3957
+ setStatus((prev) => ({ ...prev, copied: true }));
3958
+ setTimeout(() => {
3959
+ setStatus((prev) => ({ ...prev, copied: false }));
3960
+ }, 2e3);
3961
+ };
3962
+ const copyText = (text, successTip) => {
3963
+ const triggerToast = () => {
3964
+ showToast(
3965
+ successTip || (currentLang === "en" ? "Copied to clipboard!" : "\u5DF2\u6210\u529F\u590D\u5236\u5230\u526A\u8D34\u677F\uFF01"),
3966
+ "success",
3967
+ 2500
3968
+ );
3969
+ };
3970
+ if (typeof navigator !== "undefined" && navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
3971
+ navigator.clipboard.writeText(text).then(triggerToast).catch(() => {
3972
+ try {
3973
+ const ta = document.createElement("textarea");
3974
+ ta.value = text;
3975
+ ta.style.position = "fixed";
3976
+ ta.style.opacity = "0";
3977
+ document.body.appendChild(ta);
3978
+ ta.select();
3979
+ document.execCommand("copy");
3980
+ document.body.removeChild(ta);
3981
+ triggerToast();
3982
+ } catch (e) {
3983
+ }
3984
+ });
3985
+ } else {
3986
+ try {
3987
+ const ta = document.createElement("textarea");
3988
+ ta.value = text;
3989
+ ta.style.position = "fixed";
3990
+ ta.style.opacity = "0";
3991
+ document.body.appendChild(ta);
3992
+ ta.select();
3993
+ document.execCommand("copy");
3994
+ document.body.removeChild(ta);
3995
+ triggerToast();
3996
+ } catch (e) {
3997
+ }
3998
+ }
3999
+ };
4000
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
4001
+ "div",
4002
+ {
4003
+ style: {
4004
+ padding: "2px 4px",
4005
+ color: "var(--dsw-alias-label-primary, inherit)",
4006
+ display: "flex",
4007
+ flexDirection: "column",
4008
+ gap: "14px",
4009
+ position: "relative"
4010
+ },
4011
+ children: [
4012
+ toast ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
4013
+ "div",
4014
+ {
4015
+ style: {
4016
+ position: "fixed",
4017
+ top: "24px",
4018
+ left: "50%",
4019
+ transform: "translateX(-50%)",
4020
+ zIndex: 100010,
4021
+ background: toast.type === "danger" ? "linear-gradient(135deg, rgba(239, 68, 68, 0.95), rgba(185, 28, 28, 0.95))" : toast.type === "success" ? "linear-gradient(135deg, rgba(16, 185, 129, 0.95), rgba(5, 150, 105, 0.95))" : "linear-gradient(135deg, rgba(59, 130, 246, 0.95), rgba(37, 99, 235, 0.95))",
4022
+ color: "#ffffff",
4023
+ borderRadius: "10px",
4024
+ padding: "10px 18px",
4025
+ fontSize: "13px",
4026
+ fontWeight: "600",
4027
+ boxShadow: "0 12px 30px rgba(0, 0, 0, 0.35)",
4028
+ backdropFilter: "blur(10px)",
4029
+ WebkitBackdropFilter: "blur(10px)",
4030
+ border: "1px solid rgba(255, 255, 255, 0.25)",
4031
+ display: "flex",
4032
+ alignItems: "center",
4033
+ gap: "8px",
4034
+ pointerEvents: "none"
4035
+ },
4036
+ children: [
4037
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: toast.type === "danger" ? "\u{1F6A8}" : toast.type === "success" ? "\u2705" : "\u2139\uFE0F" }),
4038
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: toast.message })
4039
+ ]
4040
+ }
4041
+ ) : null,
4042
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
4043
+ "div",
4044
+ {
4045
+ style: {
4046
+ display: "flex",
4047
+ alignItems: "flex-start",
4048
+ justifyContent: "space-between",
4049
+ flexWrap: "wrap",
4050
+ gap: "12px",
4051
+ marginBottom: "4px"
4052
+ },
4053
+ children: [
4054
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { minWidth: "220px", flex: 1 }, children: [
4055
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
4056
+ "div",
4057
+ {
4058
+ style: {
4059
+ display: "flex",
4060
+ alignItems: "center",
4061
+ gap: "8px",
4062
+ flexWrap: "wrap",
4063
+ margin: "0 0 6px 0"
4064
+ },
4065
+ children: [
4066
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4067
+ "h3",
4068
+ {
4069
+ style: {
4070
+ fontSize: "20px",
4071
+ fontWeight: "700",
4072
+ margin: 0,
4073
+ color: "var(--dsw-alias-label-primary, inherit)"
4074
+ },
4075
+ children: t("title", currentLang)
4076
+ }
4077
+ ),
4078
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
4079
+ "div",
4080
+ {
4081
+ style: { position: "relative", display: "inline-flex", alignItems: "center" },
4082
+ onMouseEnter: () => setShowVersionTip(true),
4083
+ onMouseLeave: () => setShowVersionTip(false),
4084
+ onClick: () => {
4085
+ showToast(
4086
+ `\u{1F4E6} dsh-remote-mobile ${PLUGIN_VERSION} (${currentLang === "en" ? "Build: " : "\u6784\u5EFA\u65F6\u95F4: "}${BUILD_TIME})`,
4087
+ "info",
4088
+ 3e3
4089
+ );
4090
+ },
4091
+ children: [
4092
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4093
+ "span",
4094
+ {
4095
+ style: {
4096
+ fontSize: "11px",
4097
+ padding: "2px 7px",
4098
+ borderRadius: "10px",
4099
+ background: "rgba(59,130,246,0.15)",
4100
+ color: "var(--dsw-alias-brand-primary, #3b82f6)",
4101
+ fontWeight: "600",
4102
+ cursor: "pointer",
4103
+ userSelect: "none",
4104
+ transition: "all 0.2s ease"
4105
+ },
4106
+ children: PLUGIN_VERSION
4107
+ }
4108
+ ),
4109
+ showVersionTip ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4110
+ "div",
4111
+ {
4112
+ style: {
4113
+ position: "absolute",
4114
+ top: "calc(100% + 6px)",
4115
+ left: "50%",
4116
+ transform: "translateX(-50%)",
4117
+ backgroundColor: "#0f172a",
4118
+ color: "#f8fafc",
4119
+ padding: "5px 10px",
4120
+ borderRadius: "6px",
4121
+ fontSize: "11px",
4122
+ fontWeight: "500",
4123
+ whiteSpace: "nowrap",
4124
+ boxShadow: "0 8px 16px rgba(0,0,0,0.3)",
4125
+ border: "1px solid rgba(255,255,255,0.15)",
4126
+ zIndex: 9999,
4127
+ pointerEvents: "none",
4128
+ lineHeight: "1.4",
4129
+ textAlign: "center"
4130
+ },
4131
+ children: `${currentLang === "en" ? "Build: " : "\u6253\u5305\u65F6\u95F4: "}${BUILD_TIME}`
4132
+ }
4133
+ ) : null
4134
+ ]
4135
+ }
4136
+ ),
4137
+ hasUpdate && npmLatestVersion ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
4138
+ "a",
4139
+ {
4140
+ href: `https://www.npmjs.com/package/dsh-remote-mobile/v/${npmLatestVersion}`,
4141
+ target: "_blank",
4142
+ rel: "noopener noreferrer",
4143
+ style: {
4144
+ display: "inline-flex",
4145
+ alignItems: "center",
4146
+ gap: "4px",
4147
+ padding: "2px 8px",
4148
+ borderRadius: "10px",
4149
+ background: "rgba(203, 36, 49, 0.12)",
4150
+ border: "1px solid rgba(203, 36, 49, 0.3)",
4151
+ color: "var(--dsw-alias-brand-primary, #f87171)",
4152
+ fontSize: "11px",
4153
+ fontWeight: "600",
4154
+ textDecoration: "none",
4155
+ cursor: "pointer",
4156
+ whiteSpace: "nowrap"
4157
+ },
4158
+ children: [
4159
+ "\u{1F680} ",
4160
+ currentLang === "en" ? `Update available: v${npmLatestVersion}` : `\u53D1\u73B0\u65B0\u7248\u672C v${npmLatestVersion}`
4161
+ ]
4162
+ }
4163
+ ) : null
4164
+ ]
4165
+ }
4166
+ ),
4167
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4168
+ "div",
4169
+ {
4170
+ style: {
4171
+ fontSize: "13px",
4172
+ color: "var(--dsw-alias-label-tertiary, #888)"
4173
+ },
4174
+ children: t("subtitle", currentLang)
4175
+ }
4176
+ )
4177
+ ] }),
4178
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
4179
+ "a",
4180
+ {
4181
+ href: "https://github.com/IceApriler/dsh-remote-mobile",
4182
+ target: "_blank",
4183
+ rel: "noopener noreferrer",
4184
+ style: {
4185
+ display: "inline-flex",
4186
+ alignItems: "center",
4187
+ gap: "6px",
4188
+ padding: "6px 12px",
4189
+ background: "var(--dsw-alias-bg-layer-3, rgba(128,128,128,0.1))",
4190
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.2))",
4191
+ borderRadius: "8px",
4192
+ color: "var(--dsw-alias-label-secondary, inherit)",
4193
+ fontSize: "12px",
4194
+ fontWeight: "500",
4195
+ textDecoration: "none",
4196
+ transition: "all 0.2s ease",
4197
+ cursor: "pointer"
4198
+ },
4199
+ children: [
4200
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("svg", { style: { width: "16px", height: "16px", fill: "currentColor" }, viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" }) }),
4201
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: "dsh-remote-mobile" })
4202
+ ]
4203
+ }
4204
+ )
4205
+ ]
4206
+ }
4207
+ ),
4208
+ status.pairingBridgeConflict && !conflictBannerDismissed ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
4209
+ "div",
4210
+ {
4211
+ style: {
4212
+ display: "flex",
4213
+ alignItems: "flex-start",
4214
+ gap: "10px",
4215
+ padding: "12px 14px",
4216
+ borderRadius: "10px",
4217
+ border: "1px solid rgba(245, 158, 11, 0.45)",
4218
+ background: "rgba(245, 158, 11, 0.12)"
4219
+ },
4220
+ children: [
4221
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: { fontSize: "16px", lineHeight: "20px" }, children: "\u26A0\uFE0F" }),
4222
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { flex: 1, minWidth: 0 }, children: [
4223
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4224
+ "div",
4225
+ {
4226
+ style: {
4227
+ fontSize: "13px",
4228
+ fontWeight: "700",
4229
+ marginBottom: "4px",
4230
+ color: "var(--dsw-alias-label-primary, inherit)"
4231
+ },
4232
+ children: t("conflictBannerTitle", currentLang)
4233
+ }
4234
+ ),
4235
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4236
+ "div",
4237
+ {
4238
+ style: {
4239
+ fontSize: "12px",
4240
+ lineHeight: 1.6,
4241
+ whiteSpace: "pre-line",
4242
+ color: "var(--dsw-alias-label-secondary, inherit)"
4243
+ },
4244
+ children: t("conflictBannerBody", currentLang, {
4245
+ plugin: status.pairingBridgeConflict || t("conflictUnknownPlugin", currentLang)
4246
+ })
4247
+ }
4248
+ ),
4249
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4250
+ "button",
4251
+ {
4252
+ type: "button",
4253
+ onClick: () => {
4254
+ const plugin = status.pairingBridgeConflict || t("conflictUnknownPlugin", currentLang);
4255
+ const entryId = status.pairingBridgeConflictId || "";
4256
+ const report = t(
4257
+ entryId ? "conflictReportText" : "conflictReportNoEntryIdText",
4258
+ currentLang,
4259
+ { plugin, entryId }
4260
+ );
4261
+ copyText(report, t("conflictCopiedTip", currentLang));
4262
+ },
4263
+ style: {
4264
+ marginTop: "8px",
4265
+ display: "inline-flex",
4266
+ alignItems: "center",
4267
+ gap: "6px",
4268
+ padding: "5px 10px",
4269
+ fontSize: "12px",
4270
+ fontWeight: "600",
4271
+ borderRadius: "7px",
4272
+ cursor: "pointer",
4273
+ border: "1px solid rgba(245, 158, 11, 0.55)",
4274
+ background: "rgba(245, 158, 11, 0.18)",
4275
+ color: "var(--dsw-alias-label-primary, inherit)"
4276
+ },
4277
+ children: t("conflictCopyBtn", currentLang)
4278
+ }
4279
+ )
4280
+ ] }),
4281
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4282
+ "button",
4283
+ {
4284
+ type: "button",
4285
+ onClick: () => setConflictBannerDismissed(true),
4286
+ title: t("conflictBannerDismiss", currentLang),
4287
+ style: {
4288
+ flexShrink: 0,
4289
+ width: "24px",
4290
+ height: "24px",
4291
+ display: "inline-flex",
4292
+ alignItems: "center",
4293
+ justifyContent: "center",
4294
+ border: "none",
4295
+ borderRadius: "6px",
4296
+ cursor: "pointer",
4297
+ fontSize: "12px",
4298
+ background: "rgba(128, 128, 128, 0.15)",
4299
+ color: "var(--dsw-alias-label-secondary, inherit)"
4300
+ },
4301
+ children: "\u2715"
4302
+ }
4303
+ )
4304
+ ]
4305
+ }
4306
+ ) : null,
4307
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4308
+ "div",
4309
+ {
4310
+ style: {
4311
+ display: "flex",
4312
+ gap: "14px",
4313
+ borderBottom: "1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.2))",
4314
+ paddingBottom: "0",
4315
+ marginBottom: "6px",
4316
+ overflowX: "auto",
4317
+ overflowY: "hidden",
4318
+ WebkitOverflowScrolling: "touch",
4319
+ scrollbarWidth: "none"
4320
+ },
4321
+ children: [
4322
+ { id: "access", label: t("tabAccess", currentLang) },
4323
+ { id: "devices", label: t("tabDevices", currentLang) },
4324
+ { id: "style", label: t("tabStyle", currentLang) },
4325
+ { id: "storage", label: t("tabStorage", currentLang) }
4326
+ ].map((item) => {
4327
+ const isActive = activeTab === item.id;
4328
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4329
+ "button",
4330
+ {
4331
+ type: "button",
4332
+ onClick: () => switchTab(item.id),
4333
+ style: {
4334
+ background: "transparent",
4335
+ border: "none",
4336
+ outline: "none",
4337
+ padding: "6px 2px 8px 2px",
4338
+ fontSize: "13px",
4339
+ fontWeight: isActive ? "600" : "400",
4340
+ cursor: "pointer",
4341
+ whiteSpace: "nowrap",
4342
+ flexShrink: 0,
4343
+ color: isActive ? "var(--dsw-alias-label-primary, #111827)" : "var(--dsw-alias-label-tertiary, #8c8c8c)",
4344
+ borderBottom: isActive ? "2px solid var(--dsw-alias-label-primary, #111827)" : "2px solid transparent",
4345
+ marginBottom: "-1px",
4346
+ transition: "all 0.2s ease"
4347
+ },
4348
+ children: item.label
4349
+ },
4350
+ item.id
4351
+ );
4352
+ })
4353
+ }
4354
+ ),
4355
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { display: activeTab === "access" ? "flex" : "none", flexDirection: "column", gap: "16px" }, children: [
4356
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4357
+ NetworkCard,
4358
+ {
4359
+ status,
4360
+ toggleTailscale,
4361
+ toggleLan,
4362
+ copyText,
4363
+ lang: currentLang
4364
+ }
4365
+ ),
4366
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4367
+ QrPairingCard,
4368
+ {
4369
+ status,
4370
+ showCode,
4371
+ setShowCode,
4372
+ timeLeft,
4373
+ showQr,
4374
+ setShowQr,
4375
+ selectedTab,
4376
+ setSelectedTab: updateQrTab,
4377
+ generateQrSvg,
4378
+ refreshStatusAndCode,
4379
+ copyDirectLink,
4380
+ lang: currentLang
4381
+ }
4382
+ ),
4383
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4384
+ SecretCard,
4385
+ {
4386
+ status,
4387
+ secretInput,
4388
+ setSecretInput,
4389
+ showSecret,
4390
+ setShowSecret,
4391
+ saveSecret,
4392
+ clearSecret,
4393
+ lang: currentLang
4394
+ }
4395
+ )
4396
+ ] }),
4397
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { display: activeTab === "devices" ? "flex" : "none", flexDirection: "column", gap: "16px" }, children: [
4398
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4399
+ DeviceListCard,
4400
+ {
4401
+ status,
4402
+ revokeDevice,
4403
+ revokeAll,
4404
+ lang: currentLang
4405
+ }
4406
+ ),
4407
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4408
+ SecurityCard,
4409
+ {
4410
+ status,
4411
+ unlockIp,
4412
+ clearIpStats,
4413
+ lang: currentLang
4414
+ }
4415
+ ),
4416
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
4417
+ ConfigCard,
4418
+ {
4419
+ maxVisitsInput,
4420
+ setMaxVisitsInput,
4421
+ maxFailedInput,
4422
+ setMaxFailedInput,
4423
+ lockDurationMinsInput,
4424
+ setLockDurationMinsInput,
4425
+ saveAdvancedConfig,
4426
+ resetAdvancedConfigDefaults,
4427
+ isSaving,
4428
+ lang: currentLang,
4429
+ copyText
4430
+ }
4431
+ )
4432
+ ] }),
4433
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { display: activeTab === "style" ? "flex" : "none", flexDirection: "column", gap: "16px" }, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(StylesCard, { lang: currentLang, showToast }) }),
4434
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { display: activeTab === "storage" ? "flex" : "none", flexDirection: "column", gap: "16px" }, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(StorageCard, { status, lang: currentLang, copyText }) })
4435
+ ]
4436
+ }
4437
+ );
4438
+ }
4439
+
4440
+ // src/client/utils/toast.js
4441
+ function showGlobalDeviceToast(device, eventType) {
4442
+ if (typeof document === "undefined") return;
4443
+ var toastId = "dsh-global-device-toast";
4444
+ var existing = document.getElementById(toastId);
4445
+ if (existing) existing.remove();
4446
+ var isNewPair = eventType === "device-connected";
4447
+ var bgGradient = isNewPair ? "linear-gradient(135deg, rgba(16, 185, 129, 0.95), rgba(5, 150, 105, 0.95))" : "linear-gradient(135deg, rgba(59, 130, 246, 0.95), rgba(37, 99, 235, 0.95))";
4448
+ var glowShadow = isNewPair ? "rgba(16, 185, 129, 0.3)" : "rgba(59, 130, 246, 0.3)";
4449
+ var icon = isNewPair ? "\u{1F389}" : "\u{1F4F6}";
4450
+ var title = isNewPair ? "\u65B0\u8BBE\u5907\u5DF2\u6210\u529F\u914D\u5BF9\u63A5\u5165\uFF01" : "\u5DF2\u6388\u6743\u8BBE\u5907\u5DF2\u91CD\u65B0\u4E0A\u7EBF\uFF01";
4451
+ var toast = document.createElement("div");
4452
+ toast.id = toastId;
4453
+ toast.style.cssText = [
4454
+ "position: fixed",
4455
+ "top: 24px",
4456
+ "left: 50%",
4457
+ "transform: translateX(-50%)",
4458
+ "z-index: 9999999",
4459
+ "background: " + bgGradient,
4460
+ "color: white",
4461
+ "border-radius: 14px",
4462
+ "padding: 14px 20px",
4463
+ "box-shadow: 0 16px 36px rgba(0, 0, 0, 0.35), 0 0 24px " + glowShadow,
4464
+ "display: flex",
4465
+ "align-items: center",
4466
+ "gap: 14px",
4467
+ "min-width: 340px",
4468
+ "max-width: 90vw",
4469
+ "backdrop-filter: blur(12px)",
4470
+ "-webkit-backdrop-filter: blur(12px)",
4471
+ "border: 1px solid rgba(255, 255, 255, 0.25)",
4472
+ 'font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
4473
+ "animation: dshToastIn 0.35s cubic-bezier(0.16, 1, 0.3, 1)",
4474
+ "cursor: pointer"
4475
+ ].join(";");
4476
+ if (!document.getElementById("dsh-toast-anim-style")) {
4477
+ var style = document.createElement("style");
4478
+ style.id = "dsh-toast-anim-style";
4479
+ style.textContent = "@keyframes dshToastIn { from { opacity: 0; transform: translate(-50%, -20px) scale(0.95); } to { opacity: 1; transform: translate(-50%, 0) scale(1); } } @keyframes dshToastOut { from { opacity: 1; transform: translate(-50%, 0); } to { opacity: 0; transform: translate(-50%, -20px); } }";
4480
+ document.head.appendChild(style);
4481
+ }
4482
+ toast.innerHTML = [
4483
+ '<div style="font-size: 26px; line-height: 1; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));">' + icon + "</div>",
4484
+ '<div style="flex: 1; min-width: 0;">',
4485
+ ' <div style="font-size: 14px; font-weight: 700; margin-bottom: 2px; letter-spacing: 0.2px;">' + title + "</div>",
4486
+ ' <div style="font-size: 12px; opacity: 0.92; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">' + (device.deviceName || "\u79FB\u52A8\u7AEF\u8BBE\u5907") + " \xB7 IP: " + (device.ip || "\u672A\u77E5") + "</div>",
4487
+ "</div>",
4488
+ '<button id="dsh-toast-close" style="background: transparent; border: none; color: white; opacity: 0.8; font-size: 16px; cursor: pointer; padding: 4px 6px; line-height: 1; border-radius: 4px;">\u2715</button>'
4489
+ ].join("");
4490
+ document.body.appendChild(toast);
4491
+ var timer = setTimeout(function() {
4492
+ if (toast && toast.parentElement) {
4493
+ toast.style.animation = "dshToastOut 0.3s forwards";
4494
+ setTimeout(function() {
4495
+ if (toast.parentElement) toast.remove();
4496
+ }, 300);
4497
+ }
4498
+ }, 6e3);
4499
+ var closeBtn = toast.querySelector("#dsh-toast-close");
4500
+ if (closeBtn) {
4501
+ closeBtn.onclick = function(e) {
4502
+ e.stopPropagation();
4503
+ clearTimeout(timer);
4504
+ toast.remove();
4505
+ };
4506
+ }
4507
+ }
4508
+ function showGlobalSecurityToast(alertData) {
4509
+ if (typeof document === "undefined") return;
4510
+ var toastId = "dsh-global-security-toast";
4511
+ var existing = document.getElementById(toastId);
4512
+ if (existing) existing.remove();
4513
+ var toast = document.createElement("div");
4514
+ toast.id = toastId;
4515
+ toast.style.cssText = [
4516
+ "position: fixed",
4517
+ "top: 24px",
4518
+ "left: 50%",
4519
+ "transform: translateX(-50%)",
4520
+ "z-index: 9999999",
4521
+ "background: linear-gradient(135deg, rgba(239, 68, 68, 0.95), rgba(185, 28, 28, 0.95))",
4522
+ "color: white",
4523
+ "border-radius: 14px",
4524
+ "padding: 14px 20px",
4525
+ "box-shadow: 0 16px 36px rgba(0, 0, 0, 0.4), 0 0 24px rgba(239, 68, 68, 0.4)",
4526
+ "display: flex",
4527
+ "align-items: center",
4528
+ "gap: 14px",
4529
+ "min-width: 360px",
4530
+ "max-width: 90vw",
4531
+ "backdrop-filter: blur(12px)",
4532
+ "-webkit-backdrop-filter: blur(12px)",
4533
+ "border: 1px solid rgba(255, 255, 255, 0.3)",
4534
+ 'font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
4535
+ "animation: dshToastIn 0.35s cubic-bezier(0.16, 1, 0.3, 1)",
4536
+ "cursor: pointer"
4537
+ ].join(";");
4538
+ toast.innerHTML = [
4539
+ '<div style="font-size: 26px; line-height: 1; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));">\u{1F6A8}</div>',
4540
+ '<div style="flex: 1; min-width: 0;">',
4541
+ ' <div style="font-size: 14px; font-weight: 700; margin-bottom: 2px; letter-spacing: 0.2px;">\u5B89\u5168\u62E6\u622A\u9884\u8B66</div>',
4542
+ ' <div style="font-size: 12px; opacity: 0.95; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">IP ' + (alertData.ip || "\u672A\u77E5") + " " + (alertData.reason || "\u5DF2\u89E6\u53D1\u5B89\u5168\u7194\u65AD\u9501\u5B9A\uFF01") + "</div>",
4543
+ "</div>",
4544
+ '<button id="dsh-sec-toast-close" style="background: transparent; border: none; color: white; opacity: 0.8; font-size: 16px; cursor: pointer; padding: 4px 6px; line-height: 1; border-radius: 4px;">\u2715</button>'
4545
+ ].join("");
4546
+ document.body.appendChild(toast);
4547
+ var timer = setTimeout(function() {
4548
+ if (toast && toast.parentElement) {
4549
+ toast.style.animation = "dshToastOut 0.3s forwards";
4550
+ setTimeout(function() {
4551
+ if (toast.parentElement) toast.remove();
4552
+ }, 300);
4553
+ }
4554
+ }, 7e3);
4555
+ var closeBtn = toast.querySelector("#dsh-sec-toast-close");
4556
+ if (closeBtn) {
4557
+ closeBtn.onclick = function(e) {
4558
+ e.stopPropagation();
4559
+ clearTimeout(timer);
4560
+ toast.remove();
4561
+ };
4562
+ }
4563
+ }
4564
+
4565
+ // src/client/index.jsx
4566
+ var inject = ["slots", "locale", "connection", "settingsScope", "remote"];
4567
+ function apply(ctx) {
4568
+ if (typeof window !== "undefined") {
4569
+ window.__DSH_CLIENT_CTX__ = ctx;
4570
+ var meta = document.querySelector('meta[name="viewport"]');
4571
+ if (!meta) {
4572
+ meta = document.createElement("meta");
4573
+ meta.name = "viewport";
4574
+ document.head.appendChild(meta);
4575
+ }
4576
+ meta.content = "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover";
4577
+ if (!window.__DSH_REMOTE_MOBILE_SSE_LISTENER__) {
4578
+ window.__DSH_REMOTE_MOBILE_SSE_LISTENER__ = true;
4579
+ var initSse = function() {
4580
+ try {
4581
+ var eventSource = new EventSource(RM_API_BASE + "events");
4582
+ eventSource.onmessage = function(event) {
4583
+ try {
4584
+ var data = JSON.parse(event.data || "{}");
4585
+ if (data.type === "device-connected" && data.device) {
4586
+ showGlobalDeviceToast(data.device, "device-connected");
4587
+ window.dispatchEvent(new CustomEvent("dsh-device-updated", { detail: data }));
4588
+ } else if (data.type === "device-online" && data.device) {
4589
+ showGlobalDeviceToast(data.device, "device-online");
4590
+ window.dispatchEvent(new CustomEvent("dsh-device-updated", { detail: data }));
4591
+ } else if (data.type === "device-revoked") {
4592
+ window.dispatchEvent(new CustomEvent("dsh-device-updated", { detail: data }));
4593
+ } else if (data.type === "ip-security-alert") {
4594
+ showGlobalSecurityToast(data);
4595
+ window.dispatchEvent(new CustomEvent("dsh-device-updated", { detail: data }));
4596
+ } else if (data.type === "ip-security-updated") {
4597
+ window.dispatchEvent(new CustomEvent("dsh-device-updated", { detail: data }));
4598
+ }
4599
+ } catch (e) {
4600
+ }
4601
+ };
4602
+ eventSource.onerror = function() {
4603
+ try {
4604
+ eventSource.close();
4605
+ } catch (e) {
4606
+ }
4607
+ setTimeout(initSse, 3e3);
4608
+ };
4609
+ } catch (e) {
4610
+ setTimeout(initSse, 5e3);
4611
+ }
4612
+ };
4613
+ initSse();
4614
+ }
4615
+ var slots = null;
4616
+ try {
4617
+ if (ctx && typeof ctx.get === "function") {
4618
+ slots = ctx.get("slots");
4619
+ } else if (ctx && ctx.slots) {
4620
+ slots = ctx.slots;
4621
+ }
4622
+ } catch (e) {
4623
+ }
4624
+ if (slots && typeof slots.inject === "function") {
4625
+ try {
4626
+ slots.inject("settings.section", function() {
4627
+ var unregister = slots.register({
4628
+ name: "settings.section",
4629
+ id: "tailscale-mobile",
4630
+ order: 150,
4631
+ label: function() {
4632
+ var lang = resolveLocale(ctx);
4633
+ return lang === "en" ? "Remote & Mobile" : "\u8FDC\u7A0B\u4E0E\u79FB\u52A8\u7AEF";
4634
+ }
4635
+ }, TailscaleMobileSection);
4636
+ return function() {
4637
+ if (typeof unregister === "function") unregister();
4638
+ };
4639
+ });
4640
+ } catch (e) {
4641
+ }
4642
+ }
4643
+ }
4644
+ }
4645
+ var index_default = apply;
4646
+
4647
+
4648
+ return module.exports;
4649
+ }
4650
+ });