bhg-helper 1.0.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 (53) hide show
  1. package/README.md +78 -0
  2. package/api/app.ts +53 -0
  3. package/api/index.ts +9 -0
  4. package/api/lib/logger.ts +65 -0
  5. package/api/lib/paths.ts +27 -0
  6. package/api/lib/providers.ts +66 -0
  7. package/api/lib/repository.ts +153 -0
  8. package/api/lib/types.ts +43 -0
  9. package/api/relay/config.ts +76 -0
  10. package/api/relay/protocol.ts +393 -0
  11. package/api/relay/server.ts +283 -0
  12. package/api/routes/backups.ts +73 -0
  13. package/api/routes/config.ts +197 -0
  14. package/api/routes/install.ts +158 -0
  15. package/api/routes/logs.ts +20 -0
  16. package/api/routes/providers.ts +13 -0
  17. package/api/routes/relay.ts +106 -0
  18. package/api/server.ts +40 -0
  19. package/cli/cli.js +454 -0
  20. package/dist/assets/index-BjvGHrGe.js +156 -0
  21. package/dist/assets/index-CQrGCyBr.css +1 -0
  22. package/dist/favicon.svg +4 -0
  23. package/dist/index.html +20 -0
  24. package/index.html +19 -0
  25. package/nodemon.json +10 -0
  26. package/package.json +82 -0
  27. package/postcss.config.js +10 -0
  28. package/scripts/install.bat +32 -0
  29. package/scripts/start.bat +46 -0
  30. package/scripts/start.ps1 +45 -0
  31. package/src/App.tsx +73 -0
  32. package/src/assets/react.svg +1 -0
  33. package/src/components/ConsolePanel.tsx +44 -0
  34. package/src/components/Empty.tsx +8 -0
  35. package/src/components/ErrorBoundary.tsx +54 -0
  36. package/src/components/Layout.tsx +17 -0
  37. package/src/components/Page.tsx +130 -0
  38. package/src/components/Sidebar.tsx +56 -0
  39. package/src/hooks/useTheme.ts +29 -0
  40. package/src/index.css +1350 -0
  41. package/src/lib/api.ts +120 -0
  42. package/src/lib/store.ts +166 -0
  43. package/src/lib/types.ts +117 -0
  44. package/src/lib/utils.ts +6 -0
  45. package/src/main.tsx +10 -0
  46. package/src/pages/ConsolePage.tsx +48 -0
  47. package/src/pages/Dashboard.tsx +101 -0
  48. package/src/pages/Install.tsx +195 -0
  49. package/src/pages/Relay.tsx +409 -0
  50. package/src/vite-env.d.ts +1 -0
  51. package/tailwind.config.js +13 -0
  52. package/tsconfig.json +40 -0
  53. package/vite.config.ts +28 -0
