@shendeguize/remote-dsh-center 0.4.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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +197 -0
  3. package/README.md +174 -0
  4. package/package.json +48 -0
  5. package/scripts/install.mjs +208 -0
  6. package/src/api.js +725 -0
  7. package/src/cli.js +1445 -0
  8. package/src/config-sync.js +157 -0
  9. package/src/daemon.js +362 -0
  10. package/src/defaults.js +89 -0
  11. package/src/dsh-workspace.js +467 -0
  12. package/src/launcher.js +627 -0
  13. package/src/lib/bundle.js +82 -0
  14. package/src/lib/bus.js +109 -0
  15. package/src/lib/capture.js +53 -0
  16. package/src/lib/clock.js +18 -0
  17. package/src/lib/entry.js +27 -0
  18. package/src/lib/errors.js +88 -0
  19. package/src/lib/logfile.js +65 -0
  20. package/src/lib/machine.js +63 -0
  21. package/src/lib/origin-guard.js +64 -0
  22. package/src/lib/pool.js +88 -0
  23. package/src/lib/proto.js +457 -0
  24. package/src/lib/semver.js +103 -0
  25. package/src/lib/shq.js +112 -0
  26. package/src/lib/ssh.js +647 -0
  27. package/src/lib/validate.js +363 -0
  28. package/src/monitor.js +145 -0
  29. package/src/patchsync.js +310 -0
  30. package/src/ports.js +93 -0
  31. package/src/prober.js +185 -0
  32. package/src/server.js +449 -0
  33. package/src/settings-file.js +550 -0
  34. package/src/ssh-config.js +152 -0
  35. package/src/store.js +772 -0
  36. package/src/tunnel.js +589 -0
  37. package/src/updater.js +450 -0
  38. package/src/web/actions.js +409 -0
  39. package/src/web/api.js +262 -0
  40. package/src/web/app.js +347 -0
  41. package/src/web/components/config-sync-dialog.js +469 -0
  42. package/src/web/components/confirm-dialog.js +61 -0
  43. package/src/web/components/defaults-card.js +216 -0
  44. package/src/web/components/event-panel.js +98 -0
  45. package/src/web/components/host-drawer.js +1039 -0
  46. package/src/web/components/host-table.js +317 -0
  47. package/src/web/components/hub.js +143 -0
  48. package/src/web/components/iframe-pane.js +377 -0
  49. package/src/web/components/manager-card.js +65 -0
  50. package/src/web/components/setup-wizard.js +726 -0
  51. package/src/web/components/tabbar.js +577 -0
  52. package/src/web/components/toast-region.js +107 -0
  53. package/src/web/favicon.svg +7 -0
  54. package/src/web/form.js +220 -0
  55. package/src/web/host-presentation.js +73 -0
  56. package/src/web/host-rules.js +76 -0
  57. package/src/web/index.html +17 -0
  58. package/src/web/router.js +118 -0
  59. package/src/web/setup-schema.js +203 -0
  60. package/src/web/sse.js +118 -0
  61. package/src/web/store.js +405 -0
  62. package/src/web/style.css +813 -0
  63. package/src/web/utils.js +210 -0
