dsh-remote-mobile 1.1.0

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,3606 @@
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_react9 = __toESM(require("react"), 1);
51
+
52
+ // src/client/components/MainSection.jsx
53
+ var import_react8 = __toESM(require("react"), 1);
54
+
55
+ // src/client/api.js
56
+ function fetchStatus() {
57
+ return fetch("/api/remote-mobile/status?_t=" + Date.now()).then(function(res) {
58
+ return res.json();
59
+ });
60
+ }
61
+ function generatePairCode() {
62
+ return fetch("/api/remote-mobile/generate-code", { method: "POST" }).then(function(res) {
63
+ return res.json();
64
+ });
65
+ }
66
+ function updateBypassConfig(allowTailscale, allowLan) {
67
+ return fetch("/api/remote-mobile/update-options", {
68
+ method: "POST",
69
+ headers: { "Content-Type": "application/json" },
70
+ body: JSON.stringify({ allowTailscale, allowLan })
71
+ }).then(function(res) {
72
+ return res.json();
73
+ });
74
+ }
75
+ function updateAdvancedSecurityOptions(payload) {
76
+ return fetch("/api/remote-mobile/update-options", {
77
+ method: "POST",
78
+ headers: { "Content-Type": "application/json" },
79
+ body: JSON.stringify(payload)
80
+ }).then(function(res) {
81
+ return res.json();
82
+ });
83
+ }
84
+ function updateSecret(secret) {
85
+ return fetch("/api/remote-mobile/set-secret", {
86
+ method: "POST",
87
+ headers: { "Content-Type": "application/json" },
88
+ body: JSON.stringify({ secret })
89
+ }).then(function(res) {
90
+ return res.json();
91
+ });
92
+ }
93
+ function clearSecretApi() {
94
+ return fetch("/api/remote-mobile/clear-secret", {
95
+ method: "POST",
96
+ headers: { "Content-Type": "application/json" }
97
+ }).then(function(res) {
98
+ return res.json();
99
+ });
100
+ }
101
+ function revokeDeviceApi(token) {
102
+ return fetch("/api/remote-mobile/revoke-device", {
103
+ method: "POST",
104
+ headers: { "Content-Type": "application/json" },
105
+ body: JSON.stringify({ token })
106
+ }).then(function(res) {
107
+ return res.json();
108
+ });
109
+ }
110
+ function revokeAllDevicesApi() {
111
+ return fetch("/api/remote-mobile/revoke-all", {
112
+ method: "POST",
113
+ headers: { "Content-Type": "application/json" }
114
+ }).then(function(res) {
115
+ return res.json();
116
+ });
117
+ }
118
+ function unlockIpApi(ip) {
119
+ return fetch("/api/remote-mobile/unlock-ip", {
120
+ method: "POST",
121
+ headers: { "Content-Type": "application/json" },
122
+ body: JSON.stringify({ ip })
123
+ }).then(function(res) {
124
+ return res.json();
125
+ });
126
+ }
127
+ function clearIpStatsApi() {
128
+ return fetch("/api/remote-mobile/clear-ip-stats", {
129
+ method: "POST",
130
+ headers: { "Content-Type": "application/json" }
131
+ }).then(function(res) {
132
+ return res.json();
133
+ });
134
+ }
135
+
136
+ // src/client/qrcode.js
137
+ var generateQrSvg = (function() {
138
+ var PAD0 = 236, PAD1 = 17;
139
+ function QRCodeModel(typeNumber, errorCorrectLevel) {
140
+ this.typeNumber = typeNumber;
141
+ this.errorCorrectLevel = errorCorrectLevel;
142
+ this.modules = null;
143
+ this.moduleCount = 0;
144
+ this.dataCache = null;
145
+ this.dataList = [];
146
+ }
147
+ QRCodeModel.prototype = {
148
+ addData: function(data) {
149
+ this.dataList.push(new QR8bitByte(data));
150
+ this.dataCache = null;
151
+ },
152
+ isDark: function(row, col) {
153
+ if (row < 0 || this.moduleCount <= row || col < 0 || this.moduleCount <= col) throw new Error(row + "," + col);
154
+ return this.modules[row][col];
155
+ },
156
+ getModuleCount: function() {
157
+ return this.moduleCount;
158
+ },
159
+ make: function() {
160
+ if (this.typeNumber < 1) {
161
+ var typeNumber = 1;
162
+ for (typeNumber = 1; typeNumber < 40; typeNumber++) {
163
+ var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, this.errorCorrectLevel);
164
+ var buffer = new QRBitBuffer();
165
+ var totalDataCount = 0;
166
+ for (var i2 = 0; i2 < rsBlocks.length; i2++) totalDataCount += rsBlocks[i2].dataCount;
167
+ for (var i2 = 0; i2 < this.dataList.length; i2++) {
168
+ var data = this.dataList[i2];
169
+ buffer.put(data.mode, 4);
170
+ buffer.put(data.getLength(), QRUtil.getLengthInBits(data.mode, typeNumber));
171
+ data.write(buffer);
172
+ }
173
+ if (buffer.getLengthInBits() <= totalDataCount * 8) break;
174
+ }
175
+ this.typeNumber = typeNumber;
176
+ }
177
+ this.makeImpl(false, this.getBestMaskPattern());
178
+ },
179
+ makeImpl: function(test, maskPattern) {
180
+ this.moduleCount = this.typeNumber * 4 + 17;
181
+ this.modules = new Array(this.moduleCount);
182
+ for (var row = 0; row < this.moduleCount; row++) {
183
+ this.modules[row] = new Array(this.moduleCount);
184
+ for (var col = 0; col < this.moduleCount; col++) this.modules[row][col] = null;
185
+ }
186
+ this.setupPositionProbePattern(0, 0);
187
+ this.setupPositionProbePattern(this.moduleCount - 7, 0);
188
+ this.setupPositionProbePattern(0, this.moduleCount - 7);
189
+ this.setupPositionAdjustPattern();
190
+ this.setupTimingPattern();
191
+ this.setupTypeInfo(test, maskPattern);
192
+ if (this.typeNumber >= 7) this.setupTypeNumber(test);
193
+ if (this.dataCache == null) this.dataCache = QRCodeModel.createData(this.typeNumber, this.errorCorrectLevel, this.dataList);
194
+ this.mapData(this.dataCache, maskPattern);
195
+ },
196
+ setupPositionProbePattern: function(row, col) {
197
+ for (var r = -1; r <= 7; r++) {
198
+ if (row + r <= -1 || this.moduleCount <= row + r) continue;
199
+ for (var c = -1; c <= 7; c++) {
200
+ if (col + c <= -1 || this.moduleCount <= col + c) continue;
201
+ 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) {
202
+ this.modules[row + r][col + c] = true;
203
+ } else {
204
+ this.modules[row + r][col + c] = false;
205
+ }
206
+ }
207
+ }
208
+ },
209
+ getBestMaskPattern: function() {
210
+ var minLostPoint = 0, pattern = 0;
211
+ for (var i2 = 0; i2 < 8; i2++) {
212
+ this.makeImpl(true, i2);
213
+ var lostPoint = QRUtil.getLostPoint(this);
214
+ if (i2 == 0 || minLostPoint > lostPoint) {
215
+ minLostPoint = lostPoint;
216
+ pattern = i2;
217
+ }
218
+ }
219
+ return pattern;
220
+ },
221
+ setupTimingPattern: function() {
222
+ for (var r = 8; r < this.moduleCount - 8; r++) {
223
+ if (this.modules[r][6] !== null) continue;
224
+ this.modules[r][6] = r % 2 == 0;
225
+ }
226
+ for (var c = 8; c < this.moduleCount - 8; c++) {
227
+ if (this.modules[6][c] !== null) continue;
228
+ this.modules[6][c] = c % 2 == 0;
229
+ }
230
+ },
231
+ setupPositionAdjustPattern: function() {
232
+ var pos = QRUtil.getPatternPosition(this.typeNumber);
233
+ for (var i2 = 0; i2 < pos.length; i2++) {
234
+ for (var j = 0; j < pos.length; j++) {
235
+ var row = pos[i2], col = pos[j];
236
+ if (this.modules[row][col] !== null) continue;
237
+ for (var r = -2; r <= 2; r++) {
238
+ for (var c = -2; c <= 2; c++) {
239
+ if (r == -2 || r == 2 || c == -2 || c == 2 || r == 0 && c == 0) this.modules[row + r][col + c] = true;
240
+ else this.modules[row + r][col + c] = false;
241
+ }
242
+ }
243
+ }
244
+ }
245
+ },
246
+ setupTypeNumber: function(test) {
247
+ var bits = QRUtil.getBCHTypeNumber(this.typeNumber);
248
+ for (var i2 = 0; i2 < 18; i2++) {
249
+ var mod = !test && (bits >> i2 & 1) == 1;
250
+ this.modules[Math.floor(i2 / 3)][i2 % 3 + this.moduleCount - 8 - 3] = mod;
251
+ this.modules[i2 % 3 + this.moduleCount - 8 - 3][Math.floor(i2 / 3)] = mod;
252
+ }
253
+ },
254
+ setupTypeInfo: function(test, maskPattern) {
255
+ var data = this.errorCorrectLevel << 3 | maskPattern;
256
+ var bits = QRUtil.getBCHTypeInfo(data);
257
+ for (var i2 = 0; i2 < 15; i2++) {
258
+ var mod = !test && (bits >> i2 & 1) == 1;
259
+ if (i2 < 6) this.modules[i2][8] = mod;
260
+ else if (i2 < 8) this.modules[i2 + 1][8] = mod;
261
+ else this.modules[this.moduleCount - 15 + i2][8] = mod;
262
+ if (i2 < 8) this.modules[8][this.moduleCount - i2 - 1] = mod;
263
+ else if (i2 < 9) this.modules[8][15 - i2 - 1 + 1] = mod;
264
+ else this.modules[8][15 - i2 - 1] = mod;
265
+ }
266
+ this.modules[this.moduleCount - 8][8] = !test;
267
+ },
268
+ mapData: function(data, maskPattern) {
269
+ var inc = -1, row = this.moduleCount - 1, bitIndex = 7, byteIndex = 0;
270
+ for (var col = this.moduleCount - 1; col > 0; col -= 2) {
271
+ if (col == 6) col--;
272
+ while (true) {
273
+ for (var c = 0; c < 2; c++) {
274
+ if (this.modules[row][col - c] === null) {
275
+ var dark = false;
276
+ if (byteIndex < data.length) dark = (data[byteIndex] >>> bitIndex & 1) == 1;
277
+ var mask = QRUtil.getMask(maskPattern, row, col - c);
278
+ if (mask) dark = !dark;
279
+ this.modules[row][col - c] = dark;
280
+ bitIndex--;
281
+ if (bitIndex == -1) {
282
+ byteIndex++;
283
+ bitIndex = 7;
284
+ }
285
+ }
286
+ }
287
+ row += inc;
288
+ if (row < 0 || this.moduleCount <= row) {
289
+ row -= inc;
290
+ inc = -inc;
291
+ break;
292
+ }
293
+ }
294
+ }
295
+ }
296
+ };
297
+ QRCodeModel.createData = function(typeNumber, errorCorrectLevel, dataList) {
298
+ var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel);
299
+ var buffer = new QRBitBuffer();
300
+ for (var i2 = 0; i2 < dataList.length; i2++) {
301
+ var data = dataList[i2];
302
+ buffer.put(data.mode, 4);
303
+ buffer.put(data.getLength(), QRUtil.getLengthInBits(data.mode, typeNumber));
304
+ data.write(buffer);
305
+ }
306
+ var totalDataCount = 0;
307
+ for (var i2 = 0; i2 < rsBlocks.length; i2++) totalDataCount += rsBlocks[i2].dataCount;
308
+ if (buffer.getLengthInBits() > totalDataCount * 8) throw new Error("code length overflow");
309
+ if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) buffer.put(0, 4);
310
+ while (buffer.getLengthInBits() % 8 != 0) buffer.putBit(false);
311
+ while (true) {
312
+ if (buffer.getLengthInBits() >= totalDataCount * 8) break;
313
+ buffer.put(PAD0, 8);
314
+ if (buffer.getLengthInBits() >= totalDataCount * 8) break;
315
+ buffer.put(PAD1, 8);
316
+ }
317
+ return QRCodeModel.createBytes(buffer, rsBlocks);
318
+ };
319
+ QRCodeModel.createBytes = function(buffer, rsBlocks) {
320
+ var offset = 0, maxDcCount = 0, maxEcCount = 0;
321
+ var dcdata = new Array(rsBlocks.length), ecdata = new Array(rsBlocks.length);
322
+ for (var r = 0; r < rsBlocks.length; r++) {
323
+ var dcCount = rsBlocks[r].dataCount, ecCount = rsBlocks[r].totalCount - dcCount;
324
+ maxDcCount = Math.max(maxDcCount, dcCount);
325
+ maxEcCount = Math.max(maxEcCount, ecCount);
326
+ dcdata[r] = new Array(dcCount);
327
+ for (var i2 = 0; i2 < dcdata[r].length; i2++) dcdata[r][i2] = 255 & buffer.buffer[i2 + offset];
328
+ offset += dcCount;
329
+ var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount);
330
+ var rawPoly = new QRPolynomial(dcdata[r], rsPoly.getLength() - 1);
331
+ var modPoly = rawPoly.mod(rsPoly);
332
+ ecdata[r] = new Array(rsPoly.getLength() - 1);
333
+ for (var i2 = 0; i2 < ecdata[r].length; i2++) {
334
+ var modIndex = i2 + modPoly.getLength() - ecdata[r].length;
335
+ ecdata[r][i2] = modIndex >= 0 ? modPoly.get(modIndex) : 0;
336
+ }
337
+ }
338
+ var totalCodeCount = 0;
339
+ for (var i2 = 0; i2 < rsBlocks.length; i2++) totalCodeCount += rsBlocks[i2].totalCount;
340
+ var data = new Array(totalCodeCount), index = 0;
341
+ for (var i2 = 0; i2 < maxDcCount; i2++) {
342
+ for (var r = 0; r < rsBlocks.length; r++) {
343
+ if (i2 < dcdata[r].length) data[index++] = dcdata[r][i2];
344
+ }
345
+ }
346
+ for (var i2 = 0; i2 < maxEcCount; i2++) {
347
+ for (var r = 0; r < rsBlocks.length; r++) {
348
+ if (i2 < ecdata[r].length) data[index++] = ecdata[r][i2];
349
+ }
350
+ }
351
+ return data;
352
+ };
353
+ function QR8bitByte(data) {
354
+ this.mode = 4;
355
+ this.data = data;
356
+ this.parsedData = [];
357
+ for (var i2 = 0, l = this.data.length; i2 < l; i2++) {
358
+ var byteArray = [], code = this.data.charCodeAt(i2);
359
+ if (code > 65536) {
360
+ byteArray[0] = 240 | (code & 1835008) >>> 18;
361
+ byteArray[1] = 128 | (code & 258048) >>> 12;
362
+ byteArray[2] = 128 | (code & 4032) >>> 6;
363
+ byteArray[3] = 128 | code & 63;
364
+ } else if (code > 2048) {
365
+ byteArray[0] = 224 | (code & 61440) >>> 12;
366
+ byteArray[1] = 128 | (code & 4032) >>> 6;
367
+ byteArray[2] = 128 | code & 63;
368
+ } else if (code > 127) {
369
+ byteArray[0] = 192 | (code & 1984) >>> 6;
370
+ byteArray[1] = 128 | code & 63;
371
+ } else {
372
+ byteArray[0] = code;
373
+ }
374
+ this.parsedData.push(byteArray);
375
+ }
376
+ this.parsedData = Array.prototype.concat.apply([], this.parsedData);
377
+ if (this.parsedData.length != this.data.length) this.parsedData.unshift(191, 187, 239);
378
+ }
379
+ QR8bitByte.prototype = {
380
+ getLength: function() {
381
+ return this.parsedData.length;
382
+ },
383
+ write: function(buffer) {
384
+ for (var i2 = 0, l = this.parsedData.length; i2 < l; i2++) buffer.put(this.parsedData[i2], 8);
385
+ }
386
+ };
387
+ var QRUtil = {
388
+ PATTERN_POSITION_TABLE: [
389
+ [],
390
+ [6, 18],
391
+ [6, 22],
392
+ [6, 26],
393
+ [6, 30],
394
+ [6, 34],
395
+ [6, 22, 38],
396
+ [6, 24, 42],
397
+ [6, 26, 46],
398
+ [6, 28, 50],
399
+ [6, 30, 54],
400
+ [6, 32, 58],
401
+ [6, 34, 62],
402
+ [6, 26, 46, 66],
403
+ [6, 26, 48, 70],
404
+ [6, 26, 50, 74],
405
+ [6, 30, 54, 78],
406
+ [6, 30, 56, 82],
407
+ [6, 30, 58, 86],
408
+ [6, 34, 62, 90],
409
+ [6, 28, 50, 72, 94],
410
+ [6, 26, 50, 74, 98],
411
+ [6, 30, 54, 78, 102],
412
+ [6, 28, 54, 80, 106],
413
+ [6, 32, 58, 84, 110],
414
+ [6, 30, 58, 86, 114],
415
+ [6, 34, 62, 90, 118],
416
+ [6, 26, 50, 74, 98, 122],
417
+ [6, 30, 54, 78, 102, 126],
418
+ [6, 26, 52, 78, 104, 130],
419
+ [6, 30, 56, 82, 108, 134],
420
+ [6, 34, 60, 86, 112, 138],
421
+ [6, 30, 58, 86, 114, 142],
422
+ [6, 34, 62, 90, 118, 146],
423
+ [6, 30, 54, 78, 102, 126, 150],
424
+ [6, 24, 50, 76, 102, 128, 154],
425
+ [6, 28, 54, 80, 106, 132, 158],
426
+ [6, 32, 58, 84, 110, 136, 162],
427
+ [6, 26, 54, 82, 110, 138, 166],
428
+ [6, 30, 58, 86, 114, 142, 170]
429
+ ],
430
+ G15: 1 << 10 | 1 << 8 | 1 << 5 | 1 << 4 | 1 << 2 | 1 << 1 | 1 << 0,
431
+ G18: 1 << 12 | 1 << 11 | 1 << 10 | 1 << 9 | 1 << 8 | 1 << 5 | 1 << 2 | 1 << 0,
432
+ G15_MASK: 1 << 14 | 1 << 12 | 1 << 10 | 1 << 4 | 1 << 1,
433
+ getBCHTypeInfo: function(data) {
434
+ var d = data << 10;
435
+ while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) >= 0) {
436
+ d ^= QRUtil.G15 << QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15);
437
+ }
438
+ return (data << 10 | d) ^ QRUtil.G15_MASK;
439
+ },
440
+ getBCHTypeNumber: function(data) {
441
+ var d = data << 12;
442
+ while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) >= 0) {
443
+ d ^= QRUtil.G18 << QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18);
444
+ }
445
+ return data << 12 | d;
446
+ },
447
+ getBCHDigit: function(data) {
448
+ var digit = 0;
449
+ while (data != 0) {
450
+ digit++;
451
+ data >>>= 1;
452
+ }
453
+ return digit;
454
+ },
455
+ getPatternPosition: function(typeNumber) {
456
+ return QRUtil.PATTERN_POSITION_TABLE[typeNumber - 1];
457
+ },
458
+ getMask: function(maskPattern, i2, j) {
459
+ switch (maskPattern) {
460
+ case 0:
461
+ return (i2 + j) % 2 == 0;
462
+ case 1:
463
+ return i2 % 2 == 0;
464
+ case 2:
465
+ return j % 3 == 0;
466
+ case 3:
467
+ return (i2 + j) % 3 == 0;
468
+ case 4:
469
+ return (Math.floor(i2 / 2) + Math.floor(j / 3)) % 2 == 0;
470
+ case 5:
471
+ return i2 * j % 2 + i2 * j % 3 == 0;
472
+ case 6:
473
+ return (i2 * j % 2 + i2 * j % 3) % 2 == 0;
474
+ case 7:
475
+ return (i2 * j % 3 + (i2 + j) % 2) % 2 == 0;
476
+ default:
477
+ throw new Error("bad maskPattern:" + maskPattern);
478
+ }
479
+ },
480
+ getErrorCorrectPolynomial: function(errorCorrectLength) {
481
+ var a = new QRPolynomial([1], 0);
482
+ for (var i2 = 0; i2 < errorCorrectLength; i2++) a = a.multiply(new QRPolynomial([1, QRMath.gexp(i2)], 0));
483
+ return a;
484
+ },
485
+ getLengthInBits: function(mode, type) {
486
+ if (1 <= type && type < 10) return 8;
487
+ else if (type < 27) return 16;
488
+ else if (type < 41) return 16;
489
+ else throw new Error("type:" + type);
490
+ },
491
+ getLostPoint: function(qrCode) {
492
+ var moduleCount = qrCode.getModuleCount(), lostPoint = 0;
493
+ for (var row = 0; row < moduleCount; row++) {
494
+ for (var col = 0; col < moduleCount; col++) {
495
+ var sameCount = 0, dark = qrCode.isDark(row, col);
496
+ for (var r = -1; r <= 1; r++) {
497
+ if (row + r < 0 || moduleCount <= row + r) continue;
498
+ for (var c = -1; c <= 1; c++) {
499
+ if (col + c < 0 || moduleCount <= col + c) continue;
500
+ if (r == 0 && c == 0) continue;
501
+ if (dark == qrCode.isDark(row + r, col + c)) sameCount++;
502
+ }
503
+ }
504
+ if (sameCount > 5) lostPoint += 3 + sameCount - 5;
505
+ }
506
+ }
507
+ return lostPoint;
508
+ }
509
+ };
510
+ var QRMath = {
511
+ glog: function(n) {
512
+ if (n < 1) throw new Error("glog(" + n + ")");
513
+ return QRMath.LOG_TABLE[n];
514
+ },
515
+ gexp: function(n) {
516
+ while (n < 0) n += 255;
517
+ while (n >= 256) n -= 255;
518
+ return QRMath.EXP_TABLE[n];
519
+ },
520
+ EXP_TABLE: new Array(256),
521
+ LOG_TABLE: new Array(256)
522
+ };
523
+ for (var i = 0; i < 8; i++) QRMath.EXP_TABLE[i] = 1 << i;
524
+ 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];
525
+ for (var i = 0; i < 255; i++) QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]] = i;
526
+ function QRPolynomial(num, shift) {
527
+ if (num.length == void 0) throw new Error(num.length + "/" + shift);
528
+ var offset = 0;
529
+ while (offset < num.length && num[offset] == 0) offset++;
530
+ this.num = new Array(num.length - offset + shift);
531
+ for (var i2 = 0; i2 < num.length - offset; i2++) this.num[i2] = num[i2 + offset];
532
+ }
533
+ QRPolynomial.prototype = {
534
+ get: function(index) {
535
+ return this.num[index];
536
+ },
537
+ getLength: function() {
538
+ return this.num.length;
539
+ },
540
+ multiply: function(e) {
541
+ var num = new Array(this.getLength() + e.getLength() - 1);
542
+ for (var i2 = 0; i2 < this.getLength(); i2++) {
543
+ for (var j = 0; j < e.getLength(); j++) num[i2 + j] ^= QRMath.gexp(QRMath.glog(this.get(i2)) + QRMath.glog(e.get(j)));
544
+ }
545
+ return new QRPolynomial(num, 0);
546
+ },
547
+ mod: function(e) {
548
+ if (this.getLength() - e.getLength() < 0) return this;
549
+ var ratio = QRMath.glog(this.get(0)) - QRMath.glog(e.get(0));
550
+ var num = new Array(this.getLength());
551
+ for (var i2 = 0; i2 < this.getLength(); i2++) num[i2] = this.get(i2);
552
+ for (var i2 = 0; i2 < e.getLength(); i2++) num[i2] ^= QRMath.gexp(QRMath.glog(e.get(i2)) + ratio);
553
+ return new QRPolynomial(num, 0).mod(e);
554
+ }
555
+ };
556
+ function QRRSBlock(totalCount, dataCount) {
557
+ this.totalCount = totalCount;
558
+ this.dataCount = dataCount;
559
+ }
560
+ QRRSBlock.RS_BLOCK_TABLE = [
561
+ [1, 26, 19],
562
+ [1, 26, 16],
563
+ [1, 26, 13],
564
+ [1, 26, 9],
565
+ [1, 44, 34],
566
+ [1, 44, 28],
567
+ [1, 44, 22],
568
+ [1, 44, 16],
569
+ [1, 70, 55],
570
+ [1, 70, 44],
571
+ [2, 35, 17],
572
+ [2, 35, 13],
573
+ [1, 100, 80],
574
+ [2, 50, 32],
575
+ [2, 50, 24],
576
+ [4, 25, 9]
577
+ ];
578
+ QRRSBlock.getRSBlocks = function(typeNumber, errorCorrectLevel) {
579
+ var rsBlock = QRRSBlock.getRsBlockTable(typeNumber, errorCorrectLevel);
580
+ if (rsBlock == void 0) throw new Error("bad rs block @ typeNumber:" + typeNumber + "/errorCorrectLevel:" + errorCorrectLevel);
581
+ var length = rsBlock.length / 3, list = [];
582
+ for (var i2 = 0; i2 < length; i2++) {
583
+ var count = rsBlock[i2 * 3 + 0], totalCount = rsBlock[i2 * 3 + 1], dataCount = rsBlock[i2 * 3 + 2];
584
+ for (var j = 0; j < count; j++) list.push(new QRRSBlock(totalCount, dataCount));
585
+ }
586
+ return list;
587
+ };
588
+ QRRSBlock.getRsBlockTable = function(typeNumber, errorCorrectLevel) {
589
+ switch (errorCorrectLevel) {
590
+ case 1:
591
+ return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0];
592
+ case 0:
593
+ return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1];
594
+ case 3:
595
+ return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2];
596
+ case 2:
597
+ return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3];
598
+ default:
599
+ return void 0;
600
+ }
601
+ };
602
+ function QRBitBuffer() {
603
+ this.buffer = [];
604
+ this.length = 0;
605
+ }
606
+ QRBitBuffer.prototype = {
607
+ get: function(index) {
608
+ return (this.buffer[Math.floor(index / 8)] >>> 7 - index % 8 & 1) == 1;
609
+ },
610
+ put: function(num, length) {
611
+ for (var i2 = 0; i2 < length; i2++) this.putBit((num >>> length - i2 - 1 & 1) == 1);
612
+ },
613
+ getLengthInBits: function() {
614
+ return this.length;
615
+ },
616
+ putBit: function(bit) {
617
+ var bufIndex = Math.floor(this.length / 8);
618
+ if (this.buffer.length <= bufIndex) this.buffer.push(0);
619
+ if (bit) this.buffer[bufIndex] |= 128 >>> this.length % 8;
620
+ this.length++;
621
+ }
622
+ };
623
+ return function(text, size) {
624
+ size = size || 180;
625
+ var qr = new QRCodeModel(0, 1);
626
+ qr.addData(text);
627
+ qr.make();
628
+ var count = qr.getModuleCount();
629
+ var margin = 2;
630
+ var total = count + margin * 2;
631
+ var cellSize = size / total;
632
+ 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;">';
633
+ svg += '<rect width="100%" height="100%" fill="#ffffff"/>';
634
+ svg += '<g fill="#0f172a">';
635
+ for (var r = 0; r < count; r++) {
636
+ for (var c = 0; c < count; c++) {
637
+ if (qr.isDark(r, c)) {
638
+ var x = ((c + margin) * cellSize).toFixed(2);
639
+ var y = ((r + margin) * cellSize).toFixed(2);
640
+ var w = (cellSize + 0.3).toFixed(2);
641
+ svg += '<rect x="' + x + '" y="' + y + '" width="' + w + '" height="' + w + '"/>';
642
+ }
643
+ }
644
+ }
645
+ svg += "</g></svg>";
646
+ return svg;
647
+ };
648
+ })();
649
+
650
+ // src/client/components/NetworkCard.jsx
651
+ var import_react = __toESM(require("react"), 1);
652
+
653
+ // src/client/i18n.js
654
+ var translations = {
655
+ zh: {
656
+ // 主面板
657
+ title: "\u8FDC\u7A0B\u4E0E\u79FB\u52A8\u7AEF",
658
+ 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",
659
+ // 卡片 1: 网络与直连
660
+ netCardTitle: "\u7F51\u7EDC\u63A5\u5165\u5730\u5740\u4E0E\u514D\u5BC6\u76F4\u8FDE\u7BA1\u7406",
661
+ 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",
662
+ tailscaleSectionTitle: "Tailscale \u865A\u62DF\u79C1\u7F51\u63A5\u5165 (\u63A8\u8350)",
663
+ tailscaleBadge: "Tailscale IP",
664
+ tailscaleNotConnected: "\u672A\u68C0\u6D4B\u5230 Tailscale \u865A\u62DF\u79C1\u7F51 IP",
665
+ 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",
666
+ lanSectionTitle: "\u672C\u5730\u5C40\u57DF\u7F51\u63A5\u5165 (LAN / Wi-Fi)",
667
+ lanBadge: "\u5C40\u57DF\u7F51 IP",
668
+ lanNotConnected: "\u672A\u68C0\u6D4B\u5230\u53EF\u7528\u7684\u672C\u5730\u5C40\u57DF\u7F51 IP",
669
+ copyUrlBtn: "\u{1F4CB} \u590D\u5236\u94FE\u63A5",
670
+ copiedTip: "\u5DF2\u6210\u529F\u590D\u5236\u94FE\u63A5\u5230\u526A\u8D34\u677F\uFF01",
671
+ directBypassToggle: "\u514D\u5BC6\u76F4\u8FDE\u6A21\u5F0F (\u76F4\u8FBE\u9875\u9762)",
672
+ directBypassOn: "\u5DF2\u5F00\u542F\u514D\u5BC6\u76F4\u8FDE",
673
+ directBypassOff: "\u5DF2\u5173\u95ED\u514D\u5BC6\u76F4\u8FDE",
674
+ 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",
675
+ lanBypassWarningTitle: "\u{1F6A8} \u6781\u9AD8\u98CE\u9669\u8B66\u793A\uFF1A\u5C40\u57DF\u7F51\u514D\u5BC6\u63A7\u5236\u98CE\u9669",
676
+ 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",
677
+ 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",
678
+ // 卡片 2: 配对码与二维码
679
+ pairingCardTitle: "\u624B\u673A\u5FEB\u901F\u914D\u5BF9\u4E0E\u626B\u7801\u63A5\u5165",
680
+ 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",
681
+ shortCodeTitle: "6 \u4F4D\u77ED\u671F\u52A8\u6001\u914D\u5BF9\u7801",
682
+ codeValidUntil: "\u6709\u6548\u671F\u5269\u4F59\uFF1A",
683
+ codeExpired: "\u914D\u5BF9\u7801\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u70B9\u51FB\u5237\u65B0\u91CD\u65B0\u751F\u6210",
684
+ clickToReveal: "\u70B9\u51FB\u67E5\u770B 6 \u4F4D\u914D\u5BF9\u7801",
685
+ clickToHide: "\u70B9\u51FB\u9690\u85CF",
686
+ refreshCodeBtn: "\u{1F504} \u5237\u65B0\u914D\u5BF9\u7801",
687
+ qrTitle: "\u626B\u7801\u514D\u8F93\u5BC6\u7801\u76F4\u8FDE",
688
+ toggleQrShow: "\u663E\u793A\u914D\u5BF9\u4E8C\u7EF4\u7801",
689
+ toggleQrHide: "\u6536\u8D77\u4E8C\u7EF4\u7801",
690
+ switchTailscaleTab: "Tailscale \u626B\u7801",
691
+ switchLanTab: "\u5C40\u57DF\u7F51 (Wi-Fi) \u626B\u7801",
692
+ copyDirectAuthLink: "\u{1F4CB} \u590D\u5236\u5E26\u914D\u5BF9\u7801\u76F4\u94FE",
693
+ // 卡片 3: 长期密码
694
+ secretCardTitle: "\u8BBE\u7F6E\u957F\u671F\u8BBF\u95EE\u5BC6\u7801 (\u53EF\u9009)",
695
+ 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",
696
+ secretConfigured: "\u2705 \u5F53\u524D\u5DF2\u914D\u7F6E\u957F\u671F\u8BBF\u95EE\u5BC6\u7801 (\u5DF2\u5B89\u5168\u52A0\u76D0\u52A0\u5BC6\u843D\u76D8\u5B58\u50A8)",
697
+ secretNotConfigured: "\u26AA \u5F53\u524D\u672A\u914D\u7F6E\u957F\u671F\u8BBF\u95EE\u5BC6\u7801 (\u4EC5\u80FD\u901A\u8FC7\u4E34\u65F6\u914D\u5BF9\u7801\u6388\u6743)",
698
+ secretInputPlaceholder: "\u8BF7\u8F93\u5165 6 \u4F4D\u4EE5\u4E0A\u5B57\u6BCD\u4E0E\u6570\u5B57\u7EC4\u5408\u5BC6\u7801",
699
+ secretInputLabel: "\u957F\u671F\u5BC6\u7801\uFF1A",
700
+ showPassword: "\u{1F441}\uFE0F \u663E\u793A\u5BC6\u7801",
701
+ hidePassword: "\u{1F648} \u9690\u85CF\u5BC6\u7801",
702
+ saveSecretBtn: "\u{1F4BE} \u4FDD\u5B58\u5BC6\u7801",
703
+ clearSecretBtn: "\u{1F5D1}\uFE0F \u6E05\u9664\u5BC6\u7801",
704
+ secretWeakTip: "\u5BC6\u7801\u5FC5\u987B\u81F3\u5C11 6 \u4F4D\uFF0C\u4E14\u540C\u65F6\u5305\u542B\u5B57\u6BCD\u548C\u6570\u5B57",
705
+ 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",
706
+ // 卡片 4: 设备列表
707
+ deviceCardTitle: "\u5DF2\u6388\u6743\u63A5\u5165\u8BBE\u5907\u5217\u8868",
708
+ 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",
709
+ authDevicesCount: "\u53F0\u8BBE\u5907\u5DF2\u6388\u6743",
710
+ revokeAllBtn: "\u{1F5D1}\uFE0F \u4E00\u952E\u6CE8\u9500\u6240\u6709\u8BBE\u5907",
711
+ revokeAllConfirm: "\u786E\u5B9A\u8981\u64A4\u9500\u5E76\u8E22\u4E0B\u7EBF\u6240\u6709\u5DF2\u6388\u6743\u7684\u8BBE\u5907\u5417\uFF1F",
712
+ 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",
713
+ deviceOnlineNow: "\u5728\u7EBF",
714
+ deviceOffline: "\u79BB\u7EBF",
715
+ deviceBypassLabel: "\u514D\u5BC6\u76F4\u8FDE",
716
+ devicePasscodeLabel: "\u914D\u5BF9\u7801\u6388\u6743",
717
+ deviceSecretLabel: "\u957F\u671F\u5BC6\u7801",
718
+ deviceFirstSeen: "\u9996\u6B21\u6388\u6743\uFF1A",
719
+ deviceLastSeen: "\u6700\u8FD1\u6D3B\u8DC3\uFF1A",
720
+ revokeDeviceBtn: "\u8E22\u4E0B\u7EBF",
721
+ revokeDeviceConfirm: "\u786E\u5B9A\u8981\u64A4\u9500\u8FD9\u53F0\u8BBE\u5907\u7684\u8BBF\u95EE\u6743\u9650\u5417\uFF1F",
722
+ // 卡片 5: 防暴力破解与审计
723
+ securityCardTitle: "\u9632\u66B4\u529B\u7834\u89E3\u4E0E IP \u5B89\u5168\u5BA1\u8BA1",
724
+ 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",
725
+ 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",
726
+ unlockIpBtn: "\u{1F513} \u89E3\u9664\u9501\u5B9A",
727
+ unlockIpConfirm: "\u786E\u5B9A\u8981\u4E3A IP {ip} \u89E3\u9664\u5C01\u9501\u5417\uFF1F",
728
+ clearSecurityLogBtn: "\u{1F5D1}\uFE0F \u6E05\u7A7A\u5BA1\u8BA1\u65E5\u5FD7",
729
+ clearSecurityLogConfirm: "\u786E\u5B9A\u8981\u6E05\u7A7A\u6240\u6709 IP \u8BBF\u95EE\u4E0E\u5B89\u5168\u5BA1\u8BA1\u65E5\u5FD7\u5417\uFF1F",
730
+ thIp: "\u8BBF\u95EE\u6765\u6E90 IP",
731
+ thStatus: "\u5F53\u524D\u72B6\u6001",
732
+ thFailed: "\u5931\u8D25/\u603B\u6B21\u6570",
733
+ thLastSeen: "\u6700\u8FD1\u8BBF\u95EE",
734
+ thAction: "\u64CD\u4F5C",
735
+ statusLocked: "\u{1F6AB} \u5DF2\u88AB\u9501\u5B9A",
736
+ statusNormal: "\u{1F7E2} \u6B63\u5E38",
737
+ // 卡片 6: 本地存储说明
738
+ storageCardTitle: "\u672C\u5730\u6570\u636E\u4E0E\u5BC6\u94A5\u6301\u4E45\u5316\u5B58\u50A8\u8BF4\u660E",
739
+ 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",
740
+ storageDeviceFile: "\u5DF2\u6388\u6743\u8BBE\u5907\u4E0E\u4F1A\u8BDD\u7F13\u5B58\u6587\u4EF6\uFF1A",
741
+ storageRsaFile: "\u670D\u52A1\u7AEF RSA \u79C1\u94A5\u6587\u4EF6 (0o600 \u6743\u9650\u4FDD\u62A4)\uFF1A",
742
+ storageConfigFile: "\u4E3B\u914D\u7F6E\u6587\u4EF6 (\u547D\u540D\u7A7A\u95F4: dsh-remote-mobile)\uFF1A",
743
+ // 卡片 7: 全局高级配置
744
+ configCardTitle: "\u5168\u5C40\u9AD8\u7EA7\u5B89\u5168\u53C2\u6570\u914D\u7F6E",
745
+ 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\u81EA\u52A8\u540C\u6B65\u5199\u56DE ~/.dsh/settings.yaml \u914D\u7F6E\u6587\u4EF6\u3002",
746
+ copySettingsPathBtn: "\u{1F4CB} \u590D\u5236 settings.yaml \u8DEF\u5F84",
747
+ fieldVisitsLimit: "\u23F1\uFE0F \u6BCF\u5206\u949F\u8BBF\u95EE\u4E0A\u9650 (\u6B21/\u5206)",
748
+ fieldVisitsLimitHint: "\u5355 IP 1\u5206\u949F\u5185\u6253\u5F00\u767B\u5F55\u9875\u8D85\u8FC7\u6B64\u503C\u5C06\u89E6\u53D1\u9650\u9891",
749
+ fieldFailedLimit: "\u{1F6E1}\uFE0F \u8F93\u9519\u5BC6\u7801\u5C01\u7981\u9608\u503C (\u6B21)",
750
+ fieldFailedLimitHint: "\u8FDE\u7EED\u8F93\u9519\u914D\u5BF9\u7801/\u5BC6\u7801\u8FBE\u5230\u6B64\u503C\u5C06\u5C01\u9501\u8BE5 IP",
751
+ fieldLockDuration: "\u{1F6AB} \u89E6\u53D1\u5C01\u9501\u6301\u7EED\u65F6\u95F4 (\u5206\u949F)",
752
+ fieldLockDurationHint: "IP \u88AB\u9501\u5B9A\u540E\u7684\u51B7\u5374\u7B49\u5F85\u65F6\u957F (\u9ED8\u8BA4 15 \u5206\u949F)",
753
+ restoreDefaultsBtn: "\u{1F504} \u6062\u590D\u9ED8\u8BA4\u53C2\u6570",
754
+ saveConfigBtn: "\u{1F4BE} \u4FDD\u5B58\u53C2\u6570\u914D\u7F6E",
755
+ savingConfigBtn: "\u6B63\u5728\u4FDD\u5B58...",
756
+ 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\u81EA\u52A8\u540C\u6B65\u5199\u56DE ~/.dsh/settings.yaml\u3002",
757
+ 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",
758
+ saveConfigSuccessToast: "\u9AD8\u7EA7\u5B89\u5168\u53C2\u6570\u5DF2\u6210\u529F\u4FDD\u5B58\u5E76\u540C\u6B65\u5230 settings.yaml\uFF01",
759
+ restoreDefaultsSuccessToast: "\u5DF2\u6210\u529F\u6062\u590D\u9ED8\u8BA4\u5B89\u5168\u53C2\u6570\u5E76\u7ACB\u5373\u751F\u6548\uFF01",
760
+ saveConfigFailTip: "\u53C2\u6570\u5FC5\u987B\u4E3A\u5927\u4E8E 0 \u7684\u6709\u6548\u6574\u6570",
761
+ // Toast 提示
762
+ toastDeviceConnected: "\u{1F389} \u65B0\u8BBE\u5907\u63A5\u5165\u6210\u529F\uFF1A{name} ({ip})",
763
+ toastDeviceOnline: "\u{1F4F6} \u8BBE\u5907\u91CD\u65B0\u4E0A\u7EBF\uFF1A{name} ({ip})",
764
+ toastIpLocked: "\u{1F6AB} \u89E6\u53D1\u5B89\u5168\u8B66\u62A5\uFF1AIP {ip} \u8FDE\u7EED\u8BA4\u8BC1\u5931\u8D25\u5DF2\u88AB\u9501\u5B9A\uFF01"
765
+ },
766
+ en: {
767
+ // Main
768
+ title: "Remote & Mobile Access",
769
+ subtitle: "Enable access via Tailscale private network and local Wi-Fi, featuring QR code pairing and persistent passwords.",
770
+ // Card 1: Network & Bypass
771
+ netCardTitle: "Network Access & Bypass Management",
772
+ 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.",
773
+ tailscaleSectionTitle: "Tailscale Private Network (Recommended)",
774
+ tailscaleBadge: "Tailscale IP",
775
+ tailscaleNotConnected: "No Tailscale private IP detected",
776
+ tailscaleGuide: "Please ensure Tailscale is installed and logged in on this machine for secure access without port forwarding.",
777
+ lanSectionTitle: "Local Area Network (LAN / Wi-Fi)",
778
+ lanBadge: "LAN IP",
779
+ lanNotConnected: "No available local LAN IP detected",
780
+ copyUrlBtn: "\u{1F4CB} Copy Link",
781
+ copiedTip: "Link copied to clipboard!",
782
+ directBypassToggle: "Direct Bypass Mode (Passwordless)",
783
+ directBypassOn: "Bypass Enabled",
784
+ directBypassOff: "Bypass Disabled",
785
+ tailscaleBypassDesc: "When enabled, any device connecting via Tailscale can directly access the console without passwords or pairing codes.",
786
+ lanBypassWarningTitle: "\u{1F6A8} HIGH RISK SECURITY WARNING: LAN BYPASS",
787
+ 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!",
788
+ 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)!",
789
+ // Card 2: Pairing & QR
790
+ pairingCardTitle: "Quick Pairing & QR Code Access",
791
+ pairingCardDesc: "When both devices are on the same network, scan the QR code or enter the 6-digit dynamic pairing code to authenticate.",
792
+ shortCodeTitle: "6-Digit Dynamic Pairing Code",
793
+ codeValidUntil: "Expires in: ",
794
+ codeExpired: "Pairing code expired. Click refresh to generate a new one.",
795
+ clickToReveal: "Click to reveal 6-digit code",
796
+ clickToHide: "Click to hide",
797
+ refreshCodeBtn: "\u{1F504} Refresh Code",
798
+ qrTitle: "Scan QR Code for Instant Access",
799
+ toggleQrShow: "Show QR Code",
800
+ toggleQrHide: "Hide QR Code",
801
+ switchTailscaleTab: "Tailscale QR",
802
+ switchLanTab: "LAN (Wi-Fi) QR",
803
+ copyDirectAuthLink: "\u{1F4CB} Copy Link with Code",
804
+ // Card 3: Persistent Password
805
+ secretCardTitle: "Configure Persistent Password (Optional)",
806
+ 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.",
807
+ secretConfigured: "\u2705 Persistent password is configured (securely salted & encrypted)",
808
+ secretNotConfigured: "\u26AA No persistent password set (authentication via temporary codes only)",
809
+ secretInputPlaceholder: "Enter a password with at least 6 alphanumeric characters",
810
+ secretInputLabel: "Password:",
811
+ showPassword: "\u{1F441}\uFE0F Show Password",
812
+ hidePassword: "\u{1F648} Hide Password",
813
+ saveSecretBtn: "\u{1F4BE} Save Password",
814
+ clearSecretBtn: "\u{1F5D1}\uFE0F Clear Password",
815
+ secretWeakTip: "Password must be at least 6 characters and contain both letters and numbers",
816
+ clearSecretConfirm: "Are you sure you want to clear the persistent password? You will no longer be able to log in with a fixed password.",
817
+ // Card 4: Device List
818
+ deviceCardTitle: "Authorized Devices List",
819
+ deviceCardDesc: "All mobile devices successfully authorized via pairing code, persistent password, or bypass. You can revoke access at any time.",
820
+ authDevicesCount: "device(s) authorized",
821
+ revokeAllBtn: "\u{1F5D1}\uFE0F Revoke All Devices",
822
+ revokeAllConfirm: "Are you sure you want to disconnect and revoke all authorized devices?",
823
+ noDevices: "No authorized devices yet. Scan the QR code with your mobile device to connect.",
824
+ deviceOnlineNow: "Online",
825
+ deviceOffline: "Offline",
826
+ deviceBypassLabel: "Bypass",
827
+ devicePasscodeLabel: "Code",
828
+ deviceSecretLabel: "Password",
829
+ deviceFirstSeen: "Authorized: ",
830
+ deviceLastSeen: "Last active: ",
831
+ revokeDeviceBtn: "Revoke",
832
+ revokeDeviceConfirm: "Are you sure you want to revoke access for this device?",
833
+ // Card 5: Security & Audit
834
+ securityCardTitle: "Brute-Force Defense & IP Audit",
835
+ securityCardDesc: "Real-time monitoring of external access IP request frequencies and failure rates. Automatically locks IPs after 5 consecutive failures.",
836
+ securityNormal: "\u2705 All external IPs are in good standing. No brute-force attacks detected.",
837
+ unlockIpBtn: "\u{1F513} Unlock IP",
838
+ unlockIpConfirm: "Are you sure you want to unlock IP {ip}?",
839
+ clearSecurityLogBtn: "\u{1F5D1}\uFE0F Clear Audit Logs",
840
+ clearSecurityLogConfirm: "Are you sure you want to clear all IP access and security audit logs?",
841
+ thIp: "Client IP",
842
+ thStatus: "Status",
843
+ thFailed: "Fail / Total",
844
+ thLastSeen: "Last Seen",
845
+ thAction: "Actions",
846
+ statusLocked: "\u{1F6AB} Locked",
847
+ statusNormal: "\u{1F7E2} Normal",
848
+ // Card 6: Storage
849
+ storageCardTitle: "Local Storage & Key Persistence",
850
+ storageCardDesc: "All sensitive credentials are protected locally via slow salt hashing and asymmetric RSA cryptography, without cloud dependencies.",
851
+ storageDeviceFile: "Authorized devices & session cache:",
852
+ storageRsaFile: "Server RSA private key (0o600 protected):",
853
+ storageConfigFile: "Main configuration (namespace: dsh-remote-mobile):",
854
+ // Card 7: Advanced Security Config
855
+ configCardTitle: "Global Security Policy Configuration",
856
+ configCardDesc: "Configure per-IP rate limits, brute-force failure thresholds, and lockout durations. Changes take effect immediately and sync to ~/.dsh/settings.yaml.",
857
+ copySettingsPathBtn: "\u{1F4CB} Copy settings.yaml Path",
858
+ fieldVisitsLimit: "\u23F1\uFE0F Max Visits per Minute (visits/min)",
859
+ fieldVisitsLimitHint: "Rate limit triggered when a single IP opens login page more than this value",
860
+ fieldFailedLimit: "\u{1F6E1}\uFE0F Max Failed Password Attempts (times)",
861
+ fieldFailedLimitHint: "IP locked after consecutive incorrect pairing codes or passwords",
862
+ fieldLockDuration: "\u{1F6AB} Lockout Duration (minutes)",
863
+ fieldLockDurationHint: "Cool-down lock period for blocked IPs (default: 15 minutes)",
864
+ restoreDefaultsBtn: "\u{1F504} Restore Defaults",
865
+ saveConfigBtn: "\u{1F4BE} Save Configuration",
866
+ savingConfigBtn: "Saving...",
867
+ saveConfigConfirm: "Are you sure you want to save the current security policy? It will take effect immediately and sync to ~/.dsh/settings.yaml.",
868
+ 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.",
869
+ saveConfigSuccessToast: "Security policy successfully saved and synced to settings.yaml!",
870
+ restoreDefaultsSuccessToast: "Default security policies restored and applied immediately!",
871
+ saveConfigFailTip: "Parameters must be valid integers greater than 0",
872
+ // Toast
873
+ toastDeviceConnected: "\u{1F389} New device connected: {name} ({ip})",
874
+ toastDeviceOnline: "\u{1F4F6} Device reconnected: {name} ({ip})",
875
+ toastIpLocked: "\u{1F6AB} Security Alert: IP {ip} locked due to consecutive failures!"
876
+ }
877
+ };
878
+ function resolveLocale(ctx, status) {
879
+ try {
880
+ if (ctx) {
881
+ if (typeof ctx.get === "function") {
882
+ var loc = ctx.get("locale");
883
+ if (loc && loc.snapshot && loc.snapshot.active) {
884
+ return loc.snapshot.active.startsWith("en") ? "en" : "zh";
885
+ }
886
+ }
887
+ if (ctx.locale && ctx.locale.snapshot && ctx.locale.snapshot.active) {
888
+ return ctx.locale.snapshot.active.startsWith("en") ? "en" : "zh";
889
+ }
890
+ }
891
+ } catch (e) {
892
+ }
893
+ if (typeof document !== "undefined" && document.documentElement && document.documentElement.lang) {
894
+ return document.documentElement.lang.toLowerCase().startsWith("en") ? "en" : "zh";
895
+ }
896
+ if (status && typeof status.locale === "string") {
897
+ return status.locale.toLowerCase().startsWith("en") ? "en" : "zh";
898
+ }
899
+ if (typeof navigator !== "undefined" && navigator.language) {
900
+ return navigator.language.toLowerCase().startsWith("en") ? "en" : "zh";
901
+ }
902
+ return "zh";
903
+ }
904
+ function t(key, lang, params) {
905
+ var currentLang = lang && (lang.startsWith("en") || lang === "en") ? "en" : "zh";
906
+ var dict = translations[currentLang] || translations.zh;
907
+ var text = dict[key] || translations.zh[key] || key;
908
+ if (params && typeof params === "object") {
909
+ for (var p in params) {
910
+ if (Object.prototype.hasOwnProperty.call(params, p)) {
911
+ text = text.replace(new RegExp("\\{" + p + "\\}", "g"), String(params[p]));
912
+ }
913
+ }
914
+ }
915
+ return text;
916
+ }
917
+
918
+ // src/client/components/NetworkCard.jsx
919
+ var import_jsx_runtime = require("react/jsx-runtime");
920
+ function NetworkCard({ status, toggleTailscale, toggleLan, copyText, lang }) {
921
+ const tsHost = status.tailscaleIp || "100.x.y.z";
922
+ const lanHost = status.lanIp || "127.0.0.1";
923
+ const port = typeof window !== "undefined" && window.location.port || "3080";
924
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
925
+ "div",
926
+ {
927
+ style: {
928
+ background: "var(--dsw-alias-bg-layer-3, rgba(128,128,128,0.06))",
929
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.15))",
930
+ borderRadius: "12px",
931
+ padding: "18px 20px"
932
+ },
933
+ children: [
934
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
935
+ "div",
936
+ {
937
+ style: {
938
+ fontSize: "15px",
939
+ fontWeight: "600",
940
+ color: "var(--dsw-alias-label-primary, inherit)",
941
+ marginBottom: "4px"
942
+ },
943
+ children: t("netCardTitle", lang)
944
+ }
945
+ ),
946
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
947
+ "div",
948
+ {
949
+ style: {
950
+ fontSize: "12px",
951
+ color: "var(--dsw-alias-label-tertiary, #888)",
952
+ marginBottom: "16px"
953
+ },
954
+ children: t("netCardDesc", lang)
955
+ }
956
+ ),
957
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
958
+ "div",
959
+ {
960
+ style: {
961
+ display: "flex",
962
+ alignItems: "center",
963
+ justifyContent: "space-between",
964
+ flexWrap: "wrap",
965
+ gap: "12px",
966
+ paddingBottom: "16px",
967
+ borderBottom: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.12))",
968
+ marginBottom: "16px"
969
+ },
970
+ children: [
971
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { flex: 1, minWidth: "220px" }, children: [
972
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
973
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: "16px" }, children: "\u{1F512}" }),
974
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: "14px", fontWeight: "700" }, children: t("tailscaleSectionTitle", lang) }),
975
+ status.tailscaleIp ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
976
+ "span",
977
+ {
978
+ style: {
979
+ fontSize: "11px",
980
+ padding: "2px 6px",
981
+ borderRadius: "4px",
982
+ background: "rgba(16,185,129,0.15)",
983
+ color: "#10b981",
984
+ fontWeight: "600"
985
+ },
986
+ children: lang === "en" ? "Connected" : "\u5DF2\u8FDE\u63A5"
987
+ }
988
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
989
+ "span",
990
+ {
991
+ style: {
992
+ fontSize: "11px",
993
+ padding: "2px 6px",
994
+ borderRadius: "4px",
995
+ background: "rgba(239,68,68,0.15)",
996
+ color: "#ef4444",
997
+ fontWeight: "600"
998
+ },
999
+ children: lang === "en" ? "Not Connected" : "\u672A\u8FDE\u63A5"
1000
+ }
1001
+ )
1002
+ ] }),
1003
+ status.tailscaleIp ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1004
+ "div",
1005
+ {
1006
+ style: {
1007
+ fontSize: "13px",
1008
+ marginTop: "4px",
1009
+ fontFamily: "monospace",
1010
+ color: "var(--dsw-alias-brand-primary, #3b82f6)"
1011
+ },
1012
+ children: [
1013
+ `http://${tsHost}:${port}`,
1014
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1015
+ "button",
1016
+ {
1017
+ type: "button",
1018
+ onClick: () => copyText(`http://${tsHost}:${port}`, t("copiedTip", lang)),
1019
+ style: {
1020
+ marginLeft: "8px",
1021
+ padding: "2px 6px",
1022
+ background: "transparent",
1023
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.2))",
1024
+ borderRadius: "4px",
1025
+ fontSize: "11px",
1026
+ cursor: "pointer",
1027
+ color: "var(--dsw-alias-label-secondary, inherit)"
1028
+ },
1029
+ children: t("copyUrlBtn", lang)
1030
+ }
1031
+ )
1032
+ ]
1033
+ }
1034
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1035
+ "div",
1036
+ {
1037
+ style: {
1038
+ fontSize: "12px",
1039
+ color: "var(--dsw-alias-label-tertiary, #888)",
1040
+ marginTop: "4px"
1041
+ },
1042
+ children: t("tailscaleGuide", lang)
1043
+ }
1044
+ ),
1045
+ status.tailscaleIp ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1046
+ "div",
1047
+ {
1048
+ style: {
1049
+ fontSize: "12px",
1050
+ color: "var(--dsw-alias-label-tertiary, #888)",
1051
+ marginTop: "4px"
1052
+ },
1053
+ children: t("tailscaleBypassDesc", lang)
1054
+ }
1055
+ ) : null
1056
+ ] }),
1057
+ status.tailscaleIp ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
1058
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1059
+ "span",
1060
+ {
1061
+ style: {
1062
+ fontSize: "12px",
1063
+ color: status.allowTailscale ? "var(--dsw-alias-brand-primary, #3b82f6)" : "var(--dsw-alias-label-secondary, #888)",
1064
+ fontWeight: "600"
1065
+ },
1066
+ children: status.allowTailscale ? t("directBypassOn", lang) : t("directBypassOff", lang)
1067
+ }
1068
+ ),
1069
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1070
+ "label",
1071
+ {
1072
+ style: {
1073
+ position: "relative",
1074
+ display: "inline-block",
1075
+ width: "44px",
1076
+ height: "24px",
1077
+ cursor: "pointer",
1078
+ userSelect: "none"
1079
+ },
1080
+ children: [
1081
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1082
+ "input",
1083
+ {
1084
+ type: "checkbox",
1085
+ checked: !!status.allowTailscale,
1086
+ onChange: toggleTailscale,
1087
+ style: { opacity: 0, width: 0, height: 0, margin: 0 }
1088
+ }
1089
+ ),
1090
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1091
+ "span",
1092
+ {
1093
+ style: {
1094
+ position: "absolute",
1095
+ top: 0,
1096
+ left: 0,
1097
+ right: 0,
1098
+ bottom: 0,
1099
+ backgroundColor: status.allowTailscale ? "var(--dsw-alias-brand-primary, #3b82f6)" : "var(--dsw-alias-border-l2, rgba(128,128,128,0.3))",
1100
+ transition: "background-color 0.25s ease",
1101
+ borderRadius: "24px"
1102
+ }
1103
+ }
1104
+ ),
1105
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1106
+ "span",
1107
+ {
1108
+ style: {
1109
+ position: "absolute",
1110
+ top: "3px",
1111
+ left: "3px",
1112
+ width: "18px",
1113
+ height: "18px",
1114
+ backgroundColor: "#ffffff",
1115
+ borderRadius: "50%",
1116
+ boxShadow: "0 2px 4px rgba(0, 0, 0, 0.25)",
1117
+ transform: status.allowTailscale ? "translateX(20px)" : "translateX(0px)",
1118
+ transition: "transform 0.25s cubic-bezier(0.4, 0, 0.2, 1)"
1119
+ }
1120
+ }
1121
+ )
1122
+ ]
1123
+ }
1124
+ )
1125
+ ] }) : null
1126
+ ]
1127
+ }
1128
+ ),
1129
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "10px" }, children: [
1130
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1131
+ "div",
1132
+ {
1133
+ style: {
1134
+ display: "flex",
1135
+ alignItems: "center",
1136
+ justifyContent: "space-between",
1137
+ flexWrap: "wrap",
1138
+ gap: "12px"
1139
+ },
1140
+ children: [
1141
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { flex: 1, minWidth: "220px" }, children: [
1142
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
1143
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: "16px" }, children: "\u{1F3E0}" }),
1144
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: "14px", fontWeight: "700" }, children: t("lanSectionTitle", lang) }),
1145
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1146
+ "span",
1147
+ {
1148
+ style: {
1149
+ fontSize: "11px",
1150
+ padding: "2px 6px",
1151
+ borderRadius: "4px",
1152
+ background: "rgba(16,185,129,0.15)",
1153
+ color: "#10b981",
1154
+ fontWeight: "600"
1155
+ },
1156
+ children: lang === "en" ? "Ready" : "\u5DF2\u5C31\u7EEA"
1157
+ }
1158
+ )
1159
+ ] }),
1160
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1161
+ "div",
1162
+ {
1163
+ style: {
1164
+ fontSize: "13px",
1165
+ marginTop: "4px",
1166
+ fontFamily: "monospace",
1167
+ color: "var(--dsw-alias-brand-primary, #3b82f6)"
1168
+ },
1169
+ children: [
1170
+ `http://${lanHost}:${port}`,
1171
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1172
+ "button",
1173
+ {
1174
+ type: "button",
1175
+ onClick: () => copyText(`http://${lanHost}:${port}`, t("copiedTip", lang)),
1176
+ style: {
1177
+ marginLeft: "8px",
1178
+ padding: "2px 6px",
1179
+ background: "transparent",
1180
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.2))",
1181
+ borderRadius: "4px",
1182
+ fontSize: "11px",
1183
+ cursor: "pointer",
1184
+ color: "var(--dsw-alias-label-secondary, inherit)"
1185
+ },
1186
+ children: t("copyUrlBtn", lang)
1187
+ }
1188
+ )
1189
+ ]
1190
+ }
1191
+ )
1192
+ ] }),
1193
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
1194
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1195
+ "span",
1196
+ {
1197
+ style: {
1198
+ fontSize: "12px",
1199
+ color: status.allowLan ? "#ef4444" : "var(--dsw-alias-label-secondary, #888)",
1200
+ fontWeight: status.allowLan ? "700" : "500",
1201
+ display: "flex",
1202
+ alignItems: "center",
1203
+ gap: "4px"
1204
+ },
1205
+ children: status.allowLan ? lang === "en" ? "\u{1F6A8} LAN Bypass Enabled (HIGH RISK)" : "\u{1F6A8} \u5C40\u57DF\u7F51\u514D\u5BC6\u5DF2\u5F00\u542F\uFF08\u9AD8\u5371\u72B6\u6001\uFF09" : t("directBypassOff", lang)
1206
+ }
1207
+ ),
1208
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1209
+ "label",
1210
+ {
1211
+ style: {
1212
+ position: "relative",
1213
+ display: "inline-block",
1214
+ width: "44px",
1215
+ height: "24px",
1216
+ cursor: "pointer",
1217
+ userSelect: "none"
1218
+ },
1219
+ children: [
1220
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1221
+ "input",
1222
+ {
1223
+ type: "checkbox",
1224
+ checked: !!status.allowLan,
1225
+ onChange: toggleLan,
1226
+ style: { opacity: 0, width: 0, height: 0, margin: 0 }
1227
+ }
1228
+ ),
1229
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1230
+ "span",
1231
+ {
1232
+ style: {
1233
+ position: "absolute",
1234
+ top: 0,
1235
+ left: 0,
1236
+ right: 0,
1237
+ bottom: 0,
1238
+ backgroundColor: status.allowLan ? "#ef4444" : "var(--dsw-alias-border-l2, rgba(128,128,128,0.3))",
1239
+ boxShadow: status.allowLan ? "0 0 10px rgba(239, 68, 68, 0.45)" : "none",
1240
+ transition: "all 0.25s ease",
1241
+ borderRadius: "24px"
1242
+ }
1243
+ }
1244
+ ),
1245
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1246
+ "span",
1247
+ {
1248
+ style: {
1249
+ position: "absolute",
1250
+ top: "3px",
1251
+ left: "3px",
1252
+ width: "18px",
1253
+ height: "18px",
1254
+ backgroundColor: "#ffffff",
1255
+ borderRadius: "50%",
1256
+ boxShadow: "0 2px 4px rgba(0, 0, 0, 0.25)",
1257
+ transform: status.allowLan ? "translateX(20px)" : "translateX(0px)",
1258
+ transition: "transform 0.25s cubic-bezier(0.4, 0, 0.2, 1)"
1259
+ }
1260
+ }
1261
+ )
1262
+ ]
1263
+ }
1264
+ )
1265
+ ] })
1266
+ ]
1267
+ }
1268
+ ),
1269
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1270
+ "div",
1271
+ {
1272
+ style: {
1273
+ marginTop: "4px",
1274
+ padding: "10px 14px",
1275
+ borderRadius: "8px",
1276
+ background: status.allowLan ? "rgba(239, 68, 68, 0.12)" : "rgba(245, 158, 11, 0.08)",
1277
+ border: status.allowLan ? "1px solid rgba(239, 68, 68, 0.4)" : "1px solid rgba(245, 158, 11, 0.25)",
1278
+ borderLeft: status.allowLan ? "4px solid #ef4444" : "4px solid #f59e0b",
1279
+ boxShadow: status.allowLan ? "0 2px 8px rgba(239, 68, 68, 0.15)" : "none"
1280
+ },
1281
+ children: [
1282
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1283
+ "div",
1284
+ {
1285
+ style: {
1286
+ display: "flex",
1287
+ alignItems: "center",
1288
+ gap: "6px",
1289
+ fontSize: "12px",
1290
+ fontWeight: "700",
1291
+ color: status.allowLan ? "#ef4444" : "#f59e0b",
1292
+ marginBottom: "4px"
1293
+ },
1294
+ children: [
1295
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: status.allowLan ? "\u{1F6A8}" : "\u26A0\uFE0F" }),
1296
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("lanBypassWarningTitle", lang) })
1297
+ ]
1298
+ }
1299
+ ),
1300
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1301
+ "div",
1302
+ {
1303
+ style: {
1304
+ fontSize: "12px",
1305
+ lineHeight: "1.55",
1306
+ color: status.allowLan ? "var(--dsw-alias-label-primary, #fca5a5)" : "var(--dsw-alias-label-secondary, #cbd5e1)"
1307
+ },
1308
+ children: t("lanBypassDesc", lang)
1309
+ }
1310
+ )
1311
+ ]
1312
+ }
1313
+ )
1314
+ ] })
1315
+ ]
1316
+ }
1317
+ );
1318
+ }
1319
+
1320
+ // src/client/components/QrPairingCard.jsx
1321
+ var import_react2 = __toESM(require("react"), 1);
1322
+
1323
+ // src/client/utils/format.js
1324
+ function formatTime(ts, lang) {
1325
+ if (!ts) return lang === "en" ? "Unknown" : "\u672A\u77E5\u65F6\u95F4";
1326
+ var date = new Date(ts);
1327
+ var now = /* @__PURE__ */ new Date();
1328
+ var isToday = date.toDateString() === now.toDateString();
1329
+ var pad = function(n) {
1330
+ return n < 10 ? "0" + n : n;
1331
+ };
1332
+ var timeStr = pad(date.getHours()) + ":" + pad(date.getMinutes()) + ":" + pad(date.getSeconds());
1333
+ if (isToday) {
1334
+ return (lang === "en" ? "Today " : "\u4ECA\u5929 ") + timeStr;
1335
+ }
1336
+ if (lang === "en") {
1337
+ var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1338
+ return months[date.getMonth()] + " " + date.getDate() + " " + timeStr;
1339
+ }
1340
+ return date.getMonth() + 1 + "\u6708" + date.getDate() + "\u65E5 " + timeStr;
1341
+ }
1342
+ function formatCountdown(sec, lang) {
1343
+ if (sec <= 0) return lang === "en" ? "Expired" : "\u5DF2\u8FC7\u671F";
1344
+ var m = Math.floor(sec / 60);
1345
+ var s = sec % 60;
1346
+ if (lang === "en") {
1347
+ return (m > 0 ? m + "m " : "") + (s < 10 ? "0" + s : s) + "s";
1348
+ }
1349
+ return (m > 0 ? m + " \u5206 " : "") + (s < 10 ? "0" + s : s) + " \u79D2";
1350
+ }
1351
+
1352
+ // src/client/components/QrPairingCard.jsx
1353
+ var import_jsx_runtime2 = require("react/jsx-runtime");
1354
+ function QrPairingCard({
1355
+ status,
1356
+ showCode,
1357
+ setShowCode,
1358
+ timeLeft,
1359
+ showQr,
1360
+ setShowQr,
1361
+ selectedTab,
1362
+ setSelectedTab,
1363
+ generateQrSvg: generateQrSvg2,
1364
+ refreshStatusAndCode,
1365
+ copyDirectLink,
1366
+ lang
1367
+ }) {
1368
+ const port = typeof window !== "undefined" && window.location.port || "3080";
1369
+ const isExpired = timeLeft <= 0;
1370
+ const currentHost = selectedTab === "lan" && status.lanIp ? status.lanIp : status.tailscaleIp || status.lanIp || typeof window !== "undefined" && window.location.hostname || "127.0.0.1";
1371
+ const directLink = `http://${currentHost}:${port}/auth?token=${status.code || ""}`;
1372
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1373
+ "div",
1374
+ {
1375
+ style: {
1376
+ background: "var(--dsw-alias-bg-layer-3, rgba(128,128,128,0.06))",
1377
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.15))",
1378
+ borderRadius: "12px",
1379
+ padding: "20px",
1380
+ textAlign: "center"
1381
+ },
1382
+ children: [
1383
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1384
+ "div",
1385
+ {
1386
+ style: {
1387
+ fontSize: "15px",
1388
+ fontWeight: "600",
1389
+ marginBottom: "4px",
1390
+ color: "var(--dsw-alias-label-primary, inherit)"
1391
+ },
1392
+ children: `\u{1F4F1} ${t("shortCodeTitle", lang)}`
1393
+ }
1394
+ ),
1395
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1396
+ "div",
1397
+ {
1398
+ style: {
1399
+ fontSize: "12px",
1400
+ color: "var(--dsw-alias-label-tertiary, #888)",
1401
+ marginBottom: "14px"
1402
+ },
1403
+ children: t("pairingCardDesc", lang)
1404
+ }
1405
+ ),
1406
+ showQr ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1407
+ "div",
1408
+ {
1409
+ style: {
1410
+ background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.06))",
1411
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.12))",
1412
+ borderRadius: "10px",
1413
+ padding: "16px",
1414
+ maxWidth: "320px",
1415
+ margin: "0 auto 16px auto",
1416
+ display: "flex",
1417
+ flexDirection: "column",
1418
+ alignItems: "center",
1419
+ gap: "10px"
1420
+ },
1421
+ children: [
1422
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1423
+ "div",
1424
+ {
1425
+ style: {
1426
+ display: "flex",
1427
+ width: "100%",
1428
+ background: "var(--dsw-alias-bg-layer-3, rgba(128,128,128,0.1))",
1429
+ padding: "3px",
1430
+ borderRadius: "8px"
1431
+ },
1432
+ children: [
1433
+ status.tailscaleIp ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1434
+ "button",
1435
+ {
1436
+ type: "button",
1437
+ onClick: () => setSelectedTab("tailscale"),
1438
+ style: {
1439
+ flex: 1,
1440
+ padding: "5px 8px",
1441
+ borderRadius: "6px",
1442
+ border: "none",
1443
+ background: selectedTab === "tailscale" ? "var(--dsw-alias-brand-primary, #3b82f6)" : "transparent",
1444
+ color: selectedTab === "tailscale" ? "#ffffff" : "var(--dsw-alias-label-secondary, inherit)",
1445
+ fontSize: "12px",
1446
+ fontWeight: "600",
1447
+ cursor: "pointer",
1448
+ transition: "all 0.2s"
1449
+ },
1450
+ children: `\u{1F680} ${t("switchTailscaleTab", lang)}`
1451
+ }
1452
+ ) : null,
1453
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1454
+ "button",
1455
+ {
1456
+ type: "button",
1457
+ onClick: () => setSelectedTab("lan"),
1458
+ style: {
1459
+ flex: 1,
1460
+ padding: "5px 8px",
1461
+ borderRadius: "6px",
1462
+ border: "none",
1463
+ background: selectedTab === "lan" ? "var(--dsw-alias-brand-primary, #3b82f6)" : "transparent",
1464
+ color: selectedTab === "lan" ? "#ffffff" : "var(--dsw-alias-label-secondary, inherit)",
1465
+ fontSize: "12px",
1466
+ fontWeight: "600",
1467
+ cursor: "pointer",
1468
+ transition: "all 0.2s"
1469
+ },
1470
+ children: `\u{1F3E0} ${t("switchLanTab", lang)}`
1471
+ }
1472
+ )
1473
+ ]
1474
+ }
1475
+ ),
1476
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1477
+ "div",
1478
+ {
1479
+ style: {
1480
+ display: "flex",
1481
+ justifyContent: "center",
1482
+ opacity: isExpired ? 0.3 : 1
1483
+ },
1484
+ dangerouslySetInnerHTML: {
1485
+ __html: generateQrSvg2(directLink, 170)
1486
+ }
1487
+ }
1488
+ ),
1489
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1490
+ "div",
1491
+ {
1492
+ style: {
1493
+ width: "100%",
1494
+ padding: "6px 10px",
1495
+ background: "var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.12))",
1496
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.15))",
1497
+ borderRadius: "6px",
1498
+ fontFamily: "monospace",
1499
+ fontSize: "11px",
1500
+ color: isExpired ? "var(--dsw-alias-label-error, #ef4444)" : "var(--dsw-alias-label-secondary, inherit)",
1501
+ wordBreak: "break-all",
1502
+ userSelect: "all",
1503
+ lineHeight: "1.4",
1504
+ textAlign: "center"
1505
+ },
1506
+ children: [
1507
+ `http://${currentHost}:${port}/auth?token=`,
1508
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1509
+ "span",
1510
+ {
1511
+ style: {
1512
+ fontWeight: "700",
1513
+ color: showCode ? "var(--dsw-alias-brand-primary, #3b82f6)" : "inherit",
1514
+ letterSpacing: showCode ? "1px" : "2px"
1515
+ },
1516
+ children: showCode ? status.code || "" : "\u2022\u2022\u2022\u2022\u2022\u2022"
1517
+ }
1518
+ )
1519
+ ]
1520
+ }
1521
+ ),
1522
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1523
+ "button",
1524
+ {
1525
+ type: "button",
1526
+ onClick: copyDirectLink,
1527
+ style: {
1528
+ padding: "6px 16px",
1529
+ background: "var(--dsw-alias-brand-primary, #3b82f6)",
1530
+ color: "white",
1531
+ border: "none",
1532
+ borderRadius: "6px",
1533
+ fontSize: "12px",
1534
+ fontWeight: "600",
1535
+ cursor: "pointer",
1536
+ transition: "background-color 0.2s"
1537
+ },
1538
+ children: status.copied ? `\u2705 ${lang === "en" ? "Copied!" : "\u5DF2\u590D\u5236\uFF01"}` : t("copyDirectAuthLink", lang)
1539
+ }
1540
+ )
1541
+ ]
1542
+ }
1543
+ ) : null,
1544
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1545
+ "div",
1546
+ {
1547
+ style: {
1548
+ display: "inline-flex",
1549
+ alignItems: "center",
1550
+ justifyContent: "center",
1551
+ gap: "12px",
1552
+ background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.1))",
1553
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.2))",
1554
+ borderRadius: "10px",
1555
+ padding: "6px 20px",
1556
+ margin: "4px auto 8px auto"
1557
+ },
1558
+ children: [
1559
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1560
+ "span",
1561
+ {
1562
+ style: {
1563
+ fontSize: "32px",
1564
+ fontWeight: "800",
1565
+ letterSpacing: showCode ? "6px" : "4px",
1566
+ color: isExpired ? "var(--dsw-alias-label-error, #ef4444)" : "var(--dsw-alias-brand-primary, #3b82f6)",
1567
+ fontFamily: "monospace",
1568
+ userSelect: "all"
1569
+ },
1570
+ children: isExpired ? lang === "en" ? "EXPIRED" : "\u5DF2\u8FC7\u671F" : showCode ? status.code : "\u2022\u2022\u2022\u2022\u2022\u2022"
1571
+ }
1572
+ ),
1573
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1574
+ "button",
1575
+ {
1576
+ type: "button",
1577
+ onClick: () => setShowCode(!showCode),
1578
+ title: showCode ? t("clickToHide", lang) : t("clickToReveal", lang),
1579
+ style: {
1580
+ background: "transparent",
1581
+ border: "none",
1582
+ color: "var(--dsw-alias-label-secondary, #888)",
1583
+ cursor: "pointer",
1584
+ display: "flex",
1585
+ alignItems: "center",
1586
+ justifyContent: "center",
1587
+ padding: "4px"
1588
+ },
1589
+ 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" }) })
1590
+ }
1591
+ )
1592
+ ]
1593
+ }
1594
+ ),
1595
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1596
+ "div",
1597
+ {
1598
+ style: {
1599
+ fontSize: "12px",
1600
+ color: isExpired ? "var(--dsw-alias-label-error, #ef4444)" : "var(--dsw-alias-label-secondary, #888)",
1601
+ margin: "4px 0 16px 0",
1602
+ display: "flex",
1603
+ alignItems: "center",
1604
+ justifyContent: "center",
1605
+ gap: "6px"
1606
+ },
1607
+ children: [
1608
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1609
+ "span",
1610
+ {
1611
+ style: {
1612
+ display: "inline-block",
1613
+ width: "8px",
1614
+ height: "8px",
1615
+ borderRadius: "50%",
1616
+ background: isExpired ? "#ef4444" : "#10b981"
1617
+ }
1618
+ }
1619
+ ),
1620
+ isExpired ? `\u26A0\uFE0F ${t("codeExpired", lang)}` : `${t("codeValidUntil", lang)}${formatCountdown(timeLeft, lang)}`
1621
+ ]
1622
+ }
1623
+ ),
1624
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1625
+ "div",
1626
+ {
1627
+ style: {
1628
+ display: "flex",
1629
+ gap: "10px",
1630
+ justifyContent: "center",
1631
+ flexWrap: "wrap"
1632
+ },
1633
+ children: [
1634
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1635
+ "button",
1636
+ {
1637
+ type: "button",
1638
+ onClick: () => setShowQr(!showQr),
1639
+ style: {
1640
+ padding: "7px 16px",
1641
+ background: showQr ? "rgba(59,130,246,0.15)" : "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.1))",
1642
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.2))",
1643
+ color: showQr ? "var(--dsw-alias-brand-primary, #3b82f6)" : "var(--dsw-alias-label-primary, inherit)",
1644
+ borderRadius: "8px",
1645
+ fontSize: "13px",
1646
+ fontWeight: "600",
1647
+ cursor: "pointer"
1648
+ },
1649
+ children: showQr ? `\u{1F4F7} ${t("toggleQrHide", lang)}` : `\u{1F4F7} ${t("toggleQrShow", lang)}`
1650
+ }
1651
+ ),
1652
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1653
+ "button",
1654
+ {
1655
+ type: "button",
1656
+ onClick: refreshStatusAndCode,
1657
+ style: {
1658
+ padding: "7px 16px",
1659
+ background: "rgba(59,130,246,0.12)",
1660
+ border: "1px solid rgba(59,130,246,0.3)",
1661
+ color: "var(--dsw-alias-brand-primary, #3b82f6)",
1662
+ borderRadius: "8px",
1663
+ fontSize: "13px",
1664
+ fontWeight: "600",
1665
+ cursor: "pointer"
1666
+ },
1667
+ children: t("refreshCodeBtn", lang)
1668
+ }
1669
+ )
1670
+ ]
1671
+ }
1672
+ )
1673
+ ]
1674
+ }
1675
+ );
1676
+ }
1677
+
1678
+ // src/client/components/SecretCard.jsx
1679
+ var import_react3 = __toESM(require("react"), 1);
1680
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1681
+ function SecretCard({
1682
+ status,
1683
+ secretInput,
1684
+ setSecretInput,
1685
+ showSecret,
1686
+ setShowSecret,
1687
+ saveSecret,
1688
+ clearSecret,
1689
+ lang
1690
+ }) {
1691
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1692
+ "div",
1693
+ {
1694
+ style: {
1695
+ background: "var(--dsw-alias-bg-layer-3, rgba(128,128,128,0.06))",
1696
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.15))",
1697
+ borderRadius: "12px",
1698
+ padding: "18px 20px"
1699
+ },
1700
+ children: [
1701
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1702
+ "style",
1703
+ {
1704
+ dangerouslySetInnerHTML: {
1705
+ __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; }"
1706
+ }
1707
+ }
1708
+ ),
1709
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1710
+ "div",
1711
+ {
1712
+ style: {
1713
+ display: "flex",
1714
+ alignItems: "center",
1715
+ justifyContent: "space-between",
1716
+ marginBottom: "4px"
1717
+ },
1718
+ children: [
1719
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1720
+ "div",
1721
+ {
1722
+ style: {
1723
+ fontSize: "15px",
1724
+ fontWeight: "600",
1725
+ color: "var(--dsw-alias-label-primary, inherit)"
1726
+ },
1727
+ children: t("secretCardTitle", lang)
1728
+ }
1729
+ ),
1730
+ status.hasSecret ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1731
+ "span",
1732
+ {
1733
+ style: {
1734
+ fontSize: "12px",
1735
+ color: "#10b981",
1736
+ background: "rgba(16,185,129,0.12)",
1737
+ padding: "2px 8px",
1738
+ borderRadius: "12px",
1739
+ fontWeight: "500"
1740
+ },
1741
+ children: t("secretConfigured", lang)
1742
+ }
1743
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1744
+ "span",
1745
+ {
1746
+ style: {
1747
+ fontSize: "12px",
1748
+ color: "var(--dsw-alias-label-tertiary, #888)"
1749
+ },
1750
+ children: t("secretNotConfigured", lang)
1751
+ }
1752
+ )
1753
+ ]
1754
+ }
1755
+ ),
1756
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1757
+ "div",
1758
+ {
1759
+ style: {
1760
+ fontSize: "12px",
1761
+ color: "var(--dsw-alias-label-tertiary, #888)",
1762
+ marginBottom: "12px"
1763
+ },
1764
+ children: t("secretCardDesc", lang)
1765
+ }
1766
+ ),
1767
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: "10px", alignItems: "center" }, children: [
1768
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1769
+ "div",
1770
+ {
1771
+ style: {
1772
+ position: "relative",
1773
+ flex: 1,
1774
+ display: "flex",
1775
+ alignItems: "center"
1776
+ },
1777
+ children: [
1778
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1779
+ "input",
1780
+ {
1781
+ className: "dsh-secret-pwd-input",
1782
+ type: showSecret ? "text" : "password",
1783
+ value: secretInput,
1784
+ 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),
1785
+ onChange: (e) => setSecretInput(e.target.value),
1786
+ style: {
1787
+ width: "100%",
1788
+ height: "38px",
1789
+ background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.1))",
1790
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.2))",
1791
+ borderRadius: "8px",
1792
+ padding: "0 38px 0 12px",
1793
+ color: "inherit",
1794
+ fontSize: "14px",
1795
+ outline: "none"
1796
+ }
1797
+ }
1798
+ ),
1799
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1800
+ "button",
1801
+ {
1802
+ type: "button",
1803
+ onClick: () => setShowSecret(!showSecret),
1804
+ title: showSecret ? t("hidePassword", lang) : t("showPassword", lang),
1805
+ style: {
1806
+ position: "absolute",
1807
+ right: "8px",
1808
+ background: "transparent",
1809
+ border: "none",
1810
+ color: "var(--dsw-alias-label-secondary, #888)",
1811
+ cursor: "pointer",
1812
+ display: "flex",
1813
+ alignItems: "center",
1814
+ justifyContent: "center",
1815
+ padding: "4px"
1816
+ },
1817
+ 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" }) })
1818
+ }
1819
+ )
1820
+ ]
1821
+ }
1822
+ ),
1823
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1824
+ "button",
1825
+ {
1826
+ type: "button",
1827
+ onClick: saveSecret,
1828
+ style: {
1829
+ padding: "0 16px",
1830
+ height: "38px",
1831
+ background: "var(--dsw-alias-brand-primary, #3b82f6)",
1832
+ color: "white",
1833
+ border: "none",
1834
+ borderRadius: "8px",
1835
+ fontSize: "13px",
1836
+ fontWeight: "600",
1837
+ cursor: "pointer",
1838
+ whiteSpace: "nowrap",
1839
+ flexShrink: 0
1840
+ },
1841
+ children: t("saveSecretBtn", lang)
1842
+ }
1843
+ ),
1844
+ status.hasSecret ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1845
+ "button",
1846
+ {
1847
+ type: "button",
1848
+ onClick: clearSecret,
1849
+ title: t("clearSecretConfirm", lang),
1850
+ style: {
1851
+ padding: "0 12px",
1852
+ height: "38px",
1853
+ background: "rgba(239, 68, 68, 0.1)",
1854
+ color: "#ef4444",
1855
+ border: "1px solid rgba(239, 68, 68, 0.2)",
1856
+ borderRadius: "8px",
1857
+ fontSize: "13px",
1858
+ fontWeight: "500",
1859
+ cursor: "pointer",
1860
+ whiteSpace: "nowrap",
1861
+ flexShrink: 0
1862
+ },
1863
+ children: t("clearSecretBtn", lang)
1864
+ }
1865
+ ) : null
1866
+ ] })
1867
+ ]
1868
+ }
1869
+ );
1870
+ }
1871
+
1872
+ // src/client/components/DeviceListCard.jsx
1873
+ var import_react4 = __toESM(require("react"), 1);
1874
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1875
+ function isCurrentLocalIp(ip, status) {
1876
+ if (!ip) return false;
1877
+ if (ip === "127.0.0.1" || ip === "::1" || ip === "localhost") return true;
1878
+ if (status && status.lanIp && ip === status.lanIp) return true;
1879
+ if (status && status.tailscaleIp && ip === status.tailscaleIp) return true;
1880
+ if (typeof window !== "undefined" && window.location && window.location.hostname && ip === window.location.hostname)
1881
+ return true;
1882
+ return false;
1883
+ }
1884
+ function DeviceListCard({ status, revokeDevice, revokeAll, lang }) {
1885
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1886
+ "div",
1887
+ {
1888
+ style: {
1889
+ background: "var(--dsw-alias-bg-layer-3, rgba(128,128,128,0.06))",
1890
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.15))",
1891
+ borderRadius: "12px",
1892
+ padding: "18px 20px"
1893
+ },
1894
+ children: [
1895
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1896
+ "div",
1897
+ {
1898
+ style: {
1899
+ display: "flex",
1900
+ alignItems: "center",
1901
+ justifyContent: "space-between",
1902
+ gap: "12px",
1903
+ marginBottom: "14px"
1904
+ },
1905
+ children: [
1906
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { minWidth: 0 }, children: [
1907
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1908
+ "div",
1909
+ {
1910
+ style: {
1911
+ fontSize: "15px",
1912
+ fontWeight: "600",
1913
+ color: "var(--dsw-alias-label-primary, inherit)"
1914
+ },
1915
+ children: `\u{1F4F1} ${t("deviceCardTitle", lang)} (${status.devicesCount}${lang === "en" ? " devices" : " \u53F0"})`
1916
+ }
1917
+ ),
1918
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1919
+ "div",
1920
+ {
1921
+ style: {
1922
+ fontSize: "12px",
1923
+ color: "var(--dsw-alias-label-tertiary, #888)",
1924
+ marginTop: "2px"
1925
+ },
1926
+ children: t("deviceCardDesc", lang)
1927
+ }
1928
+ )
1929
+ ] }),
1930
+ status.devicesCount > 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1931
+ "button",
1932
+ {
1933
+ type: "button",
1934
+ onClick: revokeAll,
1935
+ style: {
1936
+ background: "transparent",
1937
+ border: "1px solid rgba(239,68,68,0.4)",
1938
+ color: "var(--dsw-alias-label-error, #ef4444)",
1939
+ padding: "5px 12px",
1940
+ borderRadius: "6px",
1941
+ fontSize: "12px",
1942
+ cursor: "pointer",
1943
+ whiteSpace: "nowrap",
1944
+ flexShrink: 0
1945
+ },
1946
+ children: t("revokeAllBtn", lang)
1947
+ }
1948
+ ) : null
1949
+ ]
1950
+ }
1951
+ ),
1952
+ 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) => {
1953
+ const isLocal = isCurrentLocalIp(dev.ip, status);
1954
+ const ipLabel = `${dev.ip}${isLocal ? lang === "en" ? " (Local)" : " (\u672C\u673A)" : ""}`;
1955
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1956
+ "div",
1957
+ {
1958
+ style: {
1959
+ display: "flex",
1960
+ alignItems: "center",
1961
+ justifyContent: "space-between",
1962
+ gap: "12px",
1963
+ padding: "12px 14px",
1964
+ background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.08))",
1965
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.12))",
1966
+ borderRadius: "8px"
1967
+ },
1968
+ children: [
1969
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1970
+ "div",
1971
+ {
1972
+ style: {
1973
+ display: "flex",
1974
+ flexDirection: "column",
1975
+ gap: "4px",
1976
+ minWidth: 0,
1977
+ flex: 1
1978
+ },
1979
+ children: [
1980
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1981
+ "div",
1982
+ {
1983
+ style: {
1984
+ fontSize: "14px",
1985
+ fontWeight: "600",
1986
+ color: "var(--dsw-alias-label-primary, inherit)",
1987
+ display: "flex",
1988
+ alignItems: "center",
1989
+ flexWrap: "wrap",
1990
+ gap: "8px"
1991
+ },
1992
+ children: [
1993
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: dev.deviceName || (lang === "en" ? "Mobile Device" : "\u79FB\u52A8\u7AEF\u8BBE\u5907") }),
1994
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1995
+ "span",
1996
+ {
1997
+ style: {
1998
+ fontSize: "11px",
1999
+ padding: "1px 6px",
2000
+ borderRadius: "4px",
2001
+ background: dev.isBypass ? "rgba(16,185,129,0.15)" : "rgba(59,130,246,0.15)",
2002
+ color: dev.isBypass ? "#10b981" : "var(--dsw-alias-brand-primary, #3b82f6)",
2003
+ fontWeight: "normal",
2004
+ whiteSpace: "nowrap"
2005
+ },
2006
+ children: dev.authType
2007
+ }
2008
+ )
2009
+ ]
2010
+ }
2011
+ ),
2012
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2013
+ "div",
2014
+ {
2015
+ style: {
2016
+ fontSize: "12px",
2017
+ color: "var(--dsw-alias-label-tertiary, #888)",
2018
+ lineHeight: "1.4",
2019
+ wordBreak: "break-word"
2020
+ },
2021
+ children: [
2022
+ lang === "en" ? "IP: " : "\u6765\u6E90 IP: ",
2023
+ ipLabel,
2024
+ lang === "en" ? " \xB7 Authorized: " : " \xB7 \u6388\u6743\u4E8E ",
2025
+ formatTime(dev.createdAt, lang),
2026
+ lang === "en" ? " \xB7 Last seen: " : " \xB7 \u6700\u8FD1\u6D3B\u8DC3 ",
2027
+ formatTime(dev.lastSeenAt, lang)
2028
+ ]
2029
+ }
2030
+ )
2031
+ ]
2032
+ }
2033
+ ),
2034
+ !dev.isBypass ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2035
+ "button",
2036
+ {
2037
+ type: "button",
2038
+ onClick: () => revokeDevice(dev.token),
2039
+ style: {
2040
+ padding: "5px 12px",
2041
+ background: "transparent",
2042
+ border: "1px solid rgba(239,68,68,0.3)",
2043
+ color: "var(--dsw-alias-label-error, #ef4444)",
2044
+ borderRadius: "6px",
2045
+ fontSize: "12px",
2046
+ cursor: "pointer",
2047
+ whiteSpace: "nowrap",
2048
+ flexShrink: 0
2049
+ },
2050
+ children: t("revokeDeviceBtn", lang)
2051
+ }
2052
+ ) : null
2053
+ ]
2054
+ },
2055
+ dev.token
2056
+ );
2057
+ }) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2058
+ "div",
2059
+ {
2060
+ style: {
2061
+ textAlign: "center",
2062
+ padding: "16px",
2063
+ color: "var(--dsw-alias-label-tertiary, #888)",
2064
+ fontSize: "13px"
2065
+ },
2066
+ children: t("noDevices", lang)
2067
+ }
2068
+ )
2069
+ ]
2070
+ }
2071
+ );
2072
+ }
2073
+
2074
+ // src/client/components/SecurityCard.jsx
2075
+ var import_react5 = __toESM(require("react"), 1);
2076
+ var import_jsx_runtime5 = require("react/jsx-runtime");
2077
+ function isCurrentLocalIp2(ip, status) {
2078
+ if (!ip) return false;
2079
+ if (ip === "127.0.0.1" || ip === "::1" || ip === "localhost") return true;
2080
+ if (status && status.lanIp && ip === status.lanIp) return true;
2081
+ if (status && status.tailscaleIp && ip === status.tailscaleIp) return true;
2082
+ if (typeof window !== "undefined" && window.location && window.location.hostname && ip === window.location.hostname)
2083
+ return true;
2084
+ return false;
2085
+ }
2086
+ function SecurityCard({ status, unlockIp, clearIpStats, lang }) {
2087
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2088
+ "div",
2089
+ {
2090
+ style: {
2091
+ background: "var(--dsw-alias-bg-layer-3, rgba(128,128,128,0.06))",
2092
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.15))",
2093
+ borderRadius: "12px",
2094
+ padding: "18px 20px"
2095
+ },
2096
+ children: [
2097
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2098
+ "div",
2099
+ {
2100
+ style: {
2101
+ display: "flex",
2102
+ alignItems: "center",
2103
+ justifyContent: "space-between",
2104
+ marginBottom: "8px"
2105
+ },
2106
+ children: [
2107
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
2108
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { fontSize: "16px" }, children: "\u{1F6E1}\uFE0F" }),
2109
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2110
+ "span",
2111
+ {
2112
+ style: {
2113
+ fontSize: "15px",
2114
+ fontWeight: "600",
2115
+ color: "var(--dsw-alias-label-primary, inherit)",
2116
+ whiteSpace: "nowrap"
2117
+ },
2118
+ children: t("securityCardTitle", lang)
2119
+ }
2120
+ )
2121
+ ] }),
2122
+ status.ipSecurityStats && status.ipSecurityStats.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2123
+ "button",
2124
+ {
2125
+ type: "button",
2126
+ onClick: clearIpStats,
2127
+ style: {
2128
+ background: "transparent",
2129
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))",
2130
+ color: "var(--dsw-alias-label-secondary, inherit)",
2131
+ padding: "3px 10px",
2132
+ borderRadius: "6px",
2133
+ fontSize: "12px",
2134
+ cursor: "pointer"
2135
+ },
2136
+ children: t("clearSecurityLogBtn", lang)
2137
+ }
2138
+ ) : null
2139
+ ]
2140
+ }
2141
+ ),
2142
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2143
+ "div",
2144
+ {
2145
+ style: {
2146
+ display: "flex",
2147
+ alignItems: "center",
2148
+ gap: "8px",
2149
+ flexWrap: "wrap",
2150
+ marginBottom: "8px"
2151
+ },
2152
+ children: [
2153
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2154
+ "span",
2155
+ {
2156
+ style: {
2157
+ fontSize: "12px",
2158
+ color: "#10b981",
2159
+ background: "rgba(16,185,129,0.12)",
2160
+ padding: "2px 8px",
2161
+ borderRadius: "12px",
2162
+ fontWeight: "500"
2163
+ },
2164
+ children: lang === "en" ? `\u23F1\uFE0F IP Rate Limit: ${status.maxVisitsPerMinute || 60}/min` : `\u23F1\uFE0F \u5355 IP \u9650\u9891 ${status.maxVisitsPerMinute || 60}\u6B21/\u5206\u949F`
2165
+ }
2166
+ ),
2167
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2168
+ "span",
2169
+ {
2170
+ style: {
2171
+ fontSize: "12px",
2172
+ color: "#10b981",
2173
+ background: "rgba(16,185,129,0.12)",
2174
+ padding: "2px 8px",
2175
+ borderRadius: "12px",
2176
+ fontWeight: "500"
2177
+ },
2178
+ children: lang === "en" ? `\u{1F6E1}\uFE0F Lockout: ${status.maxFailedAttempts || 5} fails / ${Math.round(
2179
+ (status.lockDurationMs || 9e5) / 6e4
2180
+ )} mins` : `\u{1F6E1}\uFE0F \u8FDE\u7EED ${status.maxFailedAttempts || 5} \u6B21\u9519\u8BEF\u5C01\u9501 ${Math.round(
2181
+ (status.lockDurationMs || 9e5) / 6e4
2182
+ )} \u5206\u949F`
2183
+ }
2184
+ )
2185
+ ]
2186
+ }
2187
+ ),
2188
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2189
+ "div",
2190
+ {
2191
+ style: {
2192
+ fontSize: "12px",
2193
+ color: "var(--dsw-alias-label-tertiary, #888)",
2194
+ marginBottom: "12px",
2195
+ lineHeight: "1.5"
2196
+ },
2197
+ children: t("securityCardDesc", lang)
2198
+ }
2199
+ ),
2200
+ 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) => {
2201
+ const isLocked = stat.lockedUntil && stat.lockedUntil > Date.now();
2202
+ const remMin = isLocked ? Math.ceil((stat.lockedUntil - Date.now()) / 6e4) : 0;
2203
+ const matchedDevice = status.devices && Array.isArray(status.devices) ? status.devices.find((d) => d.ip === stat.ip) : null;
2204
+ let effectiveAuthType = stat.authType;
2205
+ if (!effectiveAuthType || effectiveAuthType === "\u26AA \u5F85\u8BA4\u8BC1" || effectiveAuthType === "\u26AA Pending") {
2206
+ if (matchedDevice) {
2207
+ if (matchedDevice.authType && matchedDevice.authType.indexOf("\u514D\u5BC6") !== -1) {
2208
+ effectiveAuthType = lang === "en" ? "\u26A1 Bypass Access" : matchedDevice.authType;
2209
+ } else if (matchedDevice.authType && matchedDevice.authType.indexOf("\u5BC6\u7801") !== -1) {
2210
+ effectiveAuthType = lang === "en" ? "\u{1F511} Password Auth" : "\u{1F511} \u957F\u671F\u5BC6\u7801\u8BA4\u8BC1";
2211
+ } else {
2212
+ effectiveAuthType = lang === "en" ? "\u{1F4F1} QR Pairing Auth" : "\u{1F4F1} \u626B\u7801\u914D\u5BF9\u8BA4\u8BC1";
2213
+ }
2214
+ }
2215
+ }
2216
+ let effectiveDeviceName = stat.deviceName;
2217
+ if (!effectiveDeviceName && matchedDevice && matchedDevice.deviceName) {
2218
+ effectiveDeviceName = matchedDevice.deviceName;
2219
+ }
2220
+ const isLocal = isCurrentLocalIp2(stat.ip, status);
2221
+ const ipLabel = `${stat.ip}${isLocal ? lang === "en" ? " (Local)" : " (\u672C\u673A)" : ""}`;
2222
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2223
+ "div",
2224
+ {
2225
+ style: {
2226
+ display: "flex",
2227
+ alignItems: "center",
2228
+ justifyContent: "space-between",
2229
+ gap: "12px",
2230
+ padding: "10px 14px",
2231
+ background: isLocked ? "rgba(239,68,68,0.08)" : "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.08))",
2232
+ border: isLocked ? "1px solid rgba(239,68,68,0.3)" : "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.1))",
2233
+ borderRadius: "8px"
2234
+ },
2235
+ children: [
2236
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2237
+ "div",
2238
+ {
2239
+ style: {
2240
+ display: "flex",
2241
+ flexDirection: "column",
2242
+ gap: "3px",
2243
+ minWidth: 0,
2244
+ flex: 1
2245
+ },
2246
+ children: [
2247
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2248
+ "div",
2249
+ {
2250
+ style: {
2251
+ fontSize: "13px",
2252
+ fontWeight: "600",
2253
+ color: "var(--dsw-alias-label-primary, inherit)",
2254
+ display: "flex",
2255
+ alignItems: "center",
2256
+ gap: "8px",
2257
+ flexWrap: "wrap"
2258
+ },
2259
+ children: [
2260
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { display: "inline-flex", alignItems: "center", gap: "6px" }, children: [
2261
+ effectiveDeviceName ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color: "var(--dsw-alias-label-primary, inherit)" }, children: effectiveDeviceName }) : null,
2262
+ effectiveDeviceName ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color: "var(--dsw-alias-label-tertiary, #888)", fontWeight: "normal" }, children: "\xB7" }) : null,
2263
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { color: effectiveDeviceName ? "var(--dsw-alias-label-secondary, #888)" : "inherit" }, children: ipLabel })
2264
+ ] }),
2265
+ effectiveAuthType ? (() => {
2266
+ let bg = "rgba(148,163,184,0.15)";
2267
+ let color = "#94a3b8";
2268
+ if (effectiveAuthType.indexOf("\u514D\u5BC6") !== -1 || effectiveAuthType.indexOf("Bypass") !== -1) {
2269
+ bg = "rgba(16,185,129,0.15)";
2270
+ color = "#10b981";
2271
+ } else if (effectiveAuthType.indexOf("\u626B\u7801") !== -1 || effectiveAuthType.indexOf("Pairing") !== -1) {
2272
+ bg = "rgba(59,130,246,0.15)";
2273
+ color = "#3b82f6";
2274
+ } else if (effectiveAuthType.indexOf("\u5BC6\u7801") !== -1 || effectiveAuthType.indexOf("Password") !== -1) {
2275
+ bg = "rgba(139,92,246,0.15)";
2276
+ color = "#a78bfa";
2277
+ }
2278
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2279
+ "span",
2280
+ {
2281
+ style: {
2282
+ fontSize: "11px",
2283
+ padding: "1px 6px",
2284
+ borderRadius: "4px",
2285
+ background: bg,
2286
+ color,
2287
+ fontWeight: "600",
2288
+ whiteSpace: "nowrap"
2289
+ },
2290
+ children: effectiveAuthType
2291
+ }
2292
+ );
2293
+ })() : null,
2294
+ isLocked ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2295
+ "span",
2296
+ {
2297
+ style: {
2298
+ fontSize: "11px",
2299
+ padding: "1px 6px",
2300
+ borderRadius: "4px",
2301
+ background: "rgba(239,68,68,0.2)",
2302
+ color: "#ef4444",
2303
+ fontWeight: "700",
2304
+ whiteSpace: "nowrap"
2305
+ },
2306
+ children: lang === "en" ? `\u{1F6AB} Locked (${remMin}m left)` : `\u{1F6AB} \u5DF2\u88AB\u9501\u5B9A (\u8FD8\u5269 ${remMin} \u5206\u949F)`
2307
+ }
2308
+ ) : stat.failedAttempts > 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2309
+ "span",
2310
+ {
2311
+ style: {
2312
+ fontSize: "11px",
2313
+ padding: "1px 6px",
2314
+ borderRadius: "4px",
2315
+ background: "rgba(245,158,11,0.2)",
2316
+ color: "#f59e0b",
2317
+ fontWeight: "600",
2318
+ whiteSpace: "nowrap"
2319
+ },
2320
+ children: lang === "en" ? `\u26A0\uFE0F ${stat.failedAttempts} failed attempts` : `\u26A0\uFE0F \u66FE\u5931\u8D25 ${stat.failedAttempts} \u6B21`
2321
+ }
2322
+ ) : !effectiveAuthType ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2323
+ "span",
2324
+ {
2325
+ style: {
2326
+ fontSize: "11px",
2327
+ padding: "1px 6px",
2328
+ borderRadius: "4px",
2329
+ background: "rgba(148,163,184,0.15)",
2330
+ color: "#94a3b8",
2331
+ fontWeight: "600",
2332
+ whiteSpace: "nowrap"
2333
+ },
2334
+ children: lang === "en" ? "\u26AA Pending" : "\u26AA \u5F85\u8BA4\u8BC1"
2335
+ }
2336
+ ) : null
2337
+ ]
2338
+ }
2339
+ ),
2340
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2341
+ "div",
2342
+ {
2343
+ style: {
2344
+ fontSize: "11px",
2345
+ color: "var(--dsw-alias-label-tertiary, #888)",
2346
+ lineHeight: "1.4",
2347
+ wordBreak: "break-word"
2348
+ },
2349
+ 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)}`
2350
+ }
2351
+ )
2352
+ ]
2353
+ }
2354
+ ),
2355
+ isLocked ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2356
+ "button",
2357
+ {
2358
+ type: "button",
2359
+ onClick: () => unlockIp(stat.ip),
2360
+ style: {
2361
+ padding: "5px 12px",
2362
+ background: "rgba(59,130,246,0.12)",
2363
+ border: "1px solid rgba(59,130,246,0.3)",
2364
+ color: "var(--dsw-alias-brand-primary, #3b82f6)",
2365
+ borderRadius: "6px",
2366
+ fontSize: "12px",
2367
+ fontWeight: "600",
2368
+ cursor: "pointer",
2369
+ whiteSpace: "nowrap",
2370
+ flexShrink: 0
2371
+ },
2372
+ children: t("unlockIpBtn", lang)
2373
+ }
2374
+ ) : null
2375
+ ]
2376
+ },
2377
+ stat.ip
2378
+ );
2379
+ }) }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2380
+ "div",
2381
+ {
2382
+ style: {
2383
+ textAlign: "center",
2384
+ padding: "14px",
2385
+ color: "var(--dsw-alias-label-tertiary, #888)",
2386
+ fontSize: "13px"
2387
+ },
2388
+ children: t("securityNormal", lang)
2389
+ }
2390
+ )
2391
+ ]
2392
+ }
2393
+ );
2394
+ }
2395
+
2396
+ // src/client/components/StorageCard.jsx
2397
+ var import_react6 = __toESM(require("react"), 1);
2398
+ var import_jsx_runtime6 = require("react/jsx-runtime");
2399
+ function StorageCard({ status, lang }) {
2400
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2401
+ "div",
2402
+ {
2403
+ style: {
2404
+ background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.04))",
2405
+ border: "1px dashed var(--dsw-alias-border-l2, rgba(128,128,128,0.2))",
2406
+ borderRadius: "10px",
2407
+ padding: "14px 16px",
2408
+ display: "flex",
2409
+ flexDirection: "column",
2410
+ gap: "10px"
2411
+ },
2412
+ children: [
2413
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2414
+ "div",
2415
+ {
2416
+ style: {
2417
+ display: "flex",
2418
+ alignItems: "center",
2419
+ justifyContent: "space-between",
2420
+ gap: "12px"
2421
+ },
2422
+ children: [
2423
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "10px", minWidth: 0 }, children: [
2424
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: { fontSize: "18px" }, children: "\u{1F4C1}" }),
2425
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "2px", minWidth: 0 }, children: [
2426
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2427
+ "div",
2428
+ {
2429
+ style: {
2430
+ fontSize: "12px",
2431
+ fontWeight: "600",
2432
+ color: "var(--dsw-alias-label-primary, inherit)"
2433
+ },
2434
+ children: t("storageDeviceFile", lang)
2435
+ }
2436
+ ),
2437
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2438
+ "code",
2439
+ {
2440
+ style: {
2441
+ fontSize: "11px",
2442
+ color: "var(--dsw-alias-brand-primary, #3b82f6)",
2443
+ fontFamily: "monospace",
2444
+ wordBreak: "break-all"
2445
+ },
2446
+ children: status.persistPath || "~/.dsh/remote-mobile/devices.json"
2447
+ }
2448
+ )
2449
+ ] })
2450
+ ] }),
2451
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2452
+ "button",
2453
+ {
2454
+ type: "button",
2455
+ onClick: () => {
2456
+ const p = status.persistPath || "~/.dsh/remote-mobile/devices.json";
2457
+ navigator.clipboard.writeText(p).then(() => {
2458
+ alert((lang === "en" ? "Copied path:\n" : "\u5B58\u50A8\u6587\u4EF6\u8DEF\u5F84\u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F\uFF01\n") + p);
2459
+ });
2460
+ },
2461
+ style: {
2462
+ padding: "4px 10px",
2463
+ background: "var(--dsw-alias-bg-layer-3, rgba(128,128,128,0.1))",
2464
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.2))",
2465
+ color: "var(--dsw-alias-label-secondary, inherit)",
2466
+ borderRadius: "6px",
2467
+ fontSize: "11px",
2468
+ cursor: "pointer",
2469
+ whiteSpace: "nowrap"
2470
+ },
2471
+ children: t("copyUrlBtn", lang)
2472
+ }
2473
+ )
2474
+ ]
2475
+ }
2476
+ ),
2477
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2478
+ "div",
2479
+ {
2480
+ style: {
2481
+ display: "flex",
2482
+ alignItems: "center",
2483
+ justifyContent: "space-between",
2484
+ gap: "12px",
2485
+ paddingTop: "8px",
2486
+ borderTop: "1px dashed var(--dsw-alias-border-l2, rgba(128,128,128,0.15))"
2487
+ },
2488
+ children: [
2489
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "10px", minWidth: 0 }, children: [
2490
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: { fontSize: "18px" }, children: "\u{1F511}" }),
2491
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "2px", minWidth: 0 }, children: [
2492
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2493
+ "div",
2494
+ {
2495
+ style: {
2496
+ fontSize: "12px",
2497
+ fontWeight: "600",
2498
+ color: "var(--dsw-alias-label-primary, inherit)"
2499
+ },
2500
+ children: t("storageRsaFile", lang)
2501
+ }
2502
+ ),
2503
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2504
+ "code",
2505
+ {
2506
+ style: {
2507
+ fontSize: "11px",
2508
+ color: "var(--dsw-alias-brand-primary, #3b82f6)",
2509
+ fontFamily: "monospace",
2510
+ wordBreak: "break-all"
2511
+ },
2512
+ children: status.rsaKeyPath || "~/.dsh/remote-mobile/rsa-keys.json"
2513
+ }
2514
+ )
2515
+ ] })
2516
+ ] }),
2517
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2518
+ "button",
2519
+ {
2520
+ type: "button",
2521
+ onClick: () => {
2522
+ const p = status.rsaKeyPath || "~/.dsh/remote-mobile/rsa-keys.json";
2523
+ navigator.clipboard.writeText(p).then(() => {
2524
+ alert((lang === "en" ? "Copied RSA path:\n" : "RSA \u5BC6\u94A5\u6587\u4EF6\u8DEF\u5F84\u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F\uFF01\n") + p);
2525
+ });
2526
+ },
2527
+ style: {
2528
+ padding: "4px 10px",
2529
+ background: "var(--dsw-alias-bg-layer-3, rgba(128,128,128,0.1))",
2530
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.2))",
2531
+ color: "var(--dsw-alias-label-secondary, inherit)",
2532
+ borderRadius: "6px",
2533
+ fontSize: "11px",
2534
+ cursor: "pointer",
2535
+ whiteSpace: "nowrap"
2536
+ },
2537
+ children: t("copyUrlBtn", lang)
2538
+ }
2539
+ )
2540
+ ]
2541
+ }
2542
+ )
2543
+ ]
2544
+ }
2545
+ );
2546
+ }
2547
+
2548
+ // src/client/components/ConfigCard.jsx
2549
+ var import_react7 = __toESM(require("react"), 1);
2550
+ var import_jsx_runtime7 = require("react/jsx-runtime");
2551
+ function ConfigCard({
2552
+ maxVisitsInput,
2553
+ setMaxVisitsInput,
2554
+ maxFailedInput,
2555
+ setMaxFailedInput,
2556
+ lockDurationMinsInput,
2557
+ setLockDurationMinsInput,
2558
+ saveAdvancedConfig,
2559
+ resetAdvancedConfigDefaults,
2560
+ isSaving,
2561
+ lang
2562
+ }) {
2563
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2564
+ "div",
2565
+ {
2566
+ style: {
2567
+ background: "var(--dsw-alias-bg-layer-3, rgba(128,128,128,0.06))",
2568
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.15))",
2569
+ borderRadius: "12px",
2570
+ padding: "18px 20px",
2571
+ display: "flex",
2572
+ flexDirection: "column",
2573
+ gap: "14px"
2574
+ },
2575
+ children: [
2576
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2577
+ "div",
2578
+ {
2579
+ style: {
2580
+ display: "flex",
2581
+ alignItems: "center",
2582
+ justifyContent: "space-between",
2583
+ gap: "10px"
2584
+ },
2585
+ children: [
2586
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
2587
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { fontSize: "16px" }, children: "\u2699\uFE0F" }),
2588
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2589
+ "span",
2590
+ {
2591
+ style: {
2592
+ fontSize: "15px",
2593
+ fontWeight: "600",
2594
+ color: "var(--dsw-alias-label-primary, inherit)"
2595
+ },
2596
+ children: t("configCardTitle", lang)
2597
+ }
2598
+ )
2599
+ ] }),
2600
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2601
+ "button",
2602
+ {
2603
+ type: "button",
2604
+ onClick: () => {
2605
+ navigator.clipboard.writeText("~/.dsh/settings.yaml").then(() => {
2606
+ alert(
2607
+ lang === "en" ? "Copied settings path:\n~/.dsh/settings.yaml (namespace: dsh-remote-mobile)" : "\u5DF2\u590D\u5236\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84\uFF1A\n~/.dsh/settings.yaml (\u547D\u540D\u7A7A\u95F4: dsh-remote-mobile)"
2608
+ );
2609
+ });
2610
+ },
2611
+ style: {
2612
+ padding: "4px 10px",
2613
+ background: "var(--dsw-alias-bg-layer-3, rgba(128,128,128,0.1))",
2614
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.2))",
2615
+ color: "var(--dsw-alias-label-secondary, inherit)",
2616
+ borderRadius: "6px",
2617
+ fontSize: "11px",
2618
+ cursor: "pointer",
2619
+ whiteSpace: "nowrap",
2620
+ flexShrink: 0
2621
+ },
2622
+ children: t("copySettingsPathBtn", lang)
2623
+ }
2624
+ )
2625
+ ]
2626
+ }
2627
+ ),
2628
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2629
+ "div",
2630
+ {
2631
+ style: {
2632
+ fontSize: "12px",
2633
+ color: "var(--dsw-alias-label-tertiary, #888)",
2634
+ lineHeight: "1.5"
2635
+ },
2636
+ children: t("configCardDesc", lang)
2637
+ }
2638
+ ),
2639
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2640
+ "div",
2641
+ {
2642
+ style: {
2643
+ display: "grid",
2644
+ gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
2645
+ gap: "12px"
2646
+ },
2647
+ children: [
2648
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2649
+ "div",
2650
+ {
2651
+ style: {
2652
+ display: "flex",
2653
+ flexDirection: "column",
2654
+ gap: "6px",
2655
+ background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.06))",
2656
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.12))",
2657
+ borderRadius: "8px",
2658
+ padding: "10px 12px"
2659
+ },
2660
+ children: [
2661
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2662
+ "label",
2663
+ {
2664
+ style: {
2665
+ fontSize: "12px",
2666
+ fontWeight: "600",
2667
+ color: "var(--dsw-alias-label-primary, inherit)"
2668
+ },
2669
+ children: t("fieldVisitsLimit", lang)
2670
+ }
2671
+ ),
2672
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2673
+ "input",
2674
+ {
2675
+ type: "number",
2676
+ min: "1",
2677
+ max: "1000",
2678
+ value: maxVisitsInput,
2679
+ onChange: (e) => setMaxVisitsInput(e.target.value),
2680
+ placeholder: "60",
2681
+ style: {
2682
+ height: "34px",
2683
+ background: "var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.15))",
2684
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))",
2685
+ borderRadius: "6px",
2686
+ padding: "0 10px",
2687
+ color: "inherit",
2688
+ fontSize: "13px",
2689
+ outline: "none"
2690
+ }
2691
+ }
2692
+ ),
2693
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { fontSize: "11px", color: "var(--dsw-alias-label-tertiary, #888)" }, children: t("fieldVisitsLimitHint", lang) })
2694
+ ]
2695
+ }
2696
+ ),
2697
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2698
+ "div",
2699
+ {
2700
+ style: {
2701
+ display: "flex",
2702
+ flexDirection: "column",
2703
+ gap: "6px",
2704
+ background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.06))",
2705
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.12))",
2706
+ borderRadius: "8px",
2707
+ padding: "10px 12px"
2708
+ },
2709
+ children: [
2710
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2711
+ "label",
2712
+ {
2713
+ style: {
2714
+ fontSize: "12px",
2715
+ fontWeight: "600",
2716
+ color: "var(--dsw-alias-label-primary, inherit)"
2717
+ },
2718
+ children: t("fieldFailedLimit", lang)
2719
+ }
2720
+ ),
2721
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2722
+ "input",
2723
+ {
2724
+ type: "number",
2725
+ min: "1",
2726
+ max: "100",
2727
+ value: maxFailedInput,
2728
+ onChange: (e) => setMaxFailedInput(e.target.value),
2729
+ placeholder: "5",
2730
+ style: {
2731
+ height: "34px",
2732
+ background: "var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.15))",
2733
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))",
2734
+ borderRadius: "6px",
2735
+ padding: "0 10px",
2736
+ color: "inherit",
2737
+ fontSize: "13px",
2738
+ outline: "none"
2739
+ }
2740
+ }
2741
+ ),
2742
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { fontSize: "11px", color: "var(--dsw-alias-label-tertiary, #888)" }, children: t("fieldFailedLimitHint", lang) })
2743
+ ]
2744
+ }
2745
+ ),
2746
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2747
+ "div",
2748
+ {
2749
+ style: {
2750
+ display: "flex",
2751
+ flexDirection: "column",
2752
+ gap: "6px",
2753
+ background: "var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.06))",
2754
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.12))",
2755
+ borderRadius: "8px",
2756
+ padding: "10px 12px"
2757
+ },
2758
+ children: [
2759
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2760
+ "label",
2761
+ {
2762
+ style: {
2763
+ fontSize: "12px",
2764
+ fontWeight: "600",
2765
+ color: "var(--dsw-alias-label-primary, inherit)"
2766
+ },
2767
+ children: t("fieldLockDuration", lang)
2768
+ }
2769
+ ),
2770
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2771
+ "input",
2772
+ {
2773
+ type: "number",
2774
+ min: "1",
2775
+ max: "1440",
2776
+ value: lockDurationMinsInput,
2777
+ onChange: (e) => setLockDurationMinsInput(e.target.value),
2778
+ placeholder: "15",
2779
+ style: {
2780
+ height: "34px",
2781
+ background: "var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.15))",
2782
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))",
2783
+ borderRadius: "6px",
2784
+ padding: "0 10px",
2785
+ color: "inherit",
2786
+ fontSize: "13px",
2787
+ outline: "none"
2788
+ }
2789
+ }
2790
+ ),
2791
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { fontSize: "11px", color: "var(--dsw-alias-label-tertiary, #888)" }, children: t("fieldLockDurationHint", lang) })
2792
+ ]
2793
+ }
2794
+ )
2795
+ ]
2796
+ }
2797
+ ),
2798
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2799
+ "div",
2800
+ {
2801
+ style: {
2802
+ display: "flex",
2803
+ alignItems: "center",
2804
+ justifyContent: "flex-end",
2805
+ gap: "10px",
2806
+ marginTop: "4px"
2807
+ },
2808
+ children: [
2809
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2810
+ "button",
2811
+ {
2812
+ type: "button",
2813
+ onClick: resetAdvancedConfigDefaults,
2814
+ disabled: isSaving,
2815
+ style: {
2816
+ padding: "6px 14px",
2817
+ background: "transparent",
2818
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))",
2819
+ color: "var(--dsw-alias-label-secondary, inherit)",
2820
+ borderRadius: "6px",
2821
+ fontSize: "12px",
2822
+ cursor: isSaving ? "not-allowed" : "pointer",
2823
+ opacity: isSaving ? 0.6 : 1,
2824
+ whiteSpace: "nowrap"
2825
+ },
2826
+ children: t("restoreDefaultsBtn", lang)
2827
+ }
2828
+ ),
2829
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2830
+ "button",
2831
+ {
2832
+ type: "button",
2833
+ onClick: saveAdvancedConfig,
2834
+ disabled: isSaving,
2835
+ style: {
2836
+ padding: "6px 18px",
2837
+ background: "var(--dsw-alias-brand-primary, #3b82f6)",
2838
+ border: "none",
2839
+ color: "white",
2840
+ borderRadius: "6px",
2841
+ fontSize: "12px",
2842
+ fontWeight: "600",
2843
+ cursor: isSaving ? "not-allowed" : "pointer",
2844
+ opacity: isSaving ? 0.7 : 1,
2845
+ whiteSpace: "nowrap"
2846
+ },
2847
+ children: isSaving ? t("savingConfigBtn", lang) : t("saveConfigBtn", lang)
2848
+ }
2849
+ )
2850
+ ]
2851
+ }
2852
+ )
2853
+ ]
2854
+ }
2855
+ );
2856
+ }
2857
+
2858
+ // src/client/version.js
2859
+ var PLUGIN_VERSION = "v1.1.0";
2860
+
2861
+ // src/client/components/MainSection.jsx
2862
+ var import_jsx_runtime8 = require("react/jsx-runtime");
2863
+ function TailscaleMobileSection(props) {
2864
+ const ctx = props && props.ctx || typeof window !== "undefined" && window.__DSH_CLIENT_CTX__ || null;
2865
+ const [status, setStatus] = (0, import_react8.useState)({
2866
+ tailscaleIp: "",
2867
+ lanIp: "",
2868
+ code: "",
2869
+ expiresAt: 0,
2870
+ hasSecret: false,
2871
+ allowTailscale: false,
2872
+ allowLan: false,
2873
+ maxVisitsPerMinute: 60,
2874
+ maxFailedAttempts: 5,
2875
+ lockDurationMs: 9e5,
2876
+ devicesCount: 0,
2877
+ devices: [],
2878
+ ipSecurityStats: [],
2879
+ copied: false,
2880
+ loading: true,
2881
+ persistPath: "",
2882
+ locale: "zh"
2883
+ });
2884
+ const [secretInput, setSecretInput] = (0, import_react8.useState)("");
2885
+ const [showSecret, setShowSecret] = (0, import_react8.useState)(false);
2886
+ const [showCode, setShowCode] = (0, import_react8.useState)(false);
2887
+ const [showQr, setShowQr] = (0, import_react8.useState)(false);
2888
+ const [selectedTab, setSelectedTab] = (0, import_react8.useState)("lan");
2889
+ const [timeLeft, setTimeLeft] = (0, import_react8.useState)(300);
2890
+ const [toast, setToast] = (0, import_react8.useState)(null);
2891
+ const [maxVisitsInput, setMaxVisitsInput] = (0, import_react8.useState)("60");
2892
+ const [maxFailedInput, setMaxFailedInput] = (0, import_react8.useState)("5");
2893
+ const [lockDurationMinsInput, setLockDurationMinsInput] = (0, import_react8.useState)("15");
2894
+ const [isSaving, setIsSaving] = (0, import_react8.useState)(false);
2895
+ const [currentLang, setCurrentLang] = (0, import_react8.useState)(resolveLocale(ctx, status));
2896
+ const refreshStatusOnly = (0, import_react8.useCallback)(() => {
2897
+ fetchStatus().then((data) => {
2898
+ if (data.tailscaleIp) {
2899
+ setSelectedTab("tailscale");
2900
+ }
2901
+ if (data.maxVisitsPerMinute) setMaxVisitsInput(String(data.maxVisitsPerMinute));
2902
+ if (data.maxFailedAttempts) setMaxFailedInput(String(data.maxFailedAttempts));
2903
+ if (data.lockDurationMs)
2904
+ setLockDurationMinsInput(String(Math.round(data.lockDurationMs / 6e4)));
2905
+ if (data.locale) {
2906
+ setCurrentLang(resolveLocale(ctx, data));
2907
+ }
2908
+ setStatus((prev) => ({
2909
+ ...prev,
2910
+ tailscaleIp: data.tailscaleIp || "",
2911
+ lanIp: data.lanIp || "",
2912
+ hasSecret: data.hasSecret,
2913
+ allowTailscale: data.allowTailscale,
2914
+ allowLan: data.allowLan,
2915
+ maxVisitsPerMinute: data.maxVisitsPerMinute || 60,
2916
+ maxFailedAttempts: data.maxFailedAttempts || 5,
2917
+ lockDurationMs: data.lockDurationMs || 9e5,
2918
+ devicesCount: data.devicesCount || 0,
2919
+ devices: data.devices || [],
2920
+ ipSecurityStats: data.ipSecurityStats || [],
2921
+ persistPath: data.persistPath || "",
2922
+ rsaKeyPath: data.rsaKeyPath || "",
2923
+ locale: data.locale || "zh",
2924
+ loading: false
2925
+ }));
2926
+ }).catch(() => {
2927
+ });
2928
+ }, [ctx]);
2929
+ const refreshStatusAndCode = (0, import_react8.useCallback)(() => {
2930
+ refreshStatusOnly();
2931
+ generatePairCode().then((data) => {
2932
+ if (data.success) {
2933
+ setStatus((prev) => ({
2934
+ ...prev,
2935
+ code: data.code,
2936
+ expiresAt: data.expiresAt
2937
+ }));
2938
+ const remain = Math.max(0, Math.floor((data.expiresAt - Date.now()) / 1e3));
2939
+ setTimeLeft(remain);
2940
+ }
2941
+ }).catch(() => {
2942
+ });
2943
+ }, [refreshStatusOnly]);
2944
+ (0, import_react8.useEffect)(() => {
2945
+ refreshStatusAndCode();
2946
+ const localeUnsubs = [];
2947
+ try {
2948
+ const loc = ctx && (typeof ctx.get === "function" ? ctx.get("locale") : ctx.locale);
2949
+ if (loc && typeof loc.subscribe === "function") {
2950
+ const unsub = loc.subscribe(() => {
2951
+ setCurrentLang(resolveLocale(ctx, status));
2952
+ });
2953
+ if (typeof unsub === "function") localeUnsubs.push(unsub);
2954
+ }
2955
+ } catch (e) {
2956
+ }
2957
+ const handleDeviceUpdate = (e) => {
2958
+ if (!e || !e.detail) return;
2959
+ const detail = e.detail;
2960
+ if (detail.stats && Array.isArray(detail.stats)) {
2961
+ setStatus((prev) => ({ ...prev, ipSecurityStats: detail.stats }));
2962
+ }
2963
+ if (detail.device) {
2964
+ const dev = detail.device;
2965
+ setStatus((prev) => {
2966
+ const prevList = prev.devices || [];
2967
+ const nextList = [dev].concat(prevList.filter((d) => d.token !== dev.token));
2968
+ return {
2969
+ ...prev,
2970
+ devicesCount: nextList.length,
2971
+ devices: nextList
2972
+ };
2973
+ });
2974
+ const msg = detail.type === "device-connected" ? t("toastDeviceConnected", currentLang, {
2975
+ name: dev.deviceName || "Device",
2976
+ ip: dev.ip
2977
+ }) : t("toastDeviceOnline", currentLang, {
2978
+ name: dev.deviceName || "Device",
2979
+ ip: dev.ip
2980
+ });
2981
+ setToast({ message: msg, type: "info" });
2982
+ setTimeout(() => setToast(null), 4e3);
2983
+ }
2984
+ if (detail.type === "device-revoked") {
2985
+ setStatus((prev) => {
2986
+ const prevList = prev.devices || [];
2987
+ const nextList = detail.all ? [] : prevList.filter((d) => d.token !== detail.token);
2988
+ return {
2989
+ ...prev,
2990
+ devicesCount: nextList.length,
2991
+ devices: nextList
2992
+ };
2993
+ });
2994
+ }
2995
+ if (detail.type === "ip-security-alert") {
2996
+ const alertMsg = t("toastIpLocked", currentLang, { ip: detail.ip });
2997
+ setToast({ message: alertMsg, type: "danger" });
2998
+ setTimeout(() => setToast(null), 5e3);
2999
+ }
3000
+ refreshStatusOnly();
3001
+ };
3002
+ window.addEventListener("dsh-device-updated", handleDeviceUpdate);
3003
+ const pollTimer = setInterval(() => {
3004
+ refreshStatusOnly();
3005
+ }, 3e3);
3006
+ return () => {
3007
+ clearInterval(pollTimer);
3008
+ window.removeEventListener("dsh-device-updated", handleDeviceUpdate);
3009
+ for (let i = 0; i < localeUnsubs.length; i++) {
3010
+ localeUnsubs[i]();
3011
+ }
3012
+ };
3013
+ }, [refreshStatusAndCode, refreshStatusOnly, currentLang, ctx]);
3014
+ (0, import_react8.useEffect)(() => {
3015
+ if (timeLeft <= 0) return;
3016
+ const timer = setInterval(() => {
3017
+ setTimeLeft((prev) => {
3018
+ if (prev <= 1) {
3019
+ clearInterval(timer);
3020
+ return 0;
3021
+ }
3022
+ return prev - 1;
3023
+ });
3024
+ }, 1e3);
3025
+ return () => clearInterval(timer);
3026
+ }, [timeLeft]);
3027
+ const toggleTailscale = () => {
3028
+ const nextVal = !status.allowTailscale;
3029
+ updateBypassConfig(nextVal, status.allowLan).then(() => {
3030
+ setStatus((prev) => ({ ...prev, allowTailscale: nextVal }));
3031
+ });
3032
+ };
3033
+ const toggleLan = () => {
3034
+ const nextVal = !status.allowLan;
3035
+ if (nextVal) {
3036
+ const warnMsg = t("lanBypassConfirm", currentLang);
3037
+ if (!confirm(warnMsg)) return;
3038
+ }
3039
+ updateBypassConfig(status.allowTailscale, nextVal).then(() => {
3040
+ setStatus((prev) => ({ ...prev, allowLan: nextVal }));
3041
+ });
3042
+ };
3043
+ const saveSecret = () => {
3044
+ const trimmed = (secretInput || "").trim();
3045
+ if (!trimmed) {
3046
+ alert(t("secretInputPlaceholder", currentLang));
3047
+ return;
3048
+ }
3049
+ const hasLetter = /[a-zA-Z]/.test(trimmed);
3050
+ const hasNumber = /[0-9]/.test(trimmed);
3051
+ if (trimmed.length < 6 || !hasLetter || !hasNumber) {
3052
+ alert(t("secretWeakTip", currentLang));
3053
+ return;
3054
+ }
3055
+ updateSecret(trimmed).then((data) => {
3056
+ if (data.success) {
3057
+ setStatus((prev) => ({ ...prev, hasSecret: true }));
3058
+ setSecretInput("");
3059
+ setToast({
3060
+ message: currentLang === "en" ? "Persistent password saved!" : "\u957F\u671F\u8BBF\u95EE\u5BC6\u7801\u5DF2\u6210\u529F\u4FDD\u5B58\u5E76\u542F\u7528\uFF01",
3061
+ type: "success"
3062
+ });
3063
+ setTimeout(() => setToast(null), 3e3);
3064
+ } else {
3065
+ alert((currentLang === "en" ? "Save failed: " : "\u4FDD\u5B58\u5931\u8D25\uFF1A") + (data.reason || ""));
3066
+ }
3067
+ });
3068
+ };
3069
+ const clearSecret = () => {
3070
+ if (confirm(t("clearSecretConfirm", currentLang))) {
3071
+ clearSecretApi().then(() => {
3072
+ setStatus((prev) => ({ ...prev, hasSecret: false }));
3073
+ setSecretInput("");
3074
+ setToast({
3075
+ message: currentLang === "en" ? "Password cleared!" : "\u5DF2\u6210\u529F\u6E05\u9664\u957F\u671F\u8BBF\u95EE\u5BC6\u7801\uFF01",
3076
+ type: "info"
3077
+ });
3078
+ setTimeout(() => setToast(null), 3e3);
3079
+ });
3080
+ }
3081
+ };
3082
+ const revokeDevice = (token) => {
3083
+ if (confirm(t("revokeDeviceConfirm", currentLang))) {
3084
+ revokeDeviceApi(token).then(() => {
3085
+ refreshStatusAndCode();
3086
+ setToast({
3087
+ message: currentLang === "en" ? "Device disconnected!" : "\u5DF2\u6210\u529F\u8E22\u51FA\u8BE5\u8BBE\u5907\uFF01",
3088
+ type: "info"
3089
+ });
3090
+ setTimeout(() => setToast(null), 2500);
3091
+ });
3092
+ }
3093
+ };
3094
+ const revokeAll = () => {
3095
+ if (confirm(t("revokeAllConfirm", currentLang))) {
3096
+ revokeAllDevicesApi().then(() => {
3097
+ refreshStatusAndCode();
3098
+ setToast({
3099
+ message: currentLang === "en" ? "All devices disconnected!" : "\u5DF2\u6210\u529F\u6CE8\u9500\u5E76\u8E22\u4E0B\u7EBF\u6240\u6709\u8BBE\u5907\uFF01",
3100
+ type: "info"
3101
+ });
3102
+ setTimeout(() => setToast(null), 2500);
3103
+ });
3104
+ }
3105
+ };
3106
+ const unlockIp = (ip) => {
3107
+ if (confirm(t("unlockIpConfirm", currentLang, { ip }))) {
3108
+ unlockIpApi(ip).then(() => {
3109
+ refreshStatusAndCode();
3110
+ setToast({
3111
+ message: currentLang === "en" ? `IP ${ip} unlocked!` : `\u5DF2\u6210\u529F\u4E3A IP ${ip} \u89E3\u9664\u9501\u5B9A\uFF01`,
3112
+ type: "success"
3113
+ });
3114
+ setTimeout(() => setToast(null), 3e3);
3115
+ }).catch(() => {
3116
+ });
3117
+ }
3118
+ };
3119
+ const clearIpStats = () => {
3120
+ if (confirm(t("clearSecurityLogConfirm", currentLang))) {
3121
+ clearIpStatsApi().then(() => {
3122
+ refreshStatusAndCode();
3123
+ setToast({
3124
+ message: currentLang === "en" ? "Audit logs cleared!" : "\u5DF2\u6210\u529F\u6E05\u7A7A\u6240\u6709\u5B89\u5168\u5BA1\u8BA1\u65E5\u5FD7\uFF01",
3125
+ type: "success"
3126
+ });
3127
+ setTimeout(() => setToast(null), 2500);
3128
+ }).catch(() => {
3129
+ });
3130
+ }
3131
+ };
3132
+ const saveAdvancedConfig = () => {
3133
+ const visits = parseInt(maxVisitsInput, 10);
3134
+ const failed = parseInt(maxFailedInput, 10);
3135
+ const mins = parseInt(lockDurationMinsInput, 10);
3136
+ if (isNaN(visits) || visits <= 0 || isNaN(failed) || failed <= 0 || isNaN(mins) || mins <= 0) {
3137
+ alert(t("saveConfigFailTip", currentLang));
3138
+ return;
3139
+ }
3140
+ if (!confirm(t("saveConfigConfirm", currentLang))) {
3141
+ return;
3142
+ }
3143
+ setIsSaving(true);
3144
+ updateAdvancedSecurityOptions({
3145
+ maxVisitsPerMinute: visits,
3146
+ maxFailedAttempts: failed,
3147
+ lockDurationMs: mins * 60 * 1e3
3148
+ }).then((data) => {
3149
+ setIsSaving(false);
3150
+ if (data.success) {
3151
+ refreshStatusAndCode();
3152
+ setToast({ message: t("saveConfigSuccessToast", currentLang), type: "success" });
3153
+ setTimeout(() => setToast(null), 3e3);
3154
+ } else {
3155
+ alert((currentLang === "en" ? "Save failed: " : "\u4FDD\u5B58\u5931\u8D25\uFF1A") + (data.reason || ""));
3156
+ }
3157
+ }).catch(() => {
3158
+ setIsSaving(false);
3159
+ alert(currentLang === "en" ? "Network request failed, please try again." : "\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
3160
+ });
3161
+ };
3162
+ const resetAdvancedConfigDefaults = () => {
3163
+ if (!confirm(t("restoreDefaultsConfirm", currentLang))) {
3164
+ return;
3165
+ }
3166
+ setMaxVisitsInput("60");
3167
+ setMaxFailedInput("5");
3168
+ setLockDurationMinsInput("15");
3169
+ setIsSaving(true);
3170
+ updateAdvancedSecurityOptions({
3171
+ maxVisitsPerMinute: 60,
3172
+ maxFailedAttempts: 5,
3173
+ lockDurationMs: 15 * 60 * 1e3
3174
+ }).then((data) => {
3175
+ setIsSaving(false);
3176
+ if (data.success) {
3177
+ refreshStatusAndCode();
3178
+ setToast({ message: t("restoreDefaultsSuccessToast", currentLang), type: "success" });
3179
+ setTimeout(() => setToast(null), 3e3);
3180
+ } else {
3181
+ alert((currentLang === "en" ? "Reset failed: " : "\u91CD\u7F6E\u5931\u8D25\uFF1A") + (data.reason || ""));
3182
+ }
3183
+ }).catch(() => {
3184
+ setIsSaving(false);
3185
+ alert(currentLang === "en" ? "Network request failed, please try again." : "\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
3186
+ });
3187
+ };
3188
+ const copyDirectLink = () => {
3189
+ if (timeLeft <= 0) {
3190
+ alert(t("codeExpired", currentLang));
3191
+ return;
3192
+ }
3193
+ const effectiveQrIp = selectedTab === "tailscale" && status.tailscaleIp ? status.tailscaleIp : status.lanIp || "127.0.0.1";
3194
+ const port = typeof window !== "undefined" && window.location.port || "3080";
3195
+ const directLink = `http://${effectiveQrIp}:${port}/auth?token=${status.code || ""}`;
3196
+ navigator.clipboard.writeText(directLink).then(() => {
3197
+ setStatus((prev) => ({ ...prev, copied: true }));
3198
+ setTimeout(() => {
3199
+ setStatus((prev) => ({ ...prev, copied: false }));
3200
+ }, 2e3);
3201
+ });
3202
+ };
3203
+ const copyText = (text, successTip) => {
3204
+ navigator.clipboard.writeText(text).then(() => {
3205
+ setToast({
3206
+ message: successTip || (currentLang === "en" ? "Copied to clipboard!" : "\u5DF2\u6210\u529F\u590D\u5236\u5230\u526A\u8D34\u677F\uFF01"),
3207
+ type: "success"
3208
+ });
3209
+ setTimeout(() => setToast(null), 2500);
3210
+ });
3211
+ };
3212
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
3213
+ "div",
3214
+ {
3215
+ style: {
3216
+ padding: "4px 8px",
3217
+ color: "var(--dsw-alias-label-primary, inherit)",
3218
+ display: "flex",
3219
+ flexDirection: "column",
3220
+ gap: "16px"
3221
+ },
3222
+ children: [
3223
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
3224
+ "div",
3225
+ {
3226
+ style: {
3227
+ display: "flex",
3228
+ alignItems: "flex-start",
3229
+ justifyContent: "space-between",
3230
+ flexWrap: "wrap",
3231
+ gap: "12px",
3232
+ marginBottom: "4px"
3233
+ },
3234
+ children: [
3235
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { minWidth: "220px", flex: 1 }, children: [
3236
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
3237
+ "div",
3238
+ {
3239
+ style: {
3240
+ display: "flex",
3241
+ alignItems: "center",
3242
+ gap: "8px",
3243
+ flexWrap: "wrap",
3244
+ margin: "0 0 6px 0"
3245
+ },
3246
+ children: [
3247
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3248
+ "h3",
3249
+ {
3250
+ style: {
3251
+ fontSize: "20px",
3252
+ fontWeight: "700",
3253
+ margin: 0,
3254
+ color: "var(--dsw-alias-label-primary, inherit)"
3255
+ },
3256
+ children: t("title", currentLang)
3257
+ }
3258
+ ),
3259
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3260
+ "span",
3261
+ {
3262
+ style: {
3263
+ fontSize: "11px",
3264
+ padding: "2px 7px",
3265
+ borderRadius: "10px",
3266
+ background: "rgba(59,130,246,0.15)",
3267
+ color: "var(--dsw-alias-brand-primary, #3b82f6)",
3268
+ fontWeight: "600"
3269
+ },
3270
+ children: PLUGIN_VERSION
3271
+ }
3272
+ )
3273
+ ]
3274
+ }
3275
+ ),
3276
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3277
+ "div",
3278
+ {
3279
+ style: {
3280
+ fontSize: "13px",
3281
+ color: "var(--dsw-alias-label-tertiary, #888)"
3282
+ },
3283
+ children: t("subtitle", currentLang)
3284
+ }
3285
+ )
3286
+ ] }),
3287
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
3288
+ "a",
3289
+ {
3290
+ href: "https://github.com/IceApriler/dsh-remote-mobile",
3291
+ target: "_blank",
3292
+ rel: "noopener noreferrer",
3293
+ style: {
3294
+ display: "inline-flex",
3295
+ alignItems: "center",
3296
+ gap: "6px",
3297
+ padding: "6px 12px",
3298
+ background: "var(--dsw-alias-bg-layer-3, rgba(128,128,128,0.1))",
3299
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.2))",
3300
+ borderRadius: "8px",
3301
+ color: "var(--dsw-alias-label-secondary, inherit)",
3302
+ fontSize: "12px",
3303
+ fontWeight: "500",
3304
+ textDecoration: "none",
3305
+ transition: "all 0.2s ease",
3306
+ cursor: "pointer"
3307
+ },
3308
+ children: [
3309
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("svg", { style: { width: "16px", height: "16px", fill: "currentColor" }, viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime8.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" }) }),
3310
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: "IceApriler/dsh-remote-mobile" })
3311
+ ]
3312
+ }
3313
+ )
3314
+ ]
3315
+ }
3316
+ ),
3317
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3318
+ NetworkCard,
3319
+ {
3320
+ status,
3321
+ toggleTailscale,
3322
+ toggleLan,
3323
+ copyText,
3324
+ lang: currentLang
3325
+ }
3326
+ ),
3327
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3328
+ QrPairingCard,
3329
+ {
3330
+ status,
3331
+ showCode,
3332
+ setShowCode,
3333
+ timeLeft,
3334
+ showQr,
3335
+ setShowQr,
3336
+ selectedTab,
3337
+ setSelectedTab,
3338
+ generateQrSvg,
3339
+ refreshStatusAndCode,
3340
+ copyDirectLink,
3341
+ lang: currentLang
3342
+ }
3343
+ ),
3344
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3345
+ SecretCard,
3346
+ {
3347
+ status,
3348
+ secretInput,
3349
+ setSecretInput,
3350
+ showSecret,
3351
+ setShowSecret,
3352
+ saveSecret,
3353
+ clearSecret,
3354
+ lang: currentLang
3355
+ }
3356
+ ),
3357
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3358
+ DeviceListCard,
3359
+ {
3360
+ status,
3361
+ revokeDevice,
3362
+ revokeAll,
3363
+ lang: currentLang
3364
+ }
3365
+ ),
3366
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3367
+ SecurityCard,
3368
+ {
3369
+ status,
3370
+ unlockIp,
3371
+ clearIpStats,
3372
+ lang: currentLang
3373
+ }
3374
+ ),
3375
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(StorageCard, { status, lang: currentLang }),
3376
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
3377
+ ConfigCard,
3378
+ {
3379
+ maxVisitsInput,
3380
+ setMaxVisitsInput,
3381
+ maxFailedInput,
3382
+ setMaxFailedInput,
3383
+ lockDurationMinsInput,
3384
+ setLockDurationMinsInput,
3385
+ saveAdvancedConfig,
3386
+ resetAdvancedConfigDefaults,
3387
+ isSaving,
3388
+ lang: currentLang
3389
+ }
3390
+ )
3391
+ ]
3392
+ }
3393
+ );
3394
+ }
3395
+
3396
+ // src/client/utils/toast.js
3397
+ function showGlobalDeviceToast(device, eventType) {
3398
+ if (typeof document === "undefined") return;
3399
+ var toastId = "dsh-global-device-toast";
3400
+ var existing = document.getElementById(toastId);
3401
+ if (existing) existing.remove();
3402
+ var isNewPair = eventType === "device-connected";
3403
+ 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))";
3404
+ var glowShadow = isNewPair ? "rgba(16, 185, 129, 0.3)" : "rgba(59, 130, 246, 0.3)";
3405
+ var icon = isNewPair ? "\u{1F389}" : "\u{1F4F6}";
3406
+ 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";
3407
+ var toast = document.createElement("div");
3408
+ toast.id = toastId;
3409
+ toast.style.cssText = [
3410
+ "position: fixed",
3411
+ "top: 24px",
3412
+ "left: 50%",
3413
+ "transform: translateX(-50%)",
3414
+ "z-index: 9999999",
3415
+ "background: " + bgGradient,
3416
+ "color: white",
3417
+ "border-radius: 14px",
3418
+ "padding: 14px 20px",
3419
+ "box-shadow: 0 16px 36px rgba(0, 0, 0, 0.35), 0 0 24px " + glowShadow,
3420
+ "display: flex",
3421
+ "align-items: center",
3422
+ "gap: 14px",
3423
+ "min-width: 340px",
3424
+ "max-width: 90vw",
3425
+ "backdrop-filter: blur(12px)",
3426
+ "-webkit-backdrop-filter: blur(12px)",
3427
+ "border: 1px solid rgba(255, 255, 255, 0.25)",
3428
+ 'font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
3429
+ "animation: dshToastIn 0.35s cubic-bezier(0.16, 1, 0.3, 1)",
3430
+ "cursor: pointer"
3431
+ ].join(";");
3432
+ if (!document.getElementById("dsh-toast-anim-style")) {
3433
+ var style = document.createElement("style");
3434
+ style.id = "dsh-toast-anim-style";
3435
+ 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); } }";
3436
+ document.head.appendChild(style);
3437
+ }
3438
+ toast.innerHTML = [
3439
+ '<div style="font-size: 26px; line-height: 1; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));">' + icon + "</div>",
3440
+ '<div style="flex: 1; min-width: 0;">',
3441
+ ' <div style="font-size: 14px; font-weight: 700; margin-bottom: 2px; letter-spacing: 0.2px;">' + title + "</div>",
3442
+ ' <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>",
3443
+ "</div>",
3444
+ '<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>'
3445
+ ].join("");
3446
+ document.body.appendChild(toast);
3447
+ var timer = setTimeout(function() {
3448
+ if (toast && toast.parentElement) {
3449
+ toast.style.animation = "dshToastOut 0.3s forwards";
3450
+ setTimeout(function() {
3451
+ if (toast.parentElement) toast.remove();
3452
+ }, 300);
3453
+ }
3454
+ }, 6e3);
3455
+ var closeBtn = toast.querySelector("#dsh-toast-close");
3456
+ if (closeBtn) {
3457
+ closeBtn.onclick = function(e) {
3458
+ e.stopPropagation();
3459
+ clearTimeout(timer);
3460
+ toast.remove();
3461
+ };
3462
+ }
3463
+ }
3464
+ function showGlobalSecurityToast(alertData) {
3465
+ if (typeof document === "undefined") return;
3466
+ var toastId = "dsh-global-security-toast";
3467
+ var existing = document.getElementById(toastId);
3468
+ if (existing) existing.remove();
3469
+ var toast = document.createElement("div");
3470
+ toast.id = toastId;
3471
+ toast.style.cssText = [
3472
+ "position: fixed",
3473
+ "top: 24px",
3474
+ "left: 50%",
3475
+ "transform: translateX(-50%)",
3476
+ "z-index: 9999999",
3477
+ "background: linear-gradient(135deg, rgba(239, 68, 68, 0.95), rgba(185, 28, 28, 0.95))",
3478
+ "color: white",
3479
+ "border-radius: 14px",
3480
+ "padding: 14px 20px",
3481
+ "box-shadow: 0 16px 36px rgba(0, 0, 0, 0.4), 0 0 24px rgba(239, 68, 68, 0.4)",
3482
+ "display: flex",
3483
+ "align-items: center",
3484
+ "gap: 14px",
3485
+ "min-width: 360px",
3486
+ "max-width: 90vw",
3487
+ "backdrop-filter: blur(12px)",
3488
+ "-webkit-backdrop-filter: blur(12px)",
3489
+ "border: 1px solid rgba(255, 255, 255, 0.3)",
3490
+ 'font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
3491
+ "animation: dshToastIn 0.35s cubic-bezier(0.16, 1, 0.3, 1)",
3492
+ "cursor: pointer"
3493
+ ].join(";");
3494
+ toast.innerHTML = [
3495
+ '<div style="font-size: 26px; line-height: 1; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));">\u{1F6A8}</div>',
3496
+ '<div style="flex: 1; min-width: 0;">',
3497
+ ' <div style="font-size: 14px; font-weight: 700; margin-bottom: 2px; letter-spacing: 0.2px;">\u5B89\u5168\u62E6\u622A\u9884\u8B66</div>',
3498
+ ' <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>",
3499
+ "</div>",
3500
+ '<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>'
3501
+ ].join("");
3502
+ document.body.appendChild(toast);
3503
+ var timer = setTimeout(function() {
3504
+ if (toast && toast.parentElement) {
3505
+ toast.style.animation = "dshToastOut 0.3s forwards";
3506
+ setTimeout(function() {
3507
+ if (toast.parentElement) toast.remove();
3508
+ }, 300);
3509
+ }
3510
+ }, 7e3);
3511
+ var closeBtn = toast.querySelector("#dsh-sec-toast-close");
3512
+ if (closeBtn) {
3513
+ closeBtn.onclick = function(e) {
3514
+ e.stopPropagation();
3515
+ clearTimeout(timer);
3516
+ toast.remove();
3517
+ };
3518
+ }
3519
+ }
3520
+
3521
+ // src/client/index.jsx
3522
+ var inject = ["slots", "locale", "connection", "settingsScope", "remote"];
3523
+ function apply(ctx) {
3524
+ if (typeof window !== "undefined") {
3525
+ window.__DSH_CLIENT_CTX__ = ctx;
3526
+ var meta = document.querySelector('meta[name="viewport"]');
3527
+ if (!meta) {
3528
+ meta = document.createElement("meta");
3529
+ meta.name = "viewport";
3530
+ document.head.appendChild(meta);
3531
+ }
3532
+ meta.content = "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover";
3533
+ if (!window.__DSH_REMOTE_MOBILE_SSE_LISTENER__) {
3534
+ window.__DSH_REMOTE_MOBILE_SSE_LISTENER__ = true;
3535
+ var initSse = function() {
3536
+ try {
3537
+ var eventSource = new EventSource("/api/remote-mobile/events");
3538
+ eventSource.onmessage = function(event) {
3539
+ try {
3540
+ var data = JSON.parse(event.data || "{}");
3541
+ if (data.type === "device-connected" && data.device) {
3542
+ showGlobalDeviceToast(data.device, "device-connected");
3543
+ window.dispatchEvent(new CustomEvent("dsh-device-updated", { detail: data }));
3544
+ } else if (data.type === "device-online" && data.device) {
3545
+ showGlobalDeviceToast(data.device, "device-online");
3546
+ window.dispatchEvent(new CustomEvent("dsh-device-updated", { detail: data }));
3547
+ } else if (data.type === "device-revoked") {
3548
+ window.dispatchEvent(new CustomEvent("dsh-device-updated", { detail: data }));
3549
+ } else if (data.type === "ip-security-alert") {
3550
+ showGlobalSecurityToast(data);
3551
+ window.dispatchEvent(new CustomEvent("dsh-device-updated", { detail: data }));
3552
+ } else if (data.type === "ip-security-updated") {
3553
+ window.dispatchEvent(new CustomEvent("dsh-device-updated", { detail: data }));
3554
+ }
3555
+ } catch (e) {
3556
+ }
3557
+ };
3558
+ eventSource.onerror = function() {
3559
+ try {
3560
+ eventSource.close();
3561
+ } catch (e) {
3562
+ }
3563
+ setTimeout(initSse, 3e3);
3564
+ };
3565
+ } catch (e) {
3566
+ setTimeout(initSse, 5e3);
3567
+ }
3568
+ };
3569
+ initSse();
3570
+ }
3571
+ var slots = null;
3572
+ try {
3573
+ if (ctx && typeof ctx.get === "function") {
3574
+ slots = ctx.get("slots");
3575
+ } else if (ctx && ctx.slots) {
3576
+ slots = ctx.slots;
3577
+ }
3578
+ } catch (e) {
3579
+ }
3580
+ if (slots && typeof slots.inject === "function") {
3581
+ try {
3582
+ slots.inject("settings.section", function() {
3583
+ var unregister = slots.register({
3584
+ name: "settings.section",
3585
+ id: "tailscale-mobile",
3586
+ order: 150,
3587
+ label: function() {
3588
+ var lang = resolveLocale(ctx);
3589
+ return lang === "en" ? "Remote & Mobile" : "\u8FDC\u7A0B\u4E0E\u79FB\u52A8\u7AEF";
3590
+ }
3591
+ }, TailscaleMobileSection);
3592
+ return function() {
3593
+ if (typeof unregister === "function") unregister();
3594
+ };
3595
+ });
3596
+ } catch (e) {
3597
+ }
3598
+ }
3599
+ }
3600
+ }
3601
+ var index_default = apply;
3602
+
3603
+
3604
+ return module.exports;
3605
+ }
3606
+ });