abm1click-openclaw 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.
@@ -0,0 +1,991 @@
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>OneClick OpenClaw — Dashboard</title>
7
+ <meta name="description" content="OneClick OpenClaw Launcher Dashboard — Theo dõi trạng thái hệ thống, services, và cấu hình">
8
+ <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800&display=swap" rel="stylesheet">
9
+ <style>
10
+ /* ─── ABMedu Design System ─────────────────── */
11
+ :root {
12
+ --navy: #04104E;
13
+ --navy-light: #0a1a6b;
14
+ --red: #EE2B2A;
15
+ --red-hover: #d42524;
16
+ --bg-main: #F9F9FA;
17
+ --bg-white: #FFFFFF;
18
+ --bg-card: rgba(255,255,255,0.85);
19
+ --text-primary: #1a1a2e;
20
+ --text-secondary: #555770;
21
+ --text-muted: #8e90a6;
22
+ --border-light: rgba(4,16,78,0.08);
23
+ --shadow-card: 0 4px 24px rgba(4,16,78,0.06);
24
+ --shadow-hover: 0 8px 40px rgba(4,16,78,0.12);
25
+ --radius: 12px;
26
+ --radius-lg: 16px;
27
+ --gradient-red: linear-gradient(135deg, #EE2B2A, #ff6b6b);
28
+ --gradient-navy: linear-gradient(135deg, #04104E, #1a2980);
29
+ --gradient-success: linear-gradient(135deg, #10b981, #059669);
30
+ --gradient-warning: linear-gradient(135deg, #f59e0b, #d97706);
31
+ --glass-bg: rgba(255,255,255,0.65);
32
+ --glass-border: rgba(4,16,78,0.08);
33
+ }
34
+
35
+ * { margin: 0; padding: 0; box-sizing: border-box; }
36
+
37
+ body {
38
+ font-family: 'Montserrat', system-ui, -apple-system, sans-serif;
39
+ background: var(--bg-main);
40
+ color: var(--text-primary);
41
+ min-height: 100vh;
42
+ overflow-x: hidden;
43
+ }
44
+
45
+ /* ─── Background Orbs (ABMedu style) ────────── */
46
+ .bg-orbs {
47
+ position: fixed;
48
+ inset: 0;
49
+ overflow: hidden;
50
+ pointer-events: none;
51
+ z-index: 0;
52
+ }
53
+ .bg-orbs::before,
54
+ .bg-orbs::after {
55
+ content: '';
56
+ position: absolute;
57
+ border-radius: 50%;
58
+ filter: blur(120px);
59
+ opacity: 0.15;
60
+ }
61
+ .bg-orbs::before {
62
+ width: 500px; height: 500px;
63
+ background: var(--red);
64
+ top: -100px; right: -100px;
65
+ animation: float 20s ease-in-out infinite;
66
+ }
67
+ .bg-orbs::after {
68
+ width: 400px; height: 400px;
69
+ background: var(--navy);
70
+ bottom: -50px; left: -50px;
71
+ animation: float 25s ease-in-out infinite reverse;
72
+ }
73
+ @keyframes float {
74
+ 0%, 100% { transform: translate(0, 0) scale(1); }
75
+ 33% { transform: translate(30px, -30px) scale(1.05); }
76
+ 66% { transform: translate(-20px, 20px) scale(0.95); }
77
+ }
78
+
79
+ /* ─── Layout ──────────────────────────────────── */
80
+ .app-container {
81
+ position: relative;
82
+ z-index: 1;
83
+ max-width: 1200px;
84
+ margin: 0 auto;
85
+ padding: 0 24px;
86
+ }
87
+
88
+ /* ─── Nav Header ─────────────────────────────── */
89
+ .nav-header {
90
+ display: flex;
91
+ align-items: center;
92
+ justify-content: space-between;
93
+ padding: 20px 0;
94
+ border-bottom: 1px solid var(--border-light);
95
+ margin-bottom: 32px;
96
+ }
97
+ .nav-brand {
98
+ display: flex;
99
+ align-items: center;
100
+ gap: 12px;
101
+ }
102
+ .nav-logo {
103
+ width: 40px; height: 40px;
104
+ background: var(--gradient-red);
105
+ border-radius: 10px;
106
+ display: flex;
107
+ align-items: center;
108
+ justify-content: center;
109
+ color: #fff;
110
+ font-weight: 800;
111
+ font-size: 14px;
112
+ letter-spacing: -0.5px;
113
+ }
114
+ .nav-title {
115
+ font-size: 18px;
116
+ font-weight: 700;
117
+ color: var(--navy);
118
+ letter-spacing: -0.3px;
119
+ }
120
+ .nav-subtitle {
121
+ font-size: 11px;
122
+ color: var(--text-muted);
123
+ font-weight: 500;
124
+ text-transform: uppercase;
125
+ letter-spacing: 1px;
126
+ }
127
+ .nav-actions {
128
+ display: flex;
129
+ gap: 12px;
130
+ align-items: center;
131
+ }
132
+ .nav-badge {
133
+ display: inline-flex;
134
+ align-items: center;
135
+ gap: 6px;
136
+ padding: 6px 14px;
137
+ border-radius: 20px;
138
+ font-size: 12px;
139
+ font-weight: 600;
140
+ }
141
+ .badge-ok { background: #e6f9f0; color: #059669; }
142
+ .badge-warn { background: #fef3cd; color: #d97706; }
143
+ .badge-fail { background: #fde8e8; color: #dc2626; }
144
+ .badge-dot {
145
+ width: 8px; height: 8px;
146
+ border-radius: 50%;
147
+ display: inline-block;
148
+ }
149
+ .badge-dot.ok { background: #10b981; animation: pulse-green 2s infinite; }
150
+ .badge-dot.warn { background: #f59e0b; }
151
+ .badge-dot.fail { background: #ef4444; }
152
+
153
+ @keyframes pulse-green {
154
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(16,185,129,0.5); }
155
+ 50% { box-shadow: 0 0 0 6px rgba(16,185,129,0); }
156
+ }
157
+
158
+ /* ─── Hero Section ────────────────────────────── */
159
+ .hero-section {
160
+ background: var(--gradient-navy);
161
+ border-radius: var(--radius-lg);
162
+ padding: 48px 40px;
163
+ margin-bottom: 32px;
164
+ position: relative;
165
+ overflow: hidden;
166
+ }
167
+ .hero-section::before {
168
+ content: '';
169
+ position: absolute;
170
+ width: 300px; height: 300px;
171
+ background: rgba(238,43,42,0.15);
172
+ border-radius: 50%;
173
+ filter: blur(80px);
174
+ top: -80px; right: -40px;
175
+ }
176
+ .hero-section::after {
177
+ content: '';
178
+ position: absolute;
179
+ width: 200px; height: 200px;
180
+ background: rgba(255,255,255,0.05);
181
+ border-radius: 50%;
182
+ filter: blur(60px);
183
+ bottom: -60px; left: 20%;
184
+ }
185
+ .hero-content { position: relative; z-index: 1; }
186
+ .hero-label {
187
+ display: inline-flex;
188
+ align-items: center;
189
+ gap: 6px;
190
+ font-size: 11px;
191
+ color: rgba(255,255,255,0.6);
192
+ font-weight: 600;
193
+ text-transform: uppercase;
194
+ letter-spacing: 2px;
195
+ margin-bottom: 12px;
196
+ }
197
+ .hero-label .dot {
198
+ width: 6px; height: 6px;
199
+ background: var(--red);
200
+ border-radius: 50%;
201
+ }
202
+ .hero-title {
203
+ font-size: 32px;
204
+ font-weight: 800;
205
+ color: #fff;
206
+ letter-spacing: -0.5px;
207
+ margin-bottom: 8px;
208
+ }
209
+ .hero-title span { color: var(--red); }
210
+ .hero-description {
211
+ font-size: 14px;
212
+ color: rgba(255,255,255,0.65);
213
+ margin-bottom: 28px;
214
+ max-width: 500px;
215
+ line-height: 1.6;
216
+ }
217
+ .hero-stats {
218
+ display: flex;
219
+ gap: 40px;
220
+ }
221
+ .hero-stat { text-align: left; }
222
+ .hero-stat-value {
223
+ font-size: 28px;
224
+ font-weight: 800;
225
+ color: #fff;
226
+ }
227
+ .hero-stat-value span { color: var(--red); }
228
+ .hero-stat-label {
229
+ font-size: 11px;
230
+ color: rgba(255,255,255,0.5);
231
+ font-weight: 500;
232
+ text-transform: uppercase;
233
+ letter-spacing: 1px;
234
+ }
235
+
236
+ /* ─── Section Headers ─────────────────────────── */
237
+ .section-header {
238
+ text-align: center;
239
+ margin-bottom: 24px;
240
+ }
241
+ .section-tag {
242
+ display: inline-flex;
243
+ align-items: center;
244
+ gap: 6px;
245
+ font-size: 11px;
246
+ color: var(--red);
247
+ font-weight: 700;
248
+ text-transform: uppercase;
249
+ letter-spacing: 2px;
250
+ margin-bottom: 8px;
251
+ }
252
+ .section-title {
253
+ font-size: 24px;
254
+ font-weight: 800;
255
+ color: var(--navy);
256
+ letter-spacing: -0.3px;
257
+ }
258
+ .section-title em {
259
+ font-style: normal;
260
+ color: var(--red);
261
+ }
262
+
263
+ /* ─── Grid ────────────────────────────────────── */
264
+ .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
265
+ .grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 20px; }
266
+ .grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
267
+
268
+ /* ─── Cards ────────────────────────────────────── */
269
+ .card {
270
+ background: var(--bg-card);
271
+ backdrop-filter: blur(12px);
272
+ border: 1px solid var(--glass-border);
273
+ border-radius: var(--radius);
274
+ padding: 24px;
275
+ transition: all 0.3s ease;
276
+ }
277
+ .card:hover {
278
+ transform: translateY(-2px);
279
+ box-shadow: var(--shadow-hover);
280
+ border-color: rgba(238,43,42,0.15);
281
+ }
282
+ .card-icon {
283
+ width: 48px; height: 48px;
284
+ border-radius: 12px;
285
+ display: flex;
286
+ align-items: center;
287
+ justify-content: center;
288
+ font-size: 20px;
289
+ margin-bottom: 16px;
290
+ }
291
+ .card-icon.red { background: rgba(238,43,42,0.1); color: var(--red); }
292
+ .card-icon.navy { background: rgba(4,16,78,0.08); color: var(--navy); }
293
+ .card-icon.green { background: rgba(16,185,129,0.1); color: #059669; }
294
+ .card-icon.amber { background: rgba(245,158,11,0.1); color: #d97706; }
295
+
296
+ .card-title {
297
+ font-size: 14px;
298
+ font-weight: 700;
299
+ color: var(--navy);
300
+ margin-bottom: 4px;
301
+ }
302
+ .card-value {
303
+ font-size: 28px;
304
+ font-weight: 800;
305
+ color: var(--text-primary);
306
+ margin-bottom: 4px;
307
+ }
308
+ .card-desc {
309
+ font-size: 12px;
310
+ color: var(--text-muted);
311
+ line-height: 1.5;
312
+ }
313
+
314
+ /* ─── Status Table ─────────────────────────────── */
315
+ .steps-list {
316
+ display: flex;
317
+ flex-direction: column;
318
+ gap: 2px;
319
+ }
320
+ .step-item {
321
+ display: flex;
322
+ align-items: center;
323
+ gap: 12px;
324
+ padding: 14px 16px;
325
+ border-radius: 8px;
326
+ transition: background 0.2s;
327
+ }
328
+ .step-item:hover { background: rgba(4,16,78,0.03); }
329
+ .step-num {
330
+ width: 28px; height: 28px;
331
+ border-radius: 8px;
332
+ display: flex;
333
+ align-items: center;
334
+ justify-content: center;
335
+ font-size: 11px;
336
+ font-weight: 700;
337
+ flex-shrink: 0;
338
+ }
339
+ .step-num.complete { background: var(--gradient-success); color: #fff; }
340
+ .step-num.running { background: var(--gradient-red); color: #fff; animation: pulse-red 2s infinite; }
341
+ .step-num.pending { background: rgba(4,16,78,0.06); color: var(--text-muted); }
342
+ .step-num.failed { background: #fde8e8; color: #dc2626; }
343
+
344
+ @keyframes pulse-red {
345
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(238,43,42,0.4); }
346
+ 50% { box-shadow: 0 0 0 6px rgba(238,43,42,0); }
347
+ }
348
+
349
+ .step-name {
350
+ font-size: 13px;
351
+ font-weight: 600;
352
+ color: var(--text-primary);
353
+ flex: 1;
354
+ }
355
+ .step-status {
356
+ font-size: 11px;
357
+ font-weight: 600;
358
+ text-transform: uppercase;
359
+ letter-spacing: 0.5px;
360
+ }
361
+ .step-status.complete { color: #059669; }
362
+ .step-status.running { color: var(--red); }
363
+ .step-status.pending { color: var(--text-muted); }
364
+ .step-status.failed { color: #dc2626; }
365
+ .step-time {
366
+ font-size: 11px;
367
+ color: var(--text-muted);
368
+ min-width: 60px;
369
+ text-align: right;
370
+ }
371
+
372
+ /* ─── Service Cards ────────────────────────────── */
373
+ .service-card {
374
+ background: var(--bg-white);
375
+ border: 1px solid var(--border-light);
376
+ border-radius: var(--radius);
377
+ padding: 24px;
378
+ position: relative;
379
+ overflow: hidden;
380
+ }
381
+ .service-card::before {
382
+ content: '';
383
+ position: absolute;
384
+ top: 0; left: 0; right: 0;
385
+ height: 3px;
386
+ }
387
+ .service-card.active::before { background: var(--gradient-success); }
388
+ .service-card.inactive::before { background: var(--gradient-warning); }
389
+ .service-header {
390
+ display: flex;
391
+ align-items: center;
392
+ justify-content: space-between;
393
+ margin-bottom: 20px;
394
+ }
395
+ .service-name {
396
+ font-size: 16px;
397
+ font-weight: 700;
398
+ color: var(--navy);
399
+ }
400
+ .service-badge {
401
+ display: inline-flex;
402
+ align-items: center;
403
+ gap: 5px;
404
+ padding: 4px 10px;
405
+ border-radius: 16px;
406
+ font-size: 11px;
407
+ font-weight: 600;
408
+ }
409
+ .service-badge.online { background: #e6f9f0; color: #059669; }
410
+ .service-badge.offline { background: #fef3cd; color: #d97706; }
411
+ .service-info {
412
+ display: flex;
413
+ flex-direction: column;
414
+ gap: 10px;
415
+ }
416
+ .service-row {
417
+ display: flex;
418
+ justify-content: space-between;
419
+ font-size: 13px;
420
+ }
421
+ .service-row-key { color: var(--text-muted); font-weight: 500; }
422
+ .service-row-val { color: var(--text-primary); font-weight: 600; font-family: 'JetBrains Mono', monospace; }
423
+
424
+ /* ─── CTA Button ──────────────────────────────── */
425
+ .btn {
426
+ display: inline-flex;
427
+ align-items: center;
428
+ gap: 8px;
429
+ padding: 12px 24px;
430
+ border-radius: 8px;
431
+ font-family: 'Montserrat', sans-serif;
432
+ font-size: 13px;
433
+ font-weight: 700;
434
+ border: none;
435
+ cursor: pointer;
436
+ transition: all 0.3s ease;
437
+ text-decoration: none;
438
+ }
439
+ .btn-primary {
440
+ background: var(--gradient-red);
441
+ color: #fff;
442
+ box-shadow: 0 4px 14px rgba(238,43,42,0.3);
443
+ }
444
+ .btn-primary:hover {
445
+ transform: translateY(-1px);
446
+ box-shadow: 0 6px 20px rgba(238,43,42,0.4);
447
+ }
448
+ .btn-outline {
449
+ background: var(--glass-bg);
450
+ backdrop-filter: blur(8px);
451
+ color: var(--navy);
452
+ border: 1px solid var(--glass-border);
453
+ }
454
+ .btn-outline:hover {
455
+ background: rgba(4,16,78,0.04);
456
+ border-color: rgba(4,16,78,0.15);
457
+ }
458
+
459
+ /* ─── Quicklinks ──────────────────────────────── */
460
+ .quicklink {
461
+ display: flex;
462
+ align-items: center;
463
+ gap: 12px;
464
+ padding: 16px;
465
+ border-radius: 10px;
466
+ background: var(--bg-white);
467
+ border: 1px solid var(--border-light);
468
+ cursor: pointer;
469
+ transition: all 0.3s;
470
+ text-decoration: none;
471
+ color: inherit;
472
+ }
473
+ .quicklink:hover {
474
+ border-color: rgba(238,43,42,0.2);
475
+ box-shadow: var(--shadow-card);
476
+ transform: translateY(-1px);
477
+ }
478
+ .quicklink-icon {
479
+ width: 40px; height: 40px;
480
+ border-radius: 10px;
481
+ display: flex;
482
+ align-items: center;
483
+ justify-content: center;
484
+ font-size: 18px;
485
+ flex-shrink: 0;
486
+ }
487
+ .quicklink-title {
488
+ font-size: 13px;
489
+ font-weight: 700;
490
+ color: var(--navy);
491
+ }
492
+ .quicklink-desc {
493
+ font-size: 11px;
494
+ color: var(--text-muted);
495
+ }
496
+
497
+ /* ─── Footer ──────────────────────────────────── */
498
+ .footer {
499
+ text-align: center;
500
+ padding: 40px 0;
501
+ margin-top: 48px;
502
+ border-top: 1px solid var(--border-light);
503
+ }
504
+ .footer-text {
505
+ font-size: 12px;
506
+ color: var(--text-muted);
507
+ font-weight: 500;
508
+ }
509
+ .footer-text a {
510
+ color: var(--red);
511
+ text-decoration: none;
512
+ font-weight: 600;
513
+ }
514
+
515
+ /* ─── Responsive ──────────────────────────────── */
516
+ @media (max-width: 768px) {
517
+ .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
518
+ .hero-stats { flex-direction: column; gap: 16px; }
519
+ .hero-section { padding: 32px 24px; }
520
+ .hero-title { font-size: 24px; }
521
+ .nav-actions { display: none; }
522
+ }
523
+
524
+ /* ─── Animations ──────────────────────────────── */
525
+ .fade-in {
526
+ animation: fadeInUp 0.5s ease forwards;
527
+ opacity: 0;
528
+ }
529
+ .fade-in:nth-child(1) { animation-delay: 0.05s; }
530
+ .fade-in:nth-child(2) { animation-delay: 0.1s; }
531
+ .fade-in:nth-child(3) { animation-delay: 0.15s; }
532
+ .fade-in:nth-child(4) { animation-delay: 0.2s; }
533
+
534
+ @keyframes fadeInUp {
535
+ from { opacity: 0; transform: translateY(12px); }
536
+ to { opacity: 1; transform: translateY(0); }
537
+ }
538
+
539
+ /* ─── Refresh Indicator ───────────────────────── */
540
+ .refresh-bar {
541
+ height: 2px;
542
+ background: var(--gradient-red);
543
+ position: fixed;
544
+ top: 0;
545
+ left: 0;
546
+ z-index: 999;
547
+ animation: loadBar 3s ease-in-out infinite;
548
+ width: 0;
549
+ }
550
+ @keyframes loadBar {
551
+ 0% { width: 0; left: 0; }
552
+ 50% { width: 60%; left: 20%; }
553
+ 100% { width: 0; left: 100%; }
554
+ }
555
+ </style>
556
+ </head>
557
+ <body>
558
+ <div class="bg-orbs"></div>
559
+ <div class="refresh-bar" id="refreshBar" style="display:none;"></div>
560
+
561
+ <div class="app-container">
562
+ <!-- ─── Nav ────────────────────────────────── -->
563
+ <nav class="nav-header fade-in">
564
+ <div class="nav-brand">
565
+ <div class="nav-logo">1C</div>
566
+ <div>
567
+ <div class="nav-title">OneClick OpenClaw</div>
568
+ <div class="nav-subtitle">Dashboard v1.0</div>
569
+ </div>
570
+ </div>
571
+ <div class="nav-actions">
572
+ <span class="nav-badge badge-ok" id="systemBadge">
573
+ <span class="badge-dot ok"></span>
574
+ Hệ thống hoạt động
575
+ </span>
576
+ <button class="btn btn-outline" onclick="refreshDashboard()">⟳ Làm mới</button>
577
+ </div>
578
+ </nav>
579
+
580
+ <!-- ─── Hero ───────────────────────────────── -->
581
+ <section class="hero-section fade-in">
582
+ <div class="hero-content">
583
+ <div class="hero-label">
584
+ <span class="dot"></span>
585
+ OneClick Launcher
586
+ </div>
587
+ <h1 class="hero-title">Quản lý <span>OpenClaw</span> & 9Router</h1>
588
+ <p class="hero-description">
589
+ Dashboard giám sát trạng thái hệ thống, dịch vụ, và tiến trình cài đặt.
590
+ Theo dõi 9Router gateway, OpenClaw config, và kết quả smoke test real-time.
591
+ </p>
592
+ <div class="hero-stats">
593
+ <div class="hero-stat">
594
+ <div class="hero-stat-value" id="statSteps">14</div>
595
+ <div class="hero-stat-label">Install Steps</div>
596
+ </div>
597
+ <div class="hero-stat">
598
+ <div class="hero-stat-value" id="statCompleted">0<span>/14</span></div>
599
+ <div class="hero-stat-label">Hoàn Thành</div>
600
+ </div>
601
+ <div class="hero-stat">
602
+ <div class="hero-stat-value" id="statServices">2</div>
603
+ <div class="hero-stat-label">Services</div>
604
+ </div>
605
+ <div class="hero-stat">
606
+ <div class="hero-stat-value" id="statUptime">--</div>
607
+ <div class="hero-stat-label">Uptime</div>
608
+ </div>
609
+ </div>
610
+ </div>
611
+ </section>
612
+
613
+ <!-- ─── Quick Stats ────────────────────────── -->
614
+ <div class="grid-4" style="margin-bottom: 32px;">
615
+ <div class="card fade-in">
616
+ <div class="card-icon red">🚀</div>
617
+ <div class="card-title">9Router</div>
618
+ <div class="card-value" id="card9router">--</div>
619
+ <div class="card-desc">AI Gateway Status</div>
620
+ </div>
621
+ <div class="card fade-in">
622
+ <div class="card-icon navy">🔧</div>
623
+ <div class="card-title">OpenClaw</div>
624
+ <div class="card-value" id="cardOpenClaw">--</div>
625
+ <div class="card-desc">CLI Config Status</div>
626
+ </div>
627
+ <div class="card fade-in">
628
+ <div class="card-icon green">🔑</div>
629
+ <div class="card-title">Auth</div>
630
+ <div class="card-value" id="cardAuth">--</div>
631
+ <div class="card-desc">Google OAuth</div>
632
+ </div>
633
+ <div class="card fade-in">
634
+ <div class="card-icon amber">🧪</div>
635
+ <div class="card-title">Smoke Test</div>
636
+ <div class="card-value" id="cardSmoke">--</div>
637
+ <div class="card-desc">Kiểm tra E2E</div>
638
+ </div>
639
+ </div>
640
+
641
+ <!-- ─── Services ───────────────────────────── -->
642
+ <div class="section-header">
643
+ <div class="section-tag">🔌 Dịch vụ hệ thống</div>
644
+ <h2 class="section-title">Services <em>Monitor</em></h2>
645
+ </div>
646
+
647
+ <div class="grid-2" style="margin-bottom: 40px;">
648
+ <!-- 9Router -->
649
+ <div class="service-card active fade-in" id="svc9router">
650
+ <div class="service-header">
651
+ <div class="service-name">9Router Gateway</div>
652
+ <span class="service-badge online" id="svc9routerBadge">
653
+ <span class="badge-dot ok"></span> Online
654
+ </span>
655
+ </div>
656
+ <div class="service-info">
657
+ <div class="service-row">
658
+ <span class="service-row-key">API Endpoint</span>
659
+ <span class="service-row-val">127.0.0.1:20128</span>
660
+ </div>
661
+ <div class="service-row">
662
+ <span class="service-row-key">Dashboard</span>
663
+ <span class="service-row-val">127.0.0.1:20120</span>
664
+ </div>
665
+ <div class="service-row">
666
+ <span class="service-row-key">Provider</span>
667
+ <span class="service-row-val" id="svcProvider">Google Gemini</span>
668
+ </div>
669
+ <div class="service-row">
670
+ <span class="service-row-key">API Key</span>
671
+ <span class="service-row-val" id="svcApiKey">sk_****••••</span>
672
+ </div>
673
+ </div>
674
+ </div>
675
+
676
+ <!-- OpenClaw -->
677
+ <div class="service-card active fade-in" id="svcOpenClaw">
678
+ <div class="service-header">
679
+ <div class="service-name">OpenClaw CLI</div>
680
+ <span class="service-badge online" id="svcOpenClawBadge">
681
+ <span class="badge-dot ok"></span> Configured
682
+ </span>
683
+ </div>
684
+ <div class="service-info">
685
+ <div class="service-row">
686
+ <span class="service-row-key">Config Path</span>
687
+ <span class="service-row-val" id="svcConfigPath">~/.openclaw/config.json</span>
688
+ </div>
689
+ <div class="service-row">
690
+ <span class="service-row-key">Default Model</span>
691
+ <span class="service-row-val" id="svcModel">gc/gemini-3-flash</span>
692
+ </div>
693
+ <div class="service-row">
694
+ <span class="service-row-key">Providers</span>
695
+ <span class="service-row-val" id="svcProviders">1 active</span>
696
+ </div>
697
+ <div class="service-row">
698
+ <span class="service-row-key">Last Patched</span>
699
+ <span class="service-row-val" id="svcLastPatch">--</span>
700
+ </div>
701
+ </div>
702
+ </div>
703
+ </div>
704
+
705
+ <!-- ─── Install Steps ─────────────────────── -->
706
+ <div class="section-header">
707
+ <div class="section-tag">📋 Tiến trình cài đặt</div>
708
+ <h2 class="section-title">Installation <em>Pipeline</em></h2>
709
+ </div>
710
+
711
+ <div class="card fade-in" style="margin-bottom: 40px;">
712
+ <!-- Step progress bar -->
713
+ <div style="margin-bottom: 20px;">
714
+ <div style="display:flex; justify-content:space-between; margin-bottom:8px;">
715
+ <span style="font-size:12px; font-weight:600; color:var(--text-secondary);">Tiến trình</span>
716
+ <span style="font-size:12px; font-weight:700; color:var(--red);" id="progressText">0%</span>
717
+ </div>
718
+ <div style="height:6px; background:rgba(4,16,78,0.06); border-radius:3px; overflow:hidden;">
719
+ <div id="progressBar" style="height:100%; width:0%; background:var(--gradient-red); border-radius:3px; transition:width 0.5s ease;"></div>
720
+ </div>
721
+ </div>
722
+
723
+ <div class="steps-list" id="stepsList">
724
+ <!-- Steps will be dynamically rendered -->
725
+ </div>
726
+ </div>
727
+
728
+ <!-- ─── Quick Links ────────────────────────── -->
729
+ <div class="section-header">
730
+ <div class="section-tag">⚡ Hành động nhanh</div>
731
+ <h2 class="section-title">Quick <em>Actions</em></h2>
732
+ </div>
733
+
734
+ <div class="grid-3" style="margin-bottom: 40px;">
735
+ <a class="quicklink fade-in" href="javascript:void(0)" onclick="runCmd('install')">
736
+ <div class="quicklink-icon" style="background:rgba(238,43,42,0.1); color:var(--red);">▶</div>
737
+ <div>
738
+ <div class="quicklink-title">Cài đặt mới</div>
739
+ <div class="quicklink-desc">oneclick-openclaw install</div>
740
+ </div>
741
+ </a>
742
+ <a class="quicklink fade-in" href="javascript:void(0)" onclick="runCmd('resume')">
743
+ <div class="quicklink-icon" style="background:rgba(4,16,78,0.08); color:var(--navy);">↻</div>
744
+ <div>
745
+ <div class="quicklink-title">Resume</div>
746
+ <div class="quicklink-desc">Tiếp tục từ checkpoint</div>
747
+ </div>
748
+ </a>
749
+ <a class="quicklink fade-in" href="javascript:void(0)" onclick="runCmd('doctor')">
750
+ <div class="quicklink-icon" style="background:rgba(16,185,129,0.1); color:#059669;">🩺</div>
751
+ <div>
752
+ <div class="quicklink-title">Doctor</div>
753
+ <div class="quicklink-desc">Kiểm tra sức khỏe hệ thống</div>
754
+ </div>
755
+ </a>
756
+ <a class="quicklink fade-in" href="javascript:void(0)" onclick="runCmd('auth google')">
757
+ <div class="quicklink-icon" style="background:rgba(245,158,11,0.1); color:#d97706;">🔐</div>
758
+ <div>
759
+ <div class="quicklink-title">Re-Auth Google</div>
760
+ <div class="quicklink-desc">Xác thực lại OAuth</div>
761
+ </div>
762
+ </a>
763
+ <a class="quicklink fade-in" href="javascript:void(0)" onclick="runCmd('logs')">
764
+ <div class="quicklink-icon" style="background:rgba(139,92,246,0.1); color:#7c3aed;">📄</div>
765
+ <div>
766
+ <div class="quicklink-title">Xem Logs</div>
767
+ <div class="quicklink-desc">Nhật ký hệ thống</div>
768
+ </div>
769
+ </a>
770
+ <a class="quicklink fade-in" href="javascript:void(0)" onclick="runCmd('reset')">
771
+ <div class="quicklink-icon" style="background:rgba(239,68,68,0.08); color:#dc2626;">🗑</div>
772
+ <div>
773
+ <div class="quicklink-title">Reset State</div>
774
+ <div class="quicklink-desc">Xóa state, bắt đầu lại</div>
775
+ </div>
776
+ </a>
777
+ </div>
778
+
779
+ <!-- ─── Footer ─────────────────────────────── -->
780
+ <footer class="footer">
781
+ <p class="footer-text">
782
+ Powered by <a href="https://abmedu.vn" target="_blank">ABM</a> — AI Business Master &bull;
783
+ OneClick OpenClaw Launcher v1.0
784
+ </p>
785
+ </footer>
786
+ </div>
787
+
788
+ <script>
789
+ // ─── Step Data ──────────────────────────────
790
+ const STEPS = [
791
+ { id: 'PRECHECK', name: 'Kiểm tra môi trường', icon: '🔍' },
792
+ { id: 'INSTALL_9ROUTER', name: 'Cài đặt 9Router', icon: '📦' },
793
+ { id: 'START_9ROUTER', name: 'Khởi động 9Router', icon: '🚀' },
794
+ { id: 'WAIT_9ROUTER_READY', name: 'Chờ 9Router sẵn sàng', icon: '⏳' },
795
+ { id: 'OPEN_AUTH_PAGE', name: 'Mở trang xác thực', icon: '🌐' },
796
+ { id: 'WAIT_GEMINI_AUTH', name: 'Chờ Google OAuth', icon: '🔑' },
797
+ { id: 'ENSURE_9ROUTER_API_KEY', name: 'Tạo API Key', icon: '🔐' },
798
+ { id: 'INSTALL_OPENCLAW', name: 'Cài đặt OpenClaw', icon: '📥' },
799
+ { id: 'LOCATE_OPENCLAW_CONFIG', name: 'Tìm config OpenClaw', icon: '📂' },
800
+ { id: 'PATCH_OPENCLAW_PROVIDER', name: 'Patch provider config', icon: '🔧' },
801
+ { id: 'SET_DEFAULT_MODEL', name: 'Thiết lập model mặc định', icon: '🤖' },
802
+ { id: 'VALIDATE_OPENCLAW_CONFIG', name: 'Xác thực config', icon: '✅' },
803
+ { id: 'SMOKE_TEST_9ROUTER', name: 'Smoke Test 9Router', icon: '🧪' },
804
+ { id: 'SMOKE_TEST_OPENCLAW', name: 'Smoke Test OpenClaw', icon: '🎯' },
805
+ ];
806
+
807
+ // ─── Render Steps ───────────────────────────
808
+ function renderSteps(stateData) {
809
+ const list = document.getElementById('stepsList');
810
+ const completedSteps = stateData?.completedStates || [];
811
+ const currentState = stateData?.currentState || '';
812
+ let completed = 0;
813
+
814
+ list.innerHTML = STEPS.map((step, i) => {
815
+ let status = 'pending';
816
+ let statusText = 'Chờ';
817
+ let time = '';
818
+
819
+ if (completedSteps.includes(step.id)) {
820
+ status = 'complete';
821
+ statusText = 'Xong';
822
+ completed++;
823
+ const meta = stateData?.steps?.[step.id];
824
+ if (meta?.completedAt) {
825
+ time = new Date(meta.completedAt).toLocaleTimeString('vi-VN');
826
+ }
827
+ } else if (step.id === currentState) {
828
+ status = 'running';
829
+ statusText = 'Đang chạy';
830
+ }
831
+
832
+ return `
833
+ <div class="step-item">
834
+ <div class="step-num ${status}">${status === 'complete' ? '✔' : (i + 1)}</div>
835
+ <span style="font-size:16px;">${step.icon}</span>
836
+ <span class="step-name">${step.name}</span>
837
+ <span class="step-status ${status}">${statusText}</span>
838
+ <span class="step-time">${time}</span>
839
+ </div>`;
840
+ }).join('');
841
+
842
+ // Update progress
843
+ const pct = Math.round((completed / STEPS.length) * 100);
844
+ document.getElementById('progressBar').style.width = pct + '%';
845
+ document.getElementById('progressText').textContent = pct + '%';
846
+ document.getElementById('statCompleted').innerHTML = `${completed}<span>/${STEPS.length}</span>`;
847
+ }
848
+
849
+ // ─── Dashboard Data ─────────────────────────
850
+ const startTime = Date.now();
851
+ let wsConnection = null;
852
+ let pollFallbackId = null;
853
+ let connectionMode = 'connecting'; // 'live' | 'polling' | 'connecting'
854
+
855
+ function updateState(stateData) {
856
+ renderSteps(stateData);
857
+
858
+ const cs = stateData.completedStates || [];
859
+ const hasAuth = cs.includes('WAIT_GEMINI_AUTH');
860
+ const hasSmoke9r = cs.includes('SMOKE_TEST_9ROUTER');
861
+ const hasSmokeOc = cs.includes('SMOKE_TEST_OPENCLAW');
862
+ const has9r = cs.includes('INSTALL_9ROUTER');
863
+ const hasOc = cs.includes('INSTALL_OPENCLAW');
864
+
865
+ document.getElementById('card9router').textContent = has9r ? 'Active' : 'Ready';
866
+ document.getElementById('cardOpenClaw').textContent = hasOc ? 'Active' : 'Ready';
867
+ document.getElementById('cardAuth').textContent = hasAuth ? 'OK' : 'Pending';
868
+ document.getElementById('cardSmoke').textContent = (hasSmoke9r && hasSmokeOc) ? 'Pass' : 'Pending';
869
+
870
+ // Uptime
871
+ const uptimeSec = Math.floor((Date.now() - startTime) / 1000);
872
+ const mins = Math.floor(uptimeSec / 60);
873
+ const secs = uptimeSec % 60;
874
+ document.getElementById('statUptime').textContent = mins > 0 ? `${mins}m${secs}s` : `${secs}s`;
875
+
876
+ // System badge
877
+ const badge = document.getElementById('systemBadge');
878
+ if (stateData.currentState === 'DONE') {
879
+ badge.className = 'nav-badge badge-ok';
880
+ badge.innerHTML = '<span class="badge-dot ok"></span> Hoàn thành';
881
+ } else if (stateData.currentState && stateData.currentState.startsWith('FAILED')) {
882
+ badge.className = 'nav-badge badge-fail';
883
+ badge.innerHTML = '<span class="badge-dot fail"></span> Lỗi';
884
+ }
885
+ }
886
+
887
+ function updateConnectionBadge() {
888
+ const btn = document.querySelector('.btn-outline');
889
+ if (!btn) return;
890
+ if (connectionMode === 'live') {
891
+ btn.innerHTML = '<span style="color:#10b981;">●</span> Live';
892
+ btn.style.borderColor = 'rgba(16,185,129,0.3)';
893
+ } else if (connectionMode === 'polling') {
894
+ btn.innerHTML = '⟳ Polling';
895
+ btn.style.borderColor = '';
896
+ } else {
897
+ btn.innerHTML = '… Connecting';
898
+ btn.style.borderColor = '';
899
+ }
900
+ }
901
+
902
+ // ─── WebSocket Connection ─────────────────────
903
+ function connectWebSocket() {
904
+ try {
905
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
906
+ wsConnection = new WebSocket(`${protocol}//${location.host}/ws`);
907
+
908
+ wsConnection.onopen = () => {
909
+ connectionMode = 'live';
910
+ updateConnectionBadge();
911
+ // Stop polling fallback
912
+ if (pollFallbackId) { clearInterval(pollFallbackId); pollFallbackId = null; }
913
+ };
914
+
915
+ wsConnection.onmessage = (event) => {
916
+ try {
917
+ const msg = JSON.parse(event.data);
918
+ if (msg.type === 'state' && msg.data) {
919
+ updateState(msg.data);
920
+ }
921
+ } catch { /* ignore parse errors */ }
922
+ };
923
+
924
+ wsConnection.onclose = () => {
925
+ connectionMode = 'polling';
926
+ updateConnectionBadge();
927
+ startPollingFallback();
928
+ // Auto-reconnect after 3s
929
+ setTimeout(connectWebSocket, 3000);
930
+ };
931
+
932
+ wsConnection.onerror = () => {
933
+ connectionMode = 'polling';
934
+ updateConnectionBadge();
935
+ };
936
+
937
+ // Send ping every 25s to keep alive
938
+ setInterval(() => {
939
+ if (wsConnection && wsConnection.readyState === WebSocket.OPEN) {
940
+ wsConnection.send('ping');
941
+ }
942
+ }, 25000);
943
+
944
+ } catch {
945
+ connectionMode = 'polling';
946
+ updateConnectionBadge();
947
+ startPollingFallback();
948
+ }
949
+ }
950
+
951
+ // ─── HTTP Polling Fallback ────────────────────
952
+ function startPollingFallback() {
953
+ if (pollFallbackId) return;
954
+ pollFallbackId = setInterval(async () => {
955
+ try {
956
+ const res = await fetch('/api/state');
957
+ if (res.ok) updateState(await res.json());
958
+ } catch { /* ignore */ }
959
+ }, 5000);
960
+ }
961
+
962
+ async function refreshDashboard() {
963
+ document.getElementById('refreshBar').style.display = 'block';
964
+ try {
965
+ const res = await fetch('/api/state');
966
+ if (res.ok) updateState(await res.json());
967
+ } catch {
968
+ updateState({ currentState: 'IDLE', completedStates: [], stepMeta: {} });
969
+ }
970
+ setTimeout(() => {
971
+ document.getElementById('refreshBar').style.display = 'none';
972
+ }, 800);
973
+ }
974
+
975
+ function runCmd(cmd) {
976
+ alert(`Chạy lệnh trong terminal:\n\noneclick-openclaw ${cmd}`);
977
+ }
978
+
979
+ // ─── Init ─────────────────────────────────────
980
+ refreshDashboard();
981
+ connectWebSocket();
982
+ // Update uptime every second
983
+ setInterval(() => {
984
+ const uptimeSec = Math.floor((Date.now() - startTime) / 1000);
985
+ const mins = Math.floor(uptimeSec / 60);
986
+ const secs = uptimeSec % 60;
987
+ document.getElementById('statUptime').textContent = mins > 0 ? `${mins}m${secs}s` : `${secs}s`;
988
+ }, 1000);
989
+ </script>
990
+ </body>
991
+ </html>