@@ -0,0 +1,813 @@
1
+ /* DSH Center 样式(10 §2 设计令牌 + §2.3 z-index 层级)。无构建链,单文件。 */
2
+
3
+ :root {
4
+ color-scheme: light dark;
5
+
6
+ --bg: #f6f7f9;
7
+ --surface: #ffffff;
8
+ --surface-2: #f0f2f5;
9
+ --border: #d8dce3;
10
+ --text: #1d2129;
11
+ --text-dim: #6b7280;
12
+
13
+ --running: #17a34a;
14
+ --degraded: #d98800;
15
+ --crashed: #d93f3f;
16
+ --ready: #2f6fed;
17
+ --starting: #7a5af8;
18
+ --neutral: #8a9099;
19
+
20
+ --radius: 8px;
21
+ --gap: 12px;
22
+ --app-shell-height: 48px;
23
+ --card-border-width: 1px;
24
+ --card-padding-inline: 14px;
25
+ --card-content-inset: calc(var(--card-border-width) + var(--card-padding-inline));
26
+ --font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", "PingFang SC", "Microsoft YaHei", sans-serif;
27
+ --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
28
+
29
+ --z-tabbar: 20;
30
+ --z-banner: 30;
31
+ --z-scrim: 40;
32
+ --z-drawer: 50;
33
+ --z-toast: 60;
34
+ /* 菜单在 toast 之上:亲手唤出来的东西不许被一条自己会消失的通知压住(issue #68) */
35
+ --z-menu: 70;
36
+ --z-dialog: 80;
37
+ }
38
+
39
+ @media (prefers-color-scheme: dark) {
40
+ :root {
41
+ --bg: #16181d;
42
+ --surface: #1e2127;
43
+ --surface-2: #24272e;
44
+ --border: #33373f;
45
+ --text: #e6e8ea;
46
+ --text-dim: #9aa1ab;
47
+ }
48
+ }
49
+
50
+ * { box-sizing: border-box; }
51
+
52
+ /*
53
+ * 组件全靠 node.hidden 控制显隐,而下面很多类都自带 display:flex——
54
+ * 作者样式会盖掉 UA 的 [hidden]{display:none},隐藏元素不仅照旧可见,
55
+ * 键盘还能 Tab 进已隐藏的抽屉与 iframe。这一条是显隐语义的地基。
56
+ */
57
+ [hidden] { display: none !important; }
58
+
59
+ /* 键盘可达是硬要求:默认焦点圈在自定义控件上很容易被 UA 样式吃掉 */
60
+ :focus-visible { outline: 2px solid var(--ready); outline-offset: 2px; }
61
+
62
+ body {
63
+ margin: 0;
64
+ font: 14px/1.5 var(--font);
65
+ color: var(--text);
66
+ background: var(--bg);
67
+ }
68
+
69
+ #app {
70
+ display: flex;
71
+ flex-direction: column;
72
+ min-width: 0;
73
+ min-height: 100vh;
74
+ overflow-x: hidden;
75
+ }
76
+
77
+ /* ── header ─────────────────────────────────────────────────────────── */
78
+
79
+ .app-shell {
80
+ position: sticky;
81
+ top: 0;
82
+ z-index: var(--z-tabbar);
83
+ display: flex;
84
+ align-items: center;
85
+ gap: 10px;
86
+ width: 100%;
87
+ height: var(--app-shell-height);
88
+ min-width: 0;
89
+ padding: 6px 12px;
90
+ overflow: hidden;
91
+ background: var(--surface-2);
92
+ border-bottom: 1px solid var(--border);
93
+ }
94
+
95
+ .app-header {
96
+ display: flex;
97
+ align-items: center;
98
+ flex: none;
99
+ min-width: max-content;
100
+ }
101
+
102
+ .brand { display: flex; flex: none; align-items: baseline; gap: 10px; }
103
+ .brand-link { font-weight: 650; font-size: 16px; color: var(--text); text-decoration: none; }
104
+ .brand-sub { color: var(--text-dim); font-size: 12px; }
105
+
106
+ .conn-indicator {
107
+ display: block;
108
+ flex: none;
109
+ width: 10px; height: 10px; border-radius: 50%;
110
+ background: var(--neutral);
111
+ }
112
+ .conn-indicator[data-state="open"] { background: var(--running); }
113
+ .conn-indicator[data-state="connecting"],
114
+ .conn-indicator[data-state="reconnecting"] { background: var(--degraded); }
115
+ .conn-indicator[data-state="offline"] { background: var(--crashed); }
116
+
117
+ @media (max-width: 620px) {
118
+ .app-shell { gap: 6px; padding-inline: 8px; }
119
+ .brand-sub { display: none; }
120
+ }
121
+
122
+ /* ── 断联横幅 ───────────────────────────────────────────────────────── */
123
+
124
+ .disconnect-banner {
125
+ position: sticky; top: var(--app-shell-height); z-index: var(--z-banner);
126
+ padding: 8px 16px;
127
+ background: #fff4e0; color: #8a4b00;
128
+ border-bottom: 1px solid #f0d29a;
129
+ font-size: 13px;
130
+ }
131
+ .disconnect-banner[data-tone="resync"] { background: #e8f1ff; color: #1a4fa0; border-bottom-color: #b8d0f5; }
132
+
133
+ /* ── 视图与卡片 ─────────────────────────────────────────────────────── */
134
+
135
+ .view { flex: 1; padding: 16px; display: flex; flex-direction: column; gap: var(--gap); }
136
+ .view-skeleton, .view-fallback { align-items: flex-start; }
137
+
138
+ .card {
139
+ min-width: 0;
140
+ background: var(--surface);
141
+ border: var(--card-border-width) solid var(--border);
142
+ border-radius: var(--radius);
143
+ padding: 12px var(--card-padding-inline);
144
+ }
145
+ .card-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
146
+ .card-header h2 { margin: 0; font-size: 14px; font-weight: 650; }
147
+ .card-sub, .card-notice { color: var(--text-dim); font-size: 12px; }
148
+ .card-notice { margin: 8px 0 0; color: var(--degraded); }
149
+ .card-footer { display: flex; align-items: center; gap: 10px; margin-top: 12px; }
150
+
151
+ .view-dashboard > .manage-header {
152
+ flex-wrap: nowrap;
153
+ padding-inline: var(--card-content-inset);
154
+ }
155
+ .view-dashboard > .manage-header > .row-actions { margin-left: auto; }
156
+ .side-by-side { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: var(--gap); }
157
+
158
+ @media (max-width: 620px) {
159
+ .view-dashboard > .manage-header {
160
+ flex-wrap: wrap;
161
+ min-width: 0;
162
+ }
163
+ .view-dashboard > .manage-header > .row-actions {
164
+ justify-content: flex-start;
165
+ width: 100%;
166
+ min-width: 0;
167
+ margin-left: 0;
168
+ }
169
+ .side-by-side {
170
+ grid-template-columns: minmax(0, 1fr);
171
+ min-width: 0;
172
+ }
173
+ }
174
+
175
+ .empty-hint { color: var(--text-dim); margin: 8px 0; }
176
+ .link { color: var(--ready); font-size: 13px; }
177
+
178
+ /* ── Hub 起始页 ─────────────────────────────────────────────────────── */
179
+
180
+ .view-hub {
181
+ align-items: center;
182
+ min-height: 0;
183
+ padding: clamp(28px, 7vh, 72px) 20px 40px;
184
+ overflow-x: hidden;
185
+ overflow-y: auto;
186
+ }
187
+ .hub-content {
188
+ width: min(900px, 100%);
189
+ min-width: 0;
190
+ flex: none;
191
+ display: flex;
192
+ flex-direction: column;
193
+ align-items: stretch;
194
+ gap: 22px;
195
+ margin-block: auto;
196
+ }
197
+ .hub-hero { text-align: center; }
198
+ .hub-brand {
199
+ margin: 0 0 6px;
200
+ font-size: clamp(21px, 3vw, 28px);
201
+ font-weight: 700;
202
+ letter-spacing: -0.02em;
203
+ }
204
+ .hub-hero h2 { margin: 0; color: var(--text-dim); font-size: 15px; font-weight: 500; }
205
+ .hub-host-grid {
206
+ display: grid;
207
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
208
+ gap: var(--gap);
209
+ min-width: 0;
210
+ }
211
+ .hub-host-card {
212
+ min-width: 0;
213
+ display: flex;
214
+ flex-direction: column;
215
+ align-items: flex-start;
216
+ gap: 9px;
217
+ padding: 16px;
218
+ border: 1px solid var(--border);
219
+ border-radius: calc(var(--radius) + 2px);
220
+ background: var(--surface);
221
+ color: var(--text);
222
+ font: inherit;
223
+ text-align: left;
224
+ cursor: pointer;
225
+ }
226
+ .hub-host-card:hover { border-color: var(--ready); background: var(--surface-2); }
227
+ .hub-host-card.is-active { border-color: var(--ready); }
228
+ .hub-host-head { width: 100%; display: flex; align-items: center; gap: 8px; min-width: 0; }
229
+ .hub-host-head strong { overflow-wrap: anywhere; font-size: 15px; font-weight: 650; }
230
+ .hub-host-head .tag { margin: 0; flex: none; }
231
+ .hub-host-summary { color: var(--text-dim); font-size: 12px; overflow-wrap: anywhere; }
232
+ .hub-unavailable {
233
+ display: flex;
234
+ align-items: center;
235
+ justify-content: center;
236
+ flex-wrap: wrap;
237
+ gap: 6px 10px;
238
+ color: var(--text-dim);
239
+ font-size: 12px;
240
+ text-align: center;
241
+ }
242
+ .hub-empty { text-align: center; color: var(--text-dim); }
243
+ .hub-empty p { margin: 0 0 12px; }
244
+ .hub-empty-actions { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 12px; }
245
+ .hub-syncing { text-align: center; }
246
+
247
+ @media (max-width: 620px) {
248
+ .view-hub { padding: 28px 12px; }
249
+ .hub-content { margin-block: 0; }
250
+ .hub-host-grid { grid-template-columns: minmax(0, 1fr); }
251
+ .hub-host-card { width: 100%; }
252
+ }
253
+
254
+ /* ── 表格 ───────────────────────────────────────────────────────────── */
255
+
256
+ .table-scroll { overflow-x: auto; }
257
+ .host-table { width: 100%; border-collapse: collapse; }
258
+ .host-table th, .host-table td {
259
+ text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); vertical-align: top;
260
+ }
261
+ .host-table th { font-size: 12px; color: var(--text-dim); font-weight: 600; }
262
+ .host-table tbody tr { cursor: pointer; }
263
+ .host-table tbody tr:hover { background: var(--surface-2); }
264
+ .host-table tbody tr:focus-visible { outline: 2px solid var(--ready); outline-offset: -2px; }
265
+ .host-table small { display: block; color: var(--text-dim); font-size: 11px; }
266
+ .host-cell strong { font-weight: 600; }
267
+ .dsh-cell, .mapping-cell, .pid-cell { font-family: var(--mono); font-size: 12px; }
268
+ .mono-link { font-family: var(--mono); color: var(--ready); }
269
+ .row-actions { display: flex; flex-wrap: wrap; gap: 6px; }
270
+
271
+ .tag {
272
+ display: inline-block; margin-top: 2px; padding: 0 5px;
273
+ border-radius: 4px; font-size: 11px; background: var(--surface-2); color: var(--text-dim);
274
+ }
275
+ .tag-warn { background: #fff2d6; color: #8a5a00; }
276
+ .tag-lock { background: var(--surface-2); }
277
+
278
+ /* ── 状态徽章 ───────────────────────────────────────────────────────── */
279
+
280
+ .phase-badge { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
281
+ .phase-hint { margin-top: 2px; }
282
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; flex: none; }
283
+ .status-dot[data-dot="hollow"] { background: transparent; border: 1.5px solid currentColor; }
284
+ .status-dot[data-dot="none"] { background: transparent; border: 1.5px dashed currentColor; }
285
+ .status-dot[data-dot="pulse"] { animation: pulse 1.4s ease-in-out infinite; }
286
+ .status-dot[data-dot="blink"] { animation: blink 1s steps(2, end) infinite; }
287
+
288
+ .phase-badge[data-tone="running"] { color: var(--running); }
289
+ .phase-badge[data-tone="degraded"] { color: var(--degraded); }
290
+ .phase-badge[data-tone="crashed"] { color: var(--crashed); }
291
+ .phase-badge[data-tone="ready"] { color: var(--ready); }
292
+ .phase-badge[data-tone="starting"] { color: var(--starting); }
293
+ .phase-badge[data-tone="neutral"] { color: var(--neutral); }
294
+
295
+ @keyframes pulse { 50% { opacity: 0.25; } }
296
+ @keyframes blink { 50% { opacity: 0.2; } }
297
+
298
+ @media (prefers-reduced-motion: reduce) {
299
+ .status-dot[data-dot="pulse"], .status-dot[data-dot="blink"] { animation: none; }
300
+ }
301
+
302
+ /* ── 按钮与开关 ─────────────────────────────────────────────────────── */
303
+
304
+ .btn {
305
+ font: inherit; line-height: 1.2;
306
+ padding: 6px 12px; border-radius: 6px;
307
+ border: 1px solid var(--border); background: var(--surface);
308
+ color: var(--text); cursor: pointer;
309
+ }
310
+ .btn-compact { padding: 3px 8px; font-size: 12px; }
311
+ .btn:hover:not(:disabled) { background: var(--surface-2); }
312
+ .btn:disabled { opacity: 0.45; cursor: not-allowed; }
313
+ .btn-primary { background: var(--ready); border-color: var(--ready); color: #fff; }
314
+ .btn-primary:hover:not(:disabled) { filter: brightness(1.08); background: var(--ready); }
315
+ .btn-danger { background: var(--crashed); border-color: var(--crashed); color: #fff; }
316
+ .btn-danger:hover:not(:disabled) { filter: brightness(1.08); background: var(--crashed); }
317
+
318
+ .switch { display: inline-flex; align-items: center; cursor: pointer; }
319
+ .switch input { margin: 0; width: 16px; height: 16px; accent-color: var(--ready); }
320
+ .switch-track { display: none; }
321
+
322
+ /* ── 表单 ───────────────────────────────────────────────────────────── */
323
+
324
+ .field-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; }
325
+ .field { display: flex; flex-direction: column; gap: 4px; }
326
+ .field label { font-size: 12px; color: var(--text-dim); }
327
+ .field input, .field textarea, .field select {
328
+ font: inherit; padding: 5px 8px;
329
+ border: 1px solid var(--border); border-radius: 6px;
330
+ background: var(--surface); color: var(--text);
331
+ }
332
+ .field textarea { font-family: var(--mono); font-size: 12px; min-height: 72px; resize: vertical; }
333
+ .field-hint { margin: 0; font-size: 11px; color: var(--text-dim); }
334
+ .field-error { margin: 0; font-size: 11px; color: var(--crashed); }
335
+ .field.has-error input, .field.has-error textarea { border-color: var(--crashed); }
336
+ .section-note, .wizard-note { margin: 0 0 2px; font-size: 11px; color: var(--text-dim); }
337
+
338
+ /* 「保存了但还没生效」的提示:文字承载语义,颜色只是强调(无障碍要求) */
339
+ .pending-badge {
340
+ margin-left: 6px; padding: 1px 6px; border-radius: 9px;
341
+ background: var(--surface-2); color: var(--degraded); font-size: 11px;
342
+ }
343
+
344
+ .kv { display: grid; grid-template-columns: max-content 1fr; gap: 4px 14px; margin: 0; }
345
+ .kv dt { color: var(--text-dim); font-size: 12px; }
346
+ .kv dd { margin: 0; font-family: var(--mono); font-size: 12px; }
347
+
348
+ .mode-badge {
349
+ padding: 1px 7px; border-radius: 10px;
350
+ background: var(--surface-2); color: var(--text-dim); font-size: 11px;
351
+ }
352
+
353
+ /* ── 事件面板 ───────────────────────────────────────────────────────── */
354
+
355
+ .event-toolbar { display: flex; gap: 8px; margin-bottom: 8px; }
356
+ .event-filter { font: inherit; padding: 3px 6px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); color: var(--text); }
357
+ .event-list { list-style: none; margin: 0; padding: 0; max-height: 260px; overflow-y: auto; }
358
+ .event-item {
359
+ display: grid; grid-template-columns: 66px 40px 130px 1fr; gap: 8px;
360
+ padding: 3px 0; border-bottom: 1px solid var(--surface-2);
361
+ font-family: var(--mono); font-size: 12px;
362
+ }
363
+ .event-item time, .event-item .event-level, .event-item .event-host { color: var(--text-dim); }
364
+ .event-warn .event-level { color: var(--degraded); }
365
+ .event-error .event-level { color: var(--crashed); }
366
+ .event-msg { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
367
+
368
+ /* ── toast ──────────────────────────────────────────────────────────── */
369
+
370
+ .toast-region {
371
+ position: fixed; right: 16px; bottom: 16px; z-index: var(--z-toast);
372
+ display: flex; flex-direction: column; gap: 8px; max-width: 380px;
373
+ /* 容器不吃指针事件,否则多条 toast 之间那 8px 间隙也会白白吞掉一次点击 */
374
+ pointer-events: none;
375
+ }
376
+ .toast {
377
+ position: relative;
378
+ pointer-events: auto;
379
+ padding: 10px 30px 10px 12px;
380
+ border-radius: var(--radius);
381
+ border: 1px solid var(--border);
382
+ background: var(--surface);
383
+ box-shadow: 0 6px 20px rgb(0 0 0 / 0.14);
384
+ }
385
+ .toast-region[data-modal-blocked="true"] .toast { pointer-events: none; }
386
+ .toast .summary { margin: 0; font-size: 13px; }
387
+ .toast-error { border-left: 3px solid var(--crashed); }
388
+ .toast-warn { border-left: 3px solid var(--degraded); }
389
+ .toast-success { border-left: 3px solid var(--running); }
390
+ .toast-info { border-left: 3px solid var(--ready); }
391
+ .toast .detail {
392
+ margin: 6px 0 0; padding: 6px; max-height: 200px; overflow: auto;
393
+ background: var(--surface-2); border-radius: 6px;
394
+ font-family: var(--mono); font-size: 11px; white-space: pre-wrap;
395
+ }
396
+ .toast summary { cursor: pointer; font-size: 12px; color: var(--text-dim); margin-top: 4px; }
397
+ .toast .detail-actions { margin-top: 6px; }
398
+ .toast-close {
399
+ position: absolute; top: 4px; right: 6px;
400
+ border: 0; background: none; cursor: pointer;
401
+ font-size: 16px; line-height: 1; color: var(--text-dim);
402
+ }
403
+
404
+ /* ── 标签栏与右键菜单 ───────────────────────────────────────────────── */
405
+
406
+ .tabbar {
407
+ display: flex;
408
+ flex: 1;
409
+ align-items: center;
410
+ gap: 6px;
411
+ min-width: 0;
412
+ overflow: hidden;
413
+ }
414
+ .host-tabs {
415
+ display: flex;
416
+ flex: 1;
417
+ gap: 6px;
418
+ min-width: 0;
419
+ overflow-x: auto;
420
+ overflow-y: hidden;
421
+ overscroll-behavior-inline: contain;
422
+ scrollbar-width: none;
423
+ }
424
+ .host-tabs::-webkit-scrollbar { display: none; }
425
+ .tabbar-actions {
426
+ display: flex;
427
+ flex: none;
428
+ align-items: center;
429
+ gap: 6px;
430
+ margin-left: auto;
431
+ }
432
+ .tab {
433
+ display: inline-flex; align-items: center; gap: 6px;
434
+ flex: none;
435
+ min-width: 0;
436
+ font: inherit; font-size: 13px;
437
+ padding: 4px 10px; border-radius: 6px;
438
+ border: 1px solid transparent; background: transparent;
439
+ color: var(--text); cursor: pointer; white-space: nowrap;
440
+ }
441
+ .tab:hover { background: var(--surface); }
442
+ .tab.is-active { background: var(--surface); border-color: var(--border); font-weight: 600; }
443
+ .tab .status-dot { color: var(--neutral); }
444
+ .tab .dot-running { color: var(--running); }
445
+ .tab .dot-degraded { color: var(--degraded); }
446
+ .tab .dot-crashed { color: var(--crashed); }
447
+ .tab .dot-starting { color: var(--starting); }
448
+ .tab-label {
449
+ display: block;
450
+ max-width: 180px;
451
+ overflow: hidden;
452
+ text-overflow: ellipsis;
453
+ }
454
+
455
+ .icon-button {
456
+ font: inherit; font-size: 15px; line-height: 1;
457
+ padding: 4px 8px; border-radius: 6px;
458
+ border: 1px solid var(--border); background: var(--surface);
459
+ color: var(--text); cursor: pointer;
460
+ }
461
+ .icon-button:disabled { opacity: 0.45; cursor: not-allowed; }
462
+ .icon-button.is-busy { animation: spin 1s linear infinite; }
463
+ @keyframes spin { to { transform: rotate(360deg); } }
464
+ @media (prefers-reduced-motion: reduce) { .icon-button.is-busy { animation: none; } }
465
+
466
+ @media (max-width: 620px) {
467
+ .tabbar, .host-tabs, .tabbar-actions { gap: 4px; }
468
+ .tab { padding-inline: 7px; }
469
+ .tab-label { max-width: 108px; }
470
+ }
471
+
472
+ .context-menu {
473
+ position: fixed; z-index: var(--z-menu);
474
+ margin: 0; padding: 4px; min-width: 180px;
475
+ list-style: none;
476
+ background: var(--surface); border: 1px solid var(--border);
477
+ border-radius: var(--radius); box-shadow: 0 8px 24px rgb(0 0 0 / 0.18);
478
+ }
479
+ .context-menu button {
480
+ display: block; width: 100%; text-align: left;
481
+ font: inherit; font-size: 13px; padding: 5px 8px;
482
+ border: 0; border-radius: 5px; background: none; color: var(--text); cursor: pointer;
483
+ }
484
+ .context-menu button:hover:not(:disabled) { background: var(--surface-2); }
485
+ .context-menu button:disabled { opacity: 0.4; cursor: not-allowed; }
486
+
487
+ /* ── iframe pane ────────────────────────────────────────────────────── */
488
+
489
+ .iframe-stack { flex: 1; position: relative; display: flex; }
490
+ .iframe-pane { flex: 1; position: relative; }
491
+ .iframe-pane iframe { width: 100%; height: 100%; border: 0; display: block; background: var(--surface); }
492
+ .iframe-loading, .iframe-overlay {
493
+ position: absolute; inset: 0;
494
+ display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px;
495
+ background: color-mix(in srgb, var(--bg) 88%, transparent);
496
+ text-align: center; padding: 24px;
497
+ }
498
+ .iframe-loading { z-index: 1; }
499
+ .iframe-overlay { z-index: 2; }
500
+ .iframe-loading p { margin: 0; color: var(--text-dim); font-size: 13px; }
501
+ .iframe-overlay h2 { margin: 0; font-size: 15px; }
502
+ .iframe-overlay p { margin: 0; color: var(--text-dim); font-size: 13px; }
503
+ .overlay-actions { display: flex; align-items: center; gap: 12px; }
504
+
505
+ /* ── 主机详情抽屉 ───────────────────────────────────────────────────── */
506
+
507
+ .drawer-scrim { position: fixed; inset: 0; z-index: var(--z-scrim); background: rgb(0 0 0 / 0.32); }
508
+ .host-drawer {
509
+ position: fixed; top: 0; right: 0; bottom: 0; z-index: var(--z-drawer);
510
+ width: min(520px, 100vw);
511
+ display: flex; flex-direction: column; gap: 14px;
512
+ padding: 14px 16px 24px;
513
+ overflow-y: auto;
514
+ background: var(--surface);
515
+ border-left: 1px solid var(--border);
516
+ box-shadow: -8px 0 28px rgb(0 0 0 / 0.16);
517
+ }
518
+ .drawer-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
519
+ .drawer-header h2 { margin: 0 0 4px; font-size: 16px; }
520
+ .drawer-close { border: 0; background: none; font-size: 22px; line-height: 1; color: var(--text-dim); cursor: pointer; }
521
+ .drawer-form { display: flex; flex-direction: column; gap: 14px; }
522
+ .config-section { display: flex; flex-direction: column; gap: 8px; }
523
+ .config-section h3 { margin: 0; font-size: 13px; color: var(--text-dim); font-weight: 600; }
524
+ .drawer-actions { display: flex; gap: 10px; }
525
+ .probe-detail h3, .dsh-settings-editor h3, .remote-log h3 { margin: 0 0 6px; font-size: 13px; color: var(--text-dim); font-weight: 600; }
526
+ .dsh-settings-editor {
527
+ display: flex;
528
+ min-width: 0;
529
+ flex-direction: column;
530
+ gap: 8px;
531
+ padding-top: 12px;
532
+ border-top: 1px solid var(--border);
533
+ }
534
+ .dsh-settings-editor label {
535
+ font-size: 12px;
536
+ color: var(--text-dim);
537
+ }
538
+ .dsh-settings-safety {
539
+ margin: 0;
540
+ padding: 8px 10px;
541
+ border: 1px solid var(--border);
542
+ border-radius: 6px;
543
+ background: var(--surface-2);
544
+ font-size: 12px;
545
+ }
546
+ .dsh-settings-path-row {
547
+ display: flex;
548
+ min-width: 0;
549
+ flex-direction: column;
550
+ gap: 2px;
551
+ color: var(--text-dim);
552
+ font-size: 12px;
553
+ }
554
+ .dsh-settings-path {
555
+ max-width: 100%;
556
+ color: var(--text);
557
+ font-family: var(--mono);
558
+ overflow-wrap: anywhere;
559
+ white-space: pre-wrap;
560
+ }
561
+ .dsh-settings-content {
562
+ width: 100%;
563
+ min-width: 0;
564
+ min-height: 240px;
565
+ padding: 8px;
566
+ resize: vertical;
567
+ border: 1px solid var(--border);
568
+ border-radius: 6px;
569
+ background: var(--surface);
570
+ color: var(--text);
571
+ font: 12px/1.5 var(--mono);
572
+ }
573
+ .dsh-settings-content:disabled { background: var(--surface-2); }
574
+ .dsh-settings-preserved {
575
+ padding: 8px 10px;
576
+ border: 1px solid var(--border);
577
+ border-radius: 6px;
578
+ background: var(--surface-2);
579
+ }
580
+ .dsh-settings-preserved summary {
581
+ cursor: pointer;
582
+ color: var(--text-dim);
583
+ font-size: 12px;
584
+ font-weight: 650;
585
+ }
586
+ .dsh-settings-preserved-content {
587
+ width: 100%;
588
+ min-width: 0;
589
+ min-height: 140px;
590
+ margin-top: 8px;
591
+ padding: 8px;
592
+ resize: vertical;
593
+ border: 1px solid var(--border);
594
+ border-radius: 6px;
595
+ background: var(--surface);
596
+ color: var(--text);
597
+ font: 12px/1.5 var(--mono);
598
+ }
599
+ .dsh-settings-status {
600
+ min-height: 18px;
601
+ margin: 0;
602
+ color: var(--text-dim);
603
+ font-size: 12px;
604
+ }
605
+ .dsh-settings-status[data-tone="warn"] { color: var(--degraded); }
606
+ .dsh-settings-status[data-tone="error"] { color: var(--crashed); }
607
+ .dsh-settings-status[data-tone="warn"]::before {
608
+ content: "注意:";
609
+ font-weight: 650;
610
+ }
611
+ .dsh-settings-status[data-tone="error"]::before {
612
+ content: "错误:";
613
+ font-weight: 650;
614
+ }
615
+ .dsh-settings-actions {
616
+ display: flex;
617
+ flex-wrap: wrap;
618
+ gap: 8px;
619
+ }
620
+ .remote-log-body {
621
+ margin: 0; padding: 8px; max-height: 260px; overflow: auto;
622
+ background: var(--surface-2); border-radius: 6px;
623
+ font-family: var(--mono); font-size: 11px; white-space: pre-wrap;
624
+ }
625
+ .log-actions { margin-top: 8px; }
626
+
627
+ @media (max-width: 520px) {
628
+ .host-drawer { padding-inline: 12px; }
629
+ .drawer-actions, .dsh-settings-actions { flex-wrap: wrap; }
630
+ .dsh-settings-actions .btn { flex: 1 1 140px; }
631
+ }
632
+
633
+ /* ── 首启向导 ───────────────────────────────────────────────────────── */
634
+
635
+ .setup-wizard { max-width: 760px; width: 100%; margin: 0 auto; padding: 28px 16px 40px; gap: 18px; }
636
+ .wizard-head h1 { margin: 0 0 4px; font-size: 20px; }
637
+ .wizard-sub { margin: 0; color: var(--text-dim); font-size: 13px; }
638
+
639
+ .stepper { display: flex; flex-wrap: wrap; gap: 8px; margin: 0; padding: 0; list-style: none; }
640
+ .stepper .step {
641
+ padding: 5px 10px; border-radius: 999px;
642
+ border: 1px solid var(--border); background: var(--surface);
643
+ color: var(--text-dim); font-size: 12px;
644
+ }
645
+ /* 当前步不只靠颜色:加粗 + 实心边框,满足无障碍要求 */
646
+ .stepper .step[data-state="current"] { color: var(--text); font-weight: 650; border-color: var(--ready); }
647
+ .stepper .step[data-state="done"]::before { content: "✓ "; color: var(--ready); }
648
+
649
+ .step-panel {
650
+ display: flex; flex-direction: column; gap: 14px;
651
+ padding: 16px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface);
652
+ }
653
+ .step-title { margin: 0; font-size: 15px; }
654
+ .step-panel .field input, .step-panel .field textarea { height: auto; }
655
+ .step-panel textarea { min-height: 320px; }
656
+
657
+ .wizard-progress { margin: 0; color: var(--text-dim); font-size: 12px; }
658
+ .wizard-warn { margin: 0; color: var(--degraded); font-size: 12px; }
659
+ .wizard-notice { margin: 0; font-size: 12px; color: var(--text-dim); }
660
+
661
+ .setup-hosts { width: 100%; border-collapse: collapse; }
662
+ .setup-hosts th, .setup-hosts td {
663
+ padding: 7px 8px; text-align: left; border-bottom: 1px solid var(--border); font-size: 13px;
664
+ }
665
+ .setup-hosts th { font-size: 12px; color: var(--text-dim); font-weight: 600; }
666
+ .setup-hosts tr[data-probing="yes"] .probe-cell { color: var(--text-dim); font-style: italic; }
667
+ .setup-hosts input[type="checkbox"]:disabled { cursor: not-allowed; opacity: 0.45; }
668
+
669
+ .wizard-foot { display: flex; align-items: center; gap: 12px; }
670
+ .wizard-foot .btn { height: 36px; }
671
+ .migration-target { font-family: var(--mono); font-size: 13px; word-break: break-all; }
672
+
673
+ /* ── 确认框 ─────────────────────────────────────────────────────────── */
674
+
675
+ .confirm-dialog {
676
+ z-index: var(--z-dialog);
677
+ border: 1px solid var(--border); border-radius: var(--radius);
678
+ background: var(--surface); color: var(--text);
679
+ padding: 0; max-width: 460px;
680
+ }
681
+ .confirm-dialog::backdrop { background: rgb(0 0 0 / 0.4); }
682
+ .confirm-inner { padding: 16px 18px; }
683
+ .confirm-inner h2 { margin: 0 0 8px; font-size: 15px; }
684
+ .confirm-body p { margin: 4px 0; color: var(--text-dim); font-size: 13px; }
685
+ .confirm-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }
686
+
687
+ /* ── 批量配置同步 ───────────────────────────────────────────────────── */
688
+
689
+ .config-sync-dialog {
690
+ z-index: var(--z-dialog);
691
+ width: min(680px, calc(100vw - 32px));
692
+ max-width: none;
693
+ max-height: calc(100dvh - 32px);
694
+ padding: 0;
695
+ overflow: hidden;
696
+ border: 1px solid var(--border);
697
+ border-radius: var(--radius);
698
+ background: var(--surface);
699
+ color: var(--text);
700
+ }
701
+ .config-sync-dialog::backdrop { background: rgb(0 0 0 / 0.4); }
702
+ .config-sync-dialog[open] { display: flex; flex-direction: column; }
703
+ .config-sync-inner {
704
+ display: flex;
705
+ flex: 1;
706
+ min-height: 0;
707
+ flex-direction: column;
708
+ }
709
+ .config-sync-head {
710
+ flex: none;
711
+ padding: 16px 18px 10px;
712
+ border-bottom: 1px solid var(--border);
713
+ }
714
+ .config-sync-head h2 { margin: 0 0 4px; font-size: 16px; }
715
+ .config-sync-head p { margin: 0; color: var(--text-dim); font-size: 12px; }
716
+ .config-sync-body {
717
+ display: flex;
718
+ min-height: 0;
719
+ flex: 1;
720
+ flex-direction: column;
721
+ gap: 12px;
722
+ overflow-y: auto;
723
+ padding: 14px 18px;
724
+ }
725
+ .config-sync-safety {
726
+ margin: 0;
727
+ padding: 9px 10px;
728
+ border: 1px solid var(--border);
729
+ border-radius: 6px;
730
+ background: var(--surface-2);
731
+ font-size: 12px;
732
+ line-height: 1.55;
733
+ }
734
+ .config-sync-source { width: 100%; }
735
+ .config-sync-target-fieldset {
736
+ min-width: 0;
737
+ margin: 0;
738
+ padding: 10px;
739
+ border: 1px solid var(--border);
740
+ border-radius: 6px;
741
+ }
742
+ .config-sync-target-fieldset legend { padding: 0 4px; font-size: 12px; font-weight: 650; }
743
+ .config-sync-shortcuts { display: flex; gap: 6px; margin-bottom: 8px; }
744
+ .config-sync-targets {
745
+ max-height: 180px;
746
+ overflow-y: auto;
747
+ border-block: 1px solid var(--border);
748
+ }
749
+ .config-sync-target-row {
750
+ display: flex;
751
+ align-items: center;
752
+ gap: 8px;
753
+ min-height: 34px;
754
+ padding: 4px 6px;
755
+ border-bottom: 1px solid var(--border);
756
+ font-size: 13px;
757
+ }
758
+ .config-sync-target-row:last-child { border-bottom: 0; }
759
+ .config-sync-target-row input { width: 16px; height: 16px; accent-color: var(--ready); }
760
+ .config-sync-target-row input:disabled + label { color: var(--text-dim); }
761
+ .config-sync-status { min-height: 18px; margin: 0; color: var(--text-dim); font-size: 12px; }
762
+ .config-sync-results h3 { margin: 0 0 8px; font-size: 13px; }
763
+ .config-sync-result-list {
764
+ max-height: 190px;
765
+ margin: 0;
766
+ padding: 0;
767
+ overflow-y: auto;
768
+ list-style: none;
769
+ border: 1px solid var(--border);
770
+ border-radius: 6px;
771
+ }
772
+ .config-sync-result-item {
773
+ display: grid;
774
+ grid-template-columns: minmax(120px, 0.45fr) minmax(180px, 1fr);
775
+ gap: 4px 10px;
776
+ padding: 8px 10px;
777
+ border-bottom: 1px solid var(--border);
778
+ font-size: 12px;
779
+ }
780
+ .config-sync-result-item:last-child { border-bottom: 0; }
781
+ .config-sync-result-item strong { overflow-wrap: anywhere; }
782
+ .config-sync-change-summary { color: var(--text-dim); }
783
+ .config-sync-restart-note {
784
+ grid-column: 2;
785
+ font-weight: 650;
786
+ }
787
+ .config-sync-restart-note::before { content: "⚠ "; }
788
+ .config-sync-no-restart { margin: 8px 0 0; font-size: 12px; }
789
+ .config-sync-actions {
790
+ display: flex;
791
+ flex: none;
792
+ justify-content: flex-end;
793
+ gap: 8px;
794
+ padding: 12px 18px;
795
+ border-top: 1px solid var(--border);
796
+ background: var(--surface);
797
+ }
798
+
799
+ @media (max-width: 620px) {
800
+ .config-sync-dialog {
801
+ width: calc(100vw - 16px);
802
+ max-height: calc(100dvh - 16px);
803
+ }
804
+ .config-sync-head { padding: 14px 12px 9px; }
805
+ .config-sync-body { padding: 12px; }
806
+ .config-sync-result-item { grid-template-columns: minmax(0, 1fr); }
807
+ .config-sync-restart-note { grid-column: 1; }
808
+ .config-sync-actions {
809
+ flex-wrap: wrap;
810
+ padding: 10px 12px;
811
+ }
812
+ .config-sync-actions .btn { flex: 1 1 auto; }
813
+ }