package/src/index.css ADDED
@@ -0,0 +1,1350 @@
1
+ /* ============================================
2
+ BHG-helper — Global Styles
3
+ Theme: Broadcast Cockpit (dark)
4
+ ============================================ */
5
+
6
+ :root {
7
+ /* === Color tokens === */
8
+ --bg-0: #0b0e14;
9
+ --bg-1: #0f131c;
10
+ --bg-2: #141a23;
11
+ --bg-3: #1a2230;
12
+ --bg-elev: #1c2532;
13
+
14
+ --line-1: #1f2733;
15
+ --line-2: #2a3441;
16
+ --line-3: #3a4555;
17
+
18
+ --ink-0: #e6edf3;
19
+ --ink-1: #b8c1cc;
20
+ --ink-2: #7d8590;
21
+ --ink-3: #4a5563;
22
+
23
+ --accent-amber: #ffb454;
24
+ --accent-amber-dim: #b87f30;
25
+ --accent-green: #3fb950;
26
+ --accent-green-dim: #2a7a36;
27
+ --accent-red: #f85149;
28
+ --accent-red-dim: #a9322c;
29
+ --accent-cyan: #58a6ff;
30
+ --accent-cyan-dim: #2a5d9a;
31
+ --accent-violet: #a371f7;
32
+
33
+ /* === Typography === */
34
+ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans SC', sans-serif;
35
+ --font-display: 'Space Grotesk', 'Inter', sans-serif;
36
+ --font-mono: 'JetBrains Mono', 'Cascadia Code', 'Consolas', monospace;
37
+
38
+ /* === Spacing (multiples of 4) === */
39
+ --sp-1: 4px;
40
+ --sp-2: 8px;
41
+ --sp-3: 12px;
42
+ --sp-4: 16px;
43
+ --sp-5: 20px;
44
+ --sp-6: 24px;
45
+ --sp-7: 32px;
46
+ --sp-8: 40px;
47
+ --sp-9: 48px;
48
+ --sp-10: 64px;
49
+
50
+ /* === Radius === */
51
+ --r-0: 0px;
52
+ --r-1: 2px;
53
+ --r-2: 4px;
54
+ --r-3: 6px;
55
+ --r-pill: 999px;
56
+
57
+ /* === Shadows === */
58
+ --shadow-1: 0 1px 0 rgba(0, 0, 0, 0.5), 0 0 0 1px var(--line-2);
59
+ --shadow-glow-amber: 0 0 12px rgba(255, 180, 84, 0.35);
60
+ --shadow-glow-green: 0 0 12px rgba(63, 185, 80, 0.35);
61
+ --shadow-glow-red: 0 0 12px rgba(248, 81, 73, 0.35);
62
+ }
63
+
64
+ * {
65
+ box-sizing: border-box;
66
+ }
67
+
68
+ html,
69
+ body,
70
+ #root {
71
+ height: 100%;
72
+ margin: 0;
73
+ padding: 0;
74
+ }
75
+
76
+ body {
77
+ background: var(--bg-0);
78
+ color: var(--ink-0);
79
+ font-family: var(--font-sans);
80
+ font-size: 14px;
81
+ line-height: 1.5;
82
+ -webkit-font-smoothing: antialiased;
83
+ -moz-osx-font-smoothing: grayscale;
84
+ text-rendering: optimizeLegibility;
85
+ overflow: hidden;
86
+ }
87
+
88
+ /* Decorative grid background — subtle scanline feel */
89
+ body::before {
90
+ content: '';
91
+ position: fixed;
92
+ inset: 0;
93
+ background-image: linear-gradient(rgba(58, 69, 85, 0.06) 1px, transparent 1px),
94
+ linear-gradient(90deg, rgba(58, 69, 85, 0.06) 1px, transparent 1px);
95
+ background-size: 40px 40px;
96
+ pointer-events: none;
97
+ z-index: 0;
98
+ }
99
+
100
+ body::after {
101
+ content: '';
102
+ position: fixed;
103
+ inset: 0;
104
+ background: radial-gradient(ellipse at top, rgba(88, 166, 255, 0.04), transparent 60%),
105
+ radial-gradient(ellipse at bottom right, rgba(163, 113, 247, 0.04), transparent 50%);
106
+ pointer-events: none;
107
+ z-index: 0;
108
+ }
109
+
110
+ #root {
111
+ position: relative;
112
+ z-index: 1;
113
+ }
114
+
115
+ button {
116
+ font-family: inherit;
117
+ font-size: inherit;
118
+ cursor: pointer;
119
+ border: none;
120
+ background: none;
121
+ color: inherit;
122
+ }
123
+
124
+ input,
125
+ select,
126
+ textarea {
127
+ font-family: inherit;
128
+ font-size: inherit;
129
+ color: var(--ink-0);
130
+ }
131
+
132
+ a {
133
+ color: var(--accent-cyan);
134
+ text-decoration: none;
135
+ }
136
+
137
+ ::selection {
138
+ background: var(--accent-cyan-dim);
139
+ color: var(--ink-0);
140
+ }
141
+
142
+ ::-webkit-scrollbar {
143
+ width: 8px;
144
+ height: 8px;
145
+ }
146
+
147
+ ::-webkit-scrollbar-track {
148
+ background: var(--bg-0);
149
+ }
150
+
151
+ ::-webkit-scrollbar-thumb {
152
+ background: var(--line-2);
153
+ border-radius: 0;
154
+ }
155
+
156
+ ::-webkit-scrollbar-thumb:hover {
157
+ background: var(--line-3);
158
+ }
159
+
160
+ /* ============================================
161
+ Layout
162
+ ============================================ */
163
+ .app-shell {
164
+ display: grid;
165
+ grid-template-columns: 260px 1fr;
166
+ grid-template-rows: 1fr 200px;
167
+ grid-template-areas:
168
+ 'sidebar main'
169
+ 'sidebar console';
170
+ height: 100vh;
171
+ width: 100vw;
172
+ }
173
+
174
+ .app-sidebar {
175
+ grid-area: sidebar;
176
+ background: var(--bg-1);
177
+ border-right: 1px solid var(--line-2);
178
+ display: flex;
179
+ flex-direction: column;
180
+ overflow: hidden;
181
+ }
182
+
183
+ .app-main {
184
+ grid-area: main;
185
+ overflow: hidden;
186
+ position: relative;
187
+ }
188
+
189
+ .app-console {
190
+ grid-area: console;
191
+ border-top: 1px solid var(--line-2);
192
+ background: var(--bg-1);
193
+ overflow: hidden;
194
+ display: flex;
195
+ flex-direction: column;
196
+ }
197
+
198
+ /* ============================================
199
+ Sidebar
200
+ ============================================ */
201
+ .sidebar-brand {
202
+ padding: var(--sp-6) var(--sp-5) var(--sp-4);
203
+ border-bottom: 1px solid var(--line-2);
204
+ position: relative;
205
+ }
206
+
207
+ .sidebar-brand-row {
208
+ display: flex;
209
+ align-items: center;
210
+ gap: var(--sp-3);
211
+ }
212
+
213
+ .sidebar-logo {
214
+ width: 28px;
215
+ height: 28px;
216
+ background: linear-gradient(135deg, var(--accent-amber), var(--accent-red));
217
+ display: grid;
218
+ place-items: center;
219
+ font-family: var(--font-mono);
220
+ font-weight: 700;
221
+ font-size: 14px;
222
+ color: var(--bg-0);
223
+ position: relative;
224
+ }
225
+
226
+ .sidebar-logo::after {
227
+ content: '';
228
+ position: absolute;
229
+ inset: -3px;
230
+ border: 1px solid var(--line-3);
231
+ pointer-events: none;
232
+ }
233
+
234
+ .sidebar-title {
235
+ font-family: var(--font-display);
236
+ font-size: 16px;
237
+ font-weight: 700;
238
+ letter-spacing: 0.04em;
239
+ text-transform: uppercase;
240
+ }
241
+
242
+ .sidebar-subtitle {
243
+ font-family: var(--font-mono);
244
+ font-size: 10px;
245
+ color: var(--ink-2);
246
+ letter-spacing: 0.1em;
247
+ text-transform: uppercase;
248
+ margin-top: 2px;
249
+ }
250
+
251
+ .sidebar-meta {
252
+ margin-top: var(--sp-4);
253
+ padding-top: var(--sp-3);
254
+ border-top: 1px dashed var(--line-2);
255
+ display: flex;
256
+ flex-direction: column;
257
+ gap: var(--sp-1);
258
+ font-family: var(--font-mono);
259
+ font-size: 10px;
260
+ color: var(--ink-2);
261
+ }
262
+
263
+ .sidebar-meta-row {
264
+ display: flex;
265
+ justify-content: space-between;
266
+ gap: var(--sp-2);
267
+ }
268
+
269
+ .sidebar-meta-row .key {
270
+ text-transform: uppercase;
271
+ letter-spacing: 0.08em;
272
+ }
273
+
274
+ .sidebar-meta-row .val {
275
+ color: var(--ink-1);
276
+ white-space: nowrap;
277
+ overflow: hidden;
278
+ text-overflow: ellipsis;
279
+ max-width: 140px;
280
+ }
281
+
282
+ .sidebar-nav {
283
+ flex: 1;
284
+ padding: var(--sp-3) var(--sp-2);
285
+ overflow-y: auto;
286
+ }
287
+
288
+ .sidebar-section {
289
+ font-family: var(--font-mono);
290
+ font-size: 10px;
291
+ color: var(--ink-3);
292
+ text-transform: uppercase;
293
+ letter-spacing: 0.12em;
294
+ padding: var(--sp-3) var(--sp-3) var(--sp-2);
295
+ }
296
+
297
+ .sidebar-link {
298
+ display: flex;
299
+ align-items: center;
300
+ gap: var(--sp-3);
301
+ padding: var(--sp-3) var(--sp-3);
302
+ color: var(--ink-1);
303
+ border-left: 2px solid transparent;
304
+ font-size: 13px;
305
+ font-weight: 500;
306
+ transition: all 120ms ease;
307
+ position: relative;
308
+ text-transform: uppercase;
309
+ letter-spacing: 0.04em;
310
+ font-family: var(--font-display);
311
+ }
312
+
313
+ .sidebar-link:hover {
314
+ background: var(--bg-2);
315
+ color: var(--ink-0);
316
+ }
317
+
318
+ .sidebar-link.active {
319
+ background: var(--bg-2);
320
+ color: var(--ink-0);
321
+ border-left-color: var(--accent-amber);
322
+ }
323
+
324
+ .sidebar-link.active::before {
325
+ content: '▸';
326
+ position: absolute;
327
+ left: -2px;
328
+ top: 50%;
329
+ transform: translateY(-50%);
330
+ color: var(--accent-amber);
331
+ font-size: 10px;
332
+ }
333
+
334
+ .sidebar-link svg {
335
+ width: 16px;
336
+ height: 16px;
337
+ stroke-width: 1.5;
338
+ }
339
+
340
+ .sidebar-link .badge {
341
+ margin-left: auto;
342
+ font-family: var(--font-mono);
343
+ font-size: 9px;
344
+ color: var(--ink-2);
345
+ background: var(--bg-3);
346
+ padding: 1px 6px;
347
+ border: 1px solid var(--line-2);
348
+ border-radius: 1px;
349
+ }
350
+
351
+ .sidebar-footer {
352
+ padding: var(--sp-3) var(--sp-4);
353
+ border-top: 1px solid var(--line-2);
354
+ display: flex;
355
+ align-items: center;
356
+ gap: var(--sp-2);
357
+ font-family: var(--font-mono);
358
+ font-size: 10px;
359
+ color: var(--ink-2);
360
+ text-transform: uppercase;
361
+ letter-spacing: 0.08em;
362
+ }
363
+
364
+ /* ============================================
365
+ Main content
366
+ ============================================ */
367
+ .page {
368
+ height: 100%;
369
+ display: flex;
370
+ flex-direction: column;
371
+ overflow: hidden;
372
+ }
373
+
374
+ .page-header {
375
+ padding: var(--sp-5) var(--sp-7) var(--sp-4);
376
+ border-bottom: 1px solid var(--line-2);
377
+ background: var(--bg-1);
378
+ display: flex;
379
+ align-items: center;
380
+ gap: var(--sp-4);
381
+ flex-shrink: 0;
382
+ }
383
+
384
+ .page-title {
385
+ font-family: var(--font-display);
386
+ font-size: 20px;
387
+ font-weight: 700;
388
+ letter-spacing: 0.04em;
389
+ text-transform: uppercase;
390
+ margin: 0;
391
+ }
392
+
393
+ .page-title-prefix {
394
+ font-family: var(--font-mono);
395
+ color: var(--ink-3);
396
+ font-weight: 400;
397
+ margin-right: var(--sp-2);
398
+ }
399
+
400
+ .page-subtitle {
401
+ font-size: 12px;
402
+ color: var(--ink-2);
403
+ margin: 0;
404
+ }
405
+
406
+ .page-actions {
407
+ margin-left: auto;
408
+ display: flex;
409
+ gap: var(--sp-2);
410
+ }
411
+
412
+ .page-body {
413
+ flex: 1;
414
+ overflow-y: auto;
415
+ padding: var(--sp-7);
416
+ }
417
+
418
+ /* ============================================
419
+ Panels
420
+ ============================================ */
421
+ .panel {
422
+ background: var(--bg-2);
423
+ border: 1px solid var(--line-2);
424
+ position: relative;
425
+ }
426
+
427
+ .panel-header {
428
+ display: flex;
429
+ align-items: center;
430
+ gap: var(--sp-3);
431
+ padding: var(--sp-3) var(--sp-4);
432
+ border-bottom: 1px solid var(--line-2);
433
+ background: linear-gradient(180deg, var(--bg-3), var(--bg-2));
434
+ font-family: var(--font-mono);
435
+ font-size: 11px;
436
+ text-transform: uppercase;
437
+ letter-spacing: 0.1em;
438
+ color: var(--ink-1);
439
+ }
440
+
441
+ .panel-header::before {
442
+ content: '';
443
+ width: 8px;
444
+ height: 8px;
445
+ background: var(--accent-amber);
446
+ border-radius: 50%;
447
+ box-shadow: var(--shadow-glow-amber);
448
+ }
449
+
450
+ .panel-body {
451
+ padding: var(--sp-5);
452
+ }
453
+
454
+ .panel-grid {
455
+ display: grid;
456
+ gap: var(--sp-4);
457
+ }
458
+
459
+ .panel-grid.cols-3 {
460
+ grid-template-columns: repeat(3, 1fr);
461
+ }
462
+
463
+ .panel-grid.cols-2 {
464
+ grid-template-columns: repeat(2, 1fr);
465
+ }
466
+
467
+ .panel-grid.cols-4 {
468
+ grid-template-columns: repeat(2, 1fr);
469
+ }
470
+
471
+ @media (min-width: 1600px) {
472
+ .panel-grid.cols-4 {
473
+ grid-template-columns: repeat(4, 1fr);
474
+ }
475
+ }
476
+
477
+ /* ============================================
478
+ LED Indicator
479
+ ============================================ */
480
+ .led {
481
+ display: inline-flex;
482
+ align-items: center;
483
+ gap: var(--sp-2);
484
+ font-family: var(--font-mono);
485
+ font-size: 10px;
486
+ text-transform: uppercase;
487
+ letter-spacing: 0.1em;
488
+ color: var(--ink-1);
489
+ }
490
+
491
+ .led-dot {
492
+ width: 10px;
493
+ height: 10px;
494
+ border-radius: 50%;
495
+ background: var(--ink-3);
496
+ position: relative;
497
+ flex-shrink: 0;
498
+ }
499
+
500
+ .led-dot.amber {
501
+ background: var(--accent-amber);
502
+ box-shadow: var(--shadow-glow-amber);
503
+ animation: led-pulse 1.6s ease-in-out infinite;
504
+ }
505
+
506
+ .led-dot.green {
507
+ background: var(--accent-green);
508
+ box-shadow: var(--shadow-glow-green);
509
+ }
510
+
511
+ .led-dot.red {
512
+ background: var(--accent-red);
513
+ box-shadow: var(--shadow-glow-red);
514
+ animation: led-pulse 0.8s ease-in-out infinite;
515
+ }
516
+
517
+ .led-dot.gray {
518
+ background: var(--ink-3);
519
+ }
520
+
521
+ @keyframes led-pulse {
522
+ 0%, 100% { opacity: 1; }
523
+ 50% { opacity: 0.4; }
524
+ }
525
+
526
+ /* ============================================
527
+ Status Card
528
+ ============================================ */
529
+ .status-card {
530
+ background: var(--bg-2);
531
+ border: 1px solid var(--line-2);
532
+ padding: var(--sp-5);
533
+ position: relative;
534
+ overflow: hidden;
535
+ }
536
+
537
+ .status-card::before {
538
+ content: '';
539
+ position: absolute;
540
+ top: 0;
541
+ left: 0;
542
+ right: 0;
543
+ height: 2px;
544
+ background: var(--line-2);
545
+ }
546
+
547
+ .status-card.amber::before { background: var(--accent-amber); box-shadow: var(--shadow-glow-amber); }
548
+ .status-card.green::before { background: var(--accent-green); box-shadow: var(--shadow-glow-green); }
549
+ .status-card.red::before { background: var(--accent-red); box-shadow: var(--shadow-glow-red); }
550
+
551
+ .status-card-label {
552
+ font-family: var(--font-mono);
553
+ font-size: 10px;
554
+ text-transform: uppercase;
555
+ letter-spacing: 0.1em;
556
+ color: var(--ink-2);
557
+ display: flex;
558
+ align-items: center;
559
+ gap: var(--sp-2);
560
+ }
561
+
562
+ .status-card-value {
563
+ font-family: var(--font-mono);
564
+ font-size: 22px;
565
+ font-weight: 600;
566
+ color: var(--ink-0);
567
+ margin-top: var(--sp-3);
568
+ word-break: break-all;
569
+ line-height: 1.2;
570
+ }
571
+
572
+ .status-card-meta {
573
+ font-family: var(--font-mono);
574
+ font-size: 10px;
575
+ color: var(--ink-3);
576
+ margin-top: var(--sp-2);
577
+ text-transform: uppercase;
578
+ letter-spacing: 0.06em;
579
+ }
580
+
581
+ /* ============================================
582
+ Buttons
583
+ ============================================ */
584
+ .btn {
585
+ display: inline-flex;
586
+ align-items: center;
587
+ gap: var(--sp-2);
588
+ padding: var(--sp-2) var(--sp-4);
589
+ font-family: var(--font-display);
590
+ font-size: 12px;
591
+ font-weight: 600;
592
+ text-transform: uppercase;
593
+ letter-spacing: 0.06em;
594
+ border: 1px solid var(--line-2);
595
+ background: var(--bg-2);
596
+ color: var(--ink-1);
597
+ border-radius: 1px;
598
+ transition: all 100ms ease;
599
+ position: relative;
600
+ white-space: nowrap;
601
+ }
602
+
603
+ .btn:hover {
604
+ background: var(--bg-3);
605
+ color: var(--ink-0);
606
+ border-color: var(--line-3);
607
+ }
608
+
609
+ .btn:active {
610
+ background: var(--bg-1);
611
+ }
612
+
613
+ .btn:disabled {
614
+ opacity: 0.4;
615
+ cursor: not-allowed;
616
+ }
617
+
618
+ .btn-primary {
619
+ background: var(--accent-amber);
620
+ color: var(--bg-0);
621
+ border-color: var(--accent-amber);
622
+ }
623
+
624
+ .btn-primary:hover {
625
+ background: #ffc878;
626
+ border-color: #ffc878;
627
+ color: var(--bg-0);
628
+ }
629
+
630
+ .btn-danger {
631
+ border-color: var(--accent-red-dim);
632
+ color: var(--accent-red);
633
+ }
634
+
635
+ .btn-danger:hover {
636
+ background: var(--accent-red-dim);
637
+ color: var(--ink-0);
638
+ }
639
+
640
+ .btn-ghost {
641
+ border-color: transparent;
642
+ background: transparent;
643
+ }
644
+
645
+ .btn svg {
646
+ width: 14px;
647
+ height: 14px;
648
+ stroke-width: 2;
649
+ }
650
+
651
+ .btn-bar {
652
+ position: absolute;
653
+ left: -1px;
654
+ top: 8px;
655
+ bottom: 8px;
656
+ width: 3px;
657
+ background: var(--accent-amber);
658
+ }
659
+
660
+ /* ============================================
661
+ Form
662
+ ============================================ */
663
+ .field {
664
+ display: flex;
665
+ flex-direction: column;
666
+ gap: var(--sp-2);
667
+ margin-bottom: var(--sp-4);
668
+ }
669
+
670
+ .field-label {
671
+ font-family: var(--font-mono);
672
+ font-size: 10px;
673
+ text-transform: uppercase;
674
+ letter-spacing: 0.1em;
675
+ color: var(--ink-2);
676
+ display: flex;
677
+ align-items: center;
678
+ gap: var(--sp-2);
679
+ }
680
+
681
+ .field-label .key {
682
+ color: var(--ink-3);
683
+ }
684
+
685
+ .field-hint {
686
+ font-family: var(--font-mono);
687
+ font-size: 10px;
688
+ color: var(--ink-3);
689
+ margin-top: 2px;
690
+ }
691
+
692
+ .input,
693
+ .textarea,
694
+ .select {
695
+ background: var(--bg-1);
696
+ border: 1px solid var(--line-2);
697
+ padding: var(--sp-3) var(--sp-3);
698
+ font-family: var(--font-mono);
699
+ font-size: 13px;
700
+ color: var(--ink-0);
701
+ border-radius: 1px;
702
+ transition: border-color 120ms;
703
+ width: 100%;
704
+ }
705
+
706
+ .input:focus,
707
+ .textarea:focus,
708
+ .select:focus {
709
+ outline: none;
710
+ border-color: var(--accent-amber);
711
+ box-shadow: 0 0 0 1px var(--accent-amber-dim);
712
+ }
713
+
714
+ .input.mono {
715
+ font-family: var(--font-mono);
716
+ letter-spacing: 0.02em;
717
+ }
718
+
719
+ .textarea {
720
+ min-height: 80px;
721
+ resize: vertical;
722
+ font-family: var(--font-mono);
723
+ }
724
+
725
+ .input-group {
726
+ display: flex;
727
+ gap: var(--sp-2);
728
+ align-items: center;
729
+ }
730
+
731
+ .input-group .input {
732
+ flex: 1;
733
+ }
734
+
735
+ /* ============================================
736
+ Table
737
+ ============================================ */
738
+ .table {
739
+ width: 100%;
740
+ border-collapse: collapse;
741
+ font-size: 12px;
742
+ }
743
+
744
+ .table thead th {
745
+ text-align: left;
746
+ padding: var(--sp-3) var(--sp-3);
747
+ background: var(--bg-3);
748
+ font-family: var(--font-mono);
749
+ font-size: 10px;
750
+ text-transform: uppercase;
751
+ letter-spacing: 0.1em;
752
+ color: var(--ink-2);
753
+ font-weight: 500;
754
+ border-bottom: 1px solid var(--line-2);
755
+ }
756
+
757
+ .table tbody tr {
758
+ border-bottom: 1px solid var(--line-1);
759
+ transition: background 80ms;
760
+ }
761
+
762
+ .table tbody tr:hover {
763
+ background: var(--bg-3);
764
+ }
765
+
766
+ .table tbody tr.active {
767
+ background: rgba(255, 180, 84, 0.05);
768
+ }
769
+
770
+ .table td {
771
+ padding: var(--sp-3) var(--sp-3);
772
+ vertical-align: middle;
773
+ }
774
+
775
+ .cell-id {
776
+ font-family: var(--font-mono);
777
+ font-size: 12px;
778
+ color: var(--ink-0);
779
+ font-weight: 500;
780
+ }
781
+
782
+ .cell-mono {
783
+ font-family: var(--font-mono);
784
+ font-size: 11px;
785
+ color: var(--ink-1);
786
+ }
787
+
788
+ .cell-muted {
789
+ color: var(--ink-2);
790
+ }
791
+
792
+ .cell-num {
793
+ font-family: var(--font-mono);
794
+ text-align: right;
795
+ }
796
+
797
+ /* ============================================
798
+ Toggle
799
+ ============================================ */
800
+ .toggle {
801
+ position: relative;
802
+ width: 36px;
803
+ height: 18px;
804
+ background: var(--bg-1);
805
+ border: 1px solid var(--line-3);
806
+ border-radius: 1px;
807
+ cursor: pointer;
808
+ transition: all 120ms;
809
+ }
810
+
811
+ .toggle::after {
812
+ content: '';
813
+ position: absolute;
814
+ top: 1px;
815
+ left: 1px;
816
+ width: 14px;
817
+ height: 14px;
818
+ background: var(--ink-3);
819
+ transition: all 120ms;
820
+ }
821
+
822
+ .toggle.on {
823
+ background: var(--accent-green-dim);
824
+ border-color: var(--accent-green);
825
+ }
826
+
827
+ .toggle.on::after {
828
+ left: 19px;
829
+ background: var(--accent-green);
830
+ box-shadow: 0 0 6px rgba(63, 185, 80, 0.5);
831
+ }
832
+
833
+ /* ============================================
834
+ Provider Card
835
+ ============================================ */
836
+ .provider-card {
837
+ background: var(--bg-2);
838
+ border: 1px solid var(--line-2);
839
+ padding: var(--sp-5);
840
+ cursor: pointer;
841
+ transition: all 160ms;
842
+ position: relative;
843
+ display: flex;
844
+ flex-direction: column;
845
+ gap: var(--sp-3);
846
+ min-height: 160px;
847
+ }
848
+
849
+ .provider-card:hover {
850
+ border-color: var(--line-3);
851
+ transform: translateY(-2px);
852
+ }
853
+
854
+ .provider-card.active {
855
+ border-color: var(--accent-amber);
856
+ background: var(--bg-3);
857
+ }
858
+
859
+ .provider-card.active::before {
860
+ content: '';
861
+ position: absolute;
862
+ inset: 0;
863
+ border: 1px solid var(--accent-amber);
864
+ pointer-events: none;
865
+ box-shadow: var(--shadow-glow-amber);
866
+ }
867
+
868
+ .provider-card-head {
869
+ display: flex;
870
+ align-items: center;
871
+ gap: var(--sp-3);
872
+ }
873
+
874
+ .provider-card-logo {
875
+ width: 36px;
876
+ height: 36px;
877
+ background: var(--bg-1);
878
+ border: 1px solid var(--line-2);
879
+ display: grid;
880
+ place-items: center;
881
+ font-family: var(--font-display);
882
+ font-weight: 700;
883
+ font-size: 14px;
884
+ color: var(--accent-amber);
885
+ position: relative;
886
+ }
887
+
888
+ .provider-card.active .provider-card-logo {
889
+ background: var(--accent-amber);
890
+ color: var(--bg-0);
891
+ }
892
+
893
+ .provider-card-name {
894
+ font-family: var(--font-display);
895
+ font-size: 14px;
896
+ font-weight: 600;
897
+ text-transform: uppercase;
898
+ letter-spacing: 0.04em;
899
+ }
900
+
901
+ .provider-card-protocol {
902
+ font-family: var(--font-mono);
903
+ font-size: 9px;
904
+ text-transform: uppercase;
905
+ letter-spacing: 0.1em;
906
+ padding: 1px 6px;
907
+ border: 1px solid var(--line-2);
908
+ color: var(--ink-2);
909
+ border-radius: 1px;
910
+ display: inline-block;
911
+ align-self: flex-start;
912
+ }
913
+
914
+ .provider-card-notes {
915
+ font-size: 11px;
916
+ color: var(--ink-2);
917
+ line-height: 1.5;
918
+ flex: 1;
919
+ }
920
+
921
+ .provider-card-models {
922
+ font-family: var(--font-mono);
923
+ font-size: 10px;
924
+ color: var(--ink-2);
925
+ text-transform: uppercase;
926
+ letter-spacing: 0.08em;
927
+ padding-top: var(--sp-2);
928
+ border-top: 1px dashed var(--line-2);
929
+ }
930
+
931
+ /* ============================================
932
+ Console
933
+ ============================================ */
934
+ .console {
935
+ display: flex;
936
+ flex-direction: column;
937
+ height: 100%;
938
+ background: var(--bg-0);
939
+ }
940
+
941
+ .console-header {
942
+ display: flex;
943
+ align-items: center;
944
+ gap: var(--sp-3);
945
+ padding: var(--sp-2) var(--sp-4);
946
+ border-bottom: 1px solid var(--line-2);
947
+ background: var(--bg-1);
948
+ font-family: var(--font-mono);
949
+ font-size: 10px;
950
+ text-transform: uppercase;
951
+ letter-spacing: 0.1em;
952
+ color: var(--ink-2);
953
+ flex-shrink: 0;
954
+ }
955
+
956
+ .console-body {
957
+ flex: 1;
958
+ overflow-y: auto;
959
+ padding: var(--sp-2) var(--sp-3);
960
+ font-family: var(--font-mono);
961
+ font-size: 11px;
962
+ line-height: 1.5;
963
+ }
964
+
965
+ .log-row {
966
+ display: grid;
967
+ grid-template-columns: 60px 50px 90px 1fr;
968
+ gap: var(--sp-3);
969
+ padding: 2px var(--sp-2);
970
+ border-left: 2px solid transparent;
971
+ animation: log-slide 200ms ease;
972
+ }
973
+
974
+ .log-row:hover {
975
+ background: var(--bg-2);
976
+ }
977
+
978
+ @keyframes log-slide {
979
+ from { opacity: 0; transform: translateX(-4px); }
980
+ to { opacity: 1; transform: translateX(0); }
981
+ }
982
+
983
+ .log-ts { color: var(--ink-3); }
984
+ .log-level { font-weight: 600; text-align: center; border-radius: 1px; }
985
+ .log-level.INFO { color: var(--accent-cyan); }
986
+ .log-level.OK { color: var(--accent-green); }
987
+ .log-level.WARN { color: var(--accent-amber); }
988
+ .log-level.ERR { color: var(--accent-red); }
989
+ .log-scope { color: var(--ink-2); }
990
+ .log-msg { color: var(--ink-0); word-break: break-all; }
991
+
992
+ .log-row.ERR { border-left-color: var(--accent-red); }
993
+ .log-row.WARN { border-left-color: var(--accent-amber); }
994
+ .log-row.OK { border-left-color: var(--accent-green); }
995
+
996
+ /* ============================================
997
+ Drawer
998
+ ============================================ */
999
+ .drawer-mask {
1000
+ position: absolute;
1001
+ inset: 0;
1002
+ background: rgba(0, 0, 0, 0.5);
1003
+ z-index: 100;
1004
+ animation: fade-in 200ms ease;
1005
+ }
1006
+
1007
+ .drawer {
1008
+ position: absolute;
1009
+ top: 0;
1010
+ right: 0;
1011
+ bottom: 0;
1012
+ width: 520px;
1013
+ max-width: 90%;
1014
+ background: var(--bg-1);
1015
+ border-left: 1px solid var(--line-2);
1016
+ z-index: 101;
1017
+ display: flex;
1018
+ flex-direction: column;
1019
+ animation: slide-in 220ms ease;
1020
+ box-shadow: -8px 0 32px rgba(0, 0, 0, 0.5);
1021
+ }
1022
+
1023
+ @keyframes fade-in {
1024
+ from { opacity: 0; }
1025
+ to { opacity: 1; }
1026
+ }
1027
+
1028
+ @keyframes slide-in {
1029
+ from { transform: translateX(100%); }
1030
+ to { transform: translateX(0); }
1031
+ }
1032
+
1033
+ .drawer-header {
1034
+ padding: var(--sp-4) var(--sp-5);
1035
+ border-bottom: 1px solid var(--line-2);
1036
+ display: flex;
1037
+ align-items: center;
1038
+ gap: var(--sp-3);
1039
+ font-family: var(--font-display);
1040
+ text-transform: uppercase;
1041
+ letter-spacing: 0.06em;
1042
+ font-size: 14px;
1043
+ }
1044
+
1045
+ .drawer-close {
1046
+ margin-left: auto;
1047
+ width: 28px;
1048
+ height: 28px;
1049
+ display: grid;
1050
+ place-items: center;
1051
+ border: 1px solid var(--line-2);
1052
+ color: var(--ink-1);
1053
+ }
1054
+
1055
+ .drawer-close:hover {
1056
+ background: var(--bg-2);
1057
+ color: var(--ink-0);
1058
+ }
1059
+
1060
+ .drawer-body {
1061
+ flex: 1;
1062
+ overflow-y: auto;
1063
+ padding: var(--sp-5);
1064
+ }
1065
+
1066
+ .drawer-footer {
1067
+ padding: var(--sp-4) var(--sp-5);
1068
+ border-top: 1px solid var(--line-2);
1069
+ display: flex;
1070
+ gap: var(--sp-2);
1071
+ justify-content: flex-end;
1072
+ }
1073
+
1074
+ /* ============================================
1075
+ Misc
1076
+ ============================================ */
1077
+ .kbd {
1078
+ display: inline-block;
1079
+ padding: 1px 6px;
1080
+ font-family: var(--font-mono);
1081
+ font-size: 10px;
1082
+ background: var(--bg-1);
1083
+ border: 1px solid var(--line-2);
1084
+ border-bottom-width: 2px;
1085
+ color: var(--ink-1);
1086
+ border-radius: 2px;
1087
+ }
1088
+
1089
+ .divider {
1090
+ height: 1px;
1091
+ background: var(--line-2);
1092
+ margin: var(--sp-4) 0;
1093
+ }
1094
+
1095
+ .tag {
1096
+ display: inline-flex;
1097
+ align-items: center;
1098
+ font-family: var(--font-mono);
1099
+ font-size: 9px;
1100
+ text-transform: uppercase;
1101
+ letter-spacing: 0.1em;
1102
+ padding: 2px 6px;
1103
+ border: 1px solid var(--line-2);
1104
+ color: var(--ink-2);
1105
+ border-radius: 1px;
1106
+ }
1107
+
1108
+ .tag.amber { color: var(--accent-amber); border-color: var(--accent-amber-dim); }
1109
+ .tag.green { color: var(--accent-green); border-color: var(--accent-green-dim); }
1110
+ .tag.red { color: var(--accent-red); border-color: var(--accent-red-dim); }
1111
+ .tag.cyan { color: var(--accent-cyan); border-color: var(--accent-cyan-dim); }
1112
+
1113
+ .empty {
1114
+ text-align: center;
1115
+ padding: var(--sp-10) var(--sp-5);
1116
+ color: var(--ink-2);
1117
+ font-family: var(--font-mono);
1118
+ font-size: 12px;
1119
+ }
1120
+
1121
+ .empty-title {
1122
+ font-family: var(--font-display);
1123
+ font-size: 16px;
1124
+ text-transform: uppercase;
1125
+ letter-spacing: 0.06em;
1126
+ color: var(--ink-1);
1127
+ margin-bottom: var(--sp-2);
1128
+ }
1129
+
1130
+ .copyable {
1131
+ cursor: pointer;
1132
+ position: relative;
1133
+ }
1134
+
1135
+ .copyable:hover::after {
1136
+ content: '点击复制';
1137
+ position: absolute;
1138
+ top: -22px;
1139
+ left: 50%;
1140
+ transform: translateX(-50%);
1141
+ font-size: 9px;
1142
+ background: var(--bg-3);
1143
+ color: var(--ink-1);
1144
+ padding: 2px 6px;
1145
+ border: 1px solid var(--line-2);
1146
+ pointer-events: none;
1147
+ white-space: nowrap;
1148
+ font-family: var(--font-mono);
1149
+ text-transform: uppercase;
1150
+ letter-spacing: 0.1em;
1151
+ }
1152
+
1153
+ /* Quick switch widget on dashboard */
1154
+ .quick-switch {
1155
+ display: grid;
1156
+ grid-template-columns: 1fr 1fr 1fr auto;
1157
+ gap: var(--sp-3);
1158
+ align-items: end;
1159
+ }
1160
+
1161
+ @media (max-width: 900px) {
1162
+ .app-shell {
1163
+ grid-template-columns: 1fr;
1164
+ grid-template-areas:
1165
+ 'main'
1166
+ 'console';
1167
+ }
1168
+ .app-sidebar { display: none; }
1169
+ .quick-switch { grid-template-columns: 1fr; }
1170
+ }
1171
+
1172
+ /* ============================================
1173
+ Install Wizard
1174
+ ============================================ */
1175
+ .install-page {
1176
+ min-height: 100vh;
1177
+ display: flex;
1178
+ align-items: center;
1179
+ justify-content: center;
1180
+ padding: 24px;
1181
+ background: var(--bg-0);
1182
+ background-image:
1183
+ radial-gradient(800px circle at 20% 0%, rgba(16, 185, 129, 0.06), transparent 50%),
1184
+ radial-gradient(600px circle at 80% 100%, rgba(99, 102, 241, 0.05), transparent 50%);
1185
+ }
1186
+ .install-card {
1187
+ width: 100%;
1188
+ max-width: 640px;
1189
+ background: var(--bg-1);
1190
+ border: 1px solid var(--border-1, #1f2937);
1191
+ border-radius: 16px;
1192
+ padding: 32px;
1193
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
1194
+ }
1195
+ .install-header {
1196
+ display: flex;
1197
+ align-items: center;
1198
+ gap: 16px;
1199
+ margin-bottom: 24px;
1200
+ padding-bottom: 20px;
1201
+ border-bottom: 1px solid var(--border-1, #1f2937);
1202
+ }
1203
+ .install-header h1 {
1204
+ margin: 0 0 4px 0;
1205
+ font-size: 22px;
1206
+ font-weight: 700;
1207
+ color: var(--ink-0, #f3f4f6);
1208
+ }
1209
+ .install-subtitle {
1210
+ margin: 0;
1211
+ font-size: 13px;
1212
+ color: var(--ink-3, #9ca3af);
1213
+ }
1214
+ .install-checks {
1215
+ display: flex;
1216
+ flex-direction: column;
1217
+ gap: 10px;
1218
+ margin-bottom: 20px;
1219
+ }
1220
+ .check-row {
1221
+ display: flex;
1222
+ align-items: center;
1223
+ gap: 12px;
1224
+ padding: 10px 14px;
1225
+ background: var(--bg-2, #141a23);
1226
+ border: 1px solid var(--border-1, #1f2937);
1227
+ border-radius: 8px;
1228
+ font-size: 13px;
1229
+ }
1230
+ .check-ok { color: #10b981; flex-shrink: 0; }
1231
+ .check-warn { color: #f59e0b; flex-shrink: 0; }
1232
+ .check-label { font-weight: 600; color: var(--ink-0, #f3f4f6); min-width: 110px; }
1233
+ .check-detail {
1234
+ font-family: var(--font-mono, monospace);
1235
+ font-size: 12px;
1236
+ color: var(--ink-3, #9ca3af);
1237
+ overflow: hidden;
1238
+ text-overflow: ellipsis;
1239
+ white-space: nowrap;
1240
+ flex: 1;
1241
+ min-width: 0;
1242
+ }
1243
+ .install-error {
1244
+ display: flex;
1245
+ gap: 14px;
1246
+ align-items: flex-start;
1247
+ padding: 16px;
1248
+ background: rgba(245, 158, 11, 0.08);
1249
+ border: 1px solid rgba(245, 158, 11, 0.3);
1250
+ border-radius: 10px;
1251
+ margin-bottom: 20px;
1252
+ }
1253
+ .install-error strong { color: #f59e0b; display: block; margin-bottom: 4px; }
1254
+ .install-error p { margin: 4px 0; font-size: 13px; color: var(--ink-2, #d1d5db); }
1255
+ .install-error a { color: #10b981; }
1256
+ .install-error button { flex-shrink: 0; align-self: center; }
1257
+ .install-action {
1258
+ display: flex;
1259
+ flex-direction: column;
1260
+ gap: 10px;
1261
+ align-items: center;
1262
+ margin-bottom: 20px;
1263
+ }
1264
+ .btn-large {
1265
+ padding: 14px 28px;
1266
+ font-size: 15px;
1267
+ font-weight: 600;
1268
+ display: flex;
1269
+ align-items: center;
1270
+ gap: 10px;
1271
+ }
1272
+ .install-hint {
1273
+ margin: 0;
1274
+ font-size: 12px;
1275
+ color: var(--ink-3, #9ca3af);
1276
+ text-align: center;
1277
+ }
1278
+ .install-hint code {
1279
+ background: var(--bg-2, #141a23);
1280
+ padding: 2px 6px;
1281
+ border-radius: 4px;
1282
+ font-size: 11px;
1283
+ }
1284
+ .install-running {
1285
+ display: flex;
1286
+ align-items: center;
1287
+ gap: 10px;
1288
+ font-size: 14px;
1289
+ color: var(--ink-1, #e5e7eb);
1290
+ font-weight: 500;
1291
+ }
1292
+ .install-done {
1293
+ display: flex;
1294
+ align-items: center;
1295
+ gap: 14px;
1296
+ padding: 16px;
1297
+ background: rgba(16, 185, 129, 0.08);
1298
+ border: 1px solid rgba(16, 185, 129, 0.3);
1299
+ border-radius: 10px;
1300
+ margin-bottom: 20px;
1301
+ }
1302
+ .install-done strong { color: #10b981; display: block; }
1303
+ .install-done p { margin: 4px 0 0 0; font-size: 13px; color: var(--ink-3, #9ca3af); }
1304
+ .install-logs {
1305
+ max-height: 240px;
1306
+ overflow-y: auto;
1307
+ background: #000;
1308
+ border: 1px solid var(--border-1, #1f2937);
1309
+ border-radius: 8px;
1310
+ padding: 12px;
1311
+ font-family: var(--font-mono, monospace);
1312
+ font-size: 11px;
1313
+ line-height: 1.5;
1314
+ margin-bottom: 16px;
1315
+ }
1316
+ .log-line { color: #d1d5db; white-space: pre-wrap; word-break: break-all; }
1317
+ .log-err { color: #f87171; white-space: pre-wrap; word-break: break-all; }
1318
+ .install-error-text {
1319
+ color: #f87171;
1320
+ font-size: 12px;
1321
+ padding: 8px 12px;
1322
+ background: rgba(248, 113, 113, 0.08);
1323
+ border-radius: 6px;
1324
+ margin-bottom: 12px;
1325
+ }
1326
+ .install-footer {
1327
+ display: flex;
1328
+ justify-content: space-between;
1329
+ align-items: center;
1330
+ font-size: 11px;
1331
+ color: var(--ink-3, #9ca3af);
1332
+ padding-top: 16px;
1333
+ border-top: 1px solid var(--border-1, #1f2937);
1334
+ }
1335
+ .install-footer code {
1336
+ background: var(--bg-2, #141a23);
1337
+ padding: 2px 6px;
1338
+ border-radius: 4px;
1339
+ font-size: 10px;
1340
+ }
1341
+ .install-link {
1342
+ display: flex;
1343
+ align-items: center;
1344
+ gap: 4px;
1345
+ color: var(--ink-3, #9ca3af);
1346
+ text-decoration: none;
1347
+ }
1348
+ .install-link:hover { color: #10b981; }
1349
+ .spin { animation: spin 1s linear infinite; }
1350
+ @keyframes spin { to { transform: rotate(360deg); } }