@aaronbassett/midnight-local-devnet 0.2.1 → 0.3.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 (88) hide show
  1. package/README.md +16 -26
  2. package/dist/cli/commands/dashboard.js +53 -14
  3. package/dist/cli/commands/dashboard.js.map +1 -1
  4. package/dist/cli/dashboard/html.d.ts +3 -0
  5. package/dist/cli/dashboard/html.js +1095 -0
  6. package/dist/cli/dashboard/html.js.map +1 -0
  7. package/dist/cli/dashboard/server.d.ts +17 -0
  8. package/dist/cli/dashboard/server.js +133 -0
  9. package/dist/cli/dashboard/server.js.map +1 -0
  10. package/dist/cli/dashboard/state-collector.d.ts +67 -0
  11. package/dist/cli/dashboard/state-collector.js +121 -0
  12. package/dist/cli/dashboard/state-collector.js.map +1 -0
  13. package/package.json +4 -5
  14. package/dist/cli/dashboard/app.d.ts +0 -9
  15. package/dist/cli/dashboard/app.js +0 -106
  16. package/dist/cli/dashboard/app.js.map +0 -1
  17. package/dist/cli/dashboard/components/gauge.d.ts +0 -9
  18. package/dist/cli/dashboard/components/gauge.js +0 -10
  19. package/dist/cli/dashboard/components/gauge.js.map +0 -1
  20. package/dist/cli/dashboard/components/panel-box.d.ts +0 -10
  21. package/dist/cli/dashboard/components/panel-box.js +0 -6
  22. package/dist/cli/dashboard/components/panel-box.js.map +0 -1
  23. package/dist/cli/dashboard/components/sparkline.d.ts +0 -9
  24. package/dist/cli/dashboard/components/sparkline.js +0 -25
  25. package/dist/cli/dashboard/components/sparkline.js.map +0 -1
  26. package/dist/cli/dashboard/components/status-badge.d.ts +0 -6
  27. package/dist/cli/dashboard/components/status-badge.js +0 -24
  28. package/dist/cli/dashboard/components/status-badge.js.map +0 -1
  29. package/dist/cli/dashboard/hooks/use-breakpoint.d.ts +0 -7
  30. package/dist/cli/dashboard/hooks/use-breakpoint.js +0 -15
  31. package/dist/cli/dashboard/hooks/use-breakpoint.js.map +0 -1
  32. package/dist/cli/dashboard/hooks/use-health.d.ts +0 -9
  33. package/dist/cli/dashboard/hooks/use-health.js +0 -35
  34. package/dist/cli/dashboard/hooks/use-health.js.map +0 -1
  35. package/dist/cli/dashboard/hooks/use-indexer-info.d.ts +0 -5
  36. package/dist/cli/dashboard/hooks/use-indexer-info.js +0 -19
  37. package/dist/cli/dashboard/hooks/use-indexer-info.js.map +0 -1
  38. package/dist/cli/dashboard/hooks/use-logs.d.ts +0 -19
  39. package/dist/cli/dashboard/hooks/use-logs.js +0 -55
  40. package/dist/cli/dashboard/hooks/use-logs.js.map +0 -1
  41. package/dist/cli/dashboard/hooks/use-node-info.d.ts +0 -21
  42. package/dist/cli/dashboard/hooks/use-node-info.js +0 -49
  43. package/dist/cli/dashboard/hooks/use-node-info.js.map +0 -1
  44. package/dist/cli/dashboard/hooks/use-polling.d.ts +0 -7
  45. package/dist/cli/dashboard/hooks/use-polling.js +0 -35
  46. package/dist/cli/dashboard/hooks/use-polling.js.map +0 -1
  47. package/dist/cli/dashboard/hooks/use-proof-server.d.ts +0 -7
  48. package/dist/cli/dashboard/hooks/use-proof-server.js +0 -14
  49. package/dist/cli/dashboard/hooks/use-proof-server.js.map +0 -1
  50. package/dist/cli/dashboard/hooks/use-services.d.ts +0 -3
  51. package/dist/cli/dashboard/hooks/use-services.js +0 -6
  52. package/dist/cli/dashboard/hooks/use-services.js.map +0 -1
  53. package/dist/cli/dashboard/hooks/use-terminal-size.d.ts +0 -5
  54. package/dist/cli/dashboard/hooks/use-terminal-size.js +0 -22
  55. package/dist/cli/dashboard/hooks/use-terminal-size.js.map +0 -1
  56. package/dist/cli/dashboard/hooks/use-wallet-state.d.ts +0 -10
  57. package/dist/cli/dashboard/hooks/use-wallet-state.js +0 -63
  58. package/dist/cli/dashboard/hooks/use-wallet-state.js.map +0 -1
  59. package/dist/cli/dashboard/layouts/large.d.ts +0 -7
  60. package/dist/cli/dashboard/layouts/large.js +0 -13
  61. package/dist/cli/dashboard/layouts/large.js.map +0 -1
  62. package/dist/cli/dashboard/layouts/medium.d.ts +0 -7
  63. package/dist/cli/dashboard/layouts/medium.js +0 -11
  64. package/dist/cli/dashboard/layouts/medium.js.map +0 -1
  65. package/dist/cli/dashboard/layouts/small.d.ts +0 -7
  66. package/dist/cli/dashboard/layouts/small.js +0 -11
  67. package/dist/cli/dashboard/layouts/small.js.map +0 -1
  68. package/dist/cli/dashboard/panels/indexer-panel.d.ts +0 -11
  69. package/dist/cli/dashboard/panels/indexer-panel.js +0 -17
  70. package/dist/cli/dashboard/panels/indexer-panel.js.map +0 -1
  71. package/dist/cli/dashboard/panels/log-panel.d.ts +0 -13
  72. package/dist/cli/dashboard/panels/log-panel.js +0 -27
  73. package/dist/cli/dashboard/panels/log-panel.js.map +0 -1
  74. package/dist/cli/dashboard/panels/node-panel.d.ts +0 -10
  75. package/dist/cli/dashboard/panels/node-panel.js +0 -17
  76. package/dist/cli/dashboard/panels/node-panel.js.map +0 -1
  77. package/dist/cli/dashboard/panels/proof-panel.d.ts +0 -10
  78. package/dist/cli/dashboard/panels/proof-panel.js +0 -20
  79. package/dist/cli/dashboard/panels/proof-panel.js.map +0 -1
  80. package/dist/cli/dashboard/panels/response-graph.d.ts +0 -10
  81. package/dist/cli/dashboard/panels/response-graph.js +0 -12
  82. package/dist/cli/dashboard/panels/response-graph.js.map +0 -1
  83. package/dist/cli/dashboard/panels/wallet-panel.d.ts +0 -9
  84. package/dist/cli/dashboard/panels/wallet-panel.js +0 -24
  85. package/dist/cli/dashboard/panels/wallet-panel.js.map +0 -1
  86. package/dist/cli/dashboard/types.d.ts +0 -39
  87. package/dist/cli/dashboard/types.js +0 -2
  88. package/dist/cli/dashboard/types.js.map +0 -1
@@ -0,0 +1,1095 @@
1
+ /**
2
+ * Generates a complete single-page HTML dashboard for the Midnight local devnet.
3
+ * Uses Preact + HTM via CDN import maps. All CSS and JS are inline.
4
+ */
5
+ function safeJsonInScript(value) {
6
+ return JSON.stringify(value)
7
+ .replace(/</g, '\\u003c')
8
+ .replace(/>/g, '\\u003e');
9
+ }
10
+ export function generateDashboardHtml({ wsUrl }) {
11
+ return `<!DOCTYPE html>
12
+ <html lang="en">
13
+ <head>
14
+ <meta charset="utf-8" />
15
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
16
+ <title>Midnight Devnet Dashboard</title>
17
+
18
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
19
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
20
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
21
+
22
+ <script type="importmap">
23
+ {
24
+ "imports": {
25
+ "preact": "https://esm.sh/preact@10.25.4",
26
+ "preact/hooks": "https://esm.sh/preact@10.25.4/hooks",
27
+ "htm/preact": "https://esm.sh/htm@3.1.1/preact?external=preact"
28
+ }
29
+ }
30
+ </script>
31
+
32
+ <style>
33
+ :root {
34
+ --mn-void: #09090f;
35
+ --mn-surface: #0f0f1e;
36
+ --mn-surface-alt: #14142b;
37
+ --mn-border: #1c1c3a;
38
+ --mn-border-bright: #2a2a50;
39
+ --mn-accent: #3b3bff;
40
+ --mn-accent-hover: #5252ff;
41
+ --mn-accent-muted: #2a2aaa;
42
+ --mn-accent-glow: rgba(59, 59, 255, 0.25);
43
+ --mn-text: #f0f0ff;
44
+ --mn-text-secondary: #8080aa;
45
+ --mn-text-muted: #505070;
46
+ --mn-success: #22c55e;
47
+ --mn-warning: #eab308;
48
+ --mn-error: #ef4444;
49
+ --mn-gradient-hero: linear-gradient(135deg, #1a1a4a 0%, #09090f 50%, #0f1a2f 100%);
50
+ --mn-gradient-accent: linear-gradient(135deg, #3b3bff 0%, #6b3bff 100%);
51
+ --mn-gradient-card: linear-gradient(180deg, rgba(59, 59, 255, 0.08) 0%, transparent 60%);
52
+ }
53
+
54
+ *, *::before, *::after {
55
+ margin: 0;
56
+ padding: 0;
57
+ box-sizing: border-box;
58
+ }
59
+
60
+ body {
61
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
62
+ background-color: var(--mn-void);
63
+ background-image: radial-gradient(circle, rgba(59, 59, 255, 0.02) 1px, transparent 1px);
64
+ background-size: 24px 24px;
65
+ color: var(--mn-text);
66
+ min-height: 100vh;
67
+ line-height: 1.5;
68
+ -webkit-font-smoothing: antialiased;
69
+ }
70
+
71
+ .mono {
72
+ font-family: 'JetBrains Mono', monospace;
73
+ }
74
+
75
+ #app {
76
+ max-width: 1280px;
77
+ margin: 0 auto;
78
+ padding: 24px;
79
+ }
80
+
81
+ /* --- Animations --- */
82
+ @keyframes fadeInUp {
83
+ from {
84
+ opacity: 0;
85
+ transform: translateY(12px);
86
+ }
87
+ to {
88
+ opacity: 1;
89
+ transform: translateY(0);
90
+ }
91
+ }
92
+
93
+ @keyframes slideInRight {
94
+ from {
95
+ opacity: 0;
96
+ transform: translateX(100%);
97
+ }
98
+ to {
99
+ opacity: 1;
100
+ transform: translateX(0);
101
+ }
102
+ }
103
+
104
+ @keyframes fadeOut {
105
+ from { opacity: 1; }
106
+ to { opacity: 0; }
107
+ }
108
+
109
+ @keyframes pulse {
110
+ 0%, 100% { opacity: 1; }
111
+ 50% { opacity: 0.5; }
112
+ }
113
+
114
+ .fade-in {
115
+ animation: fadeInUp 0.35s ease-out both;
116
+ }
117
+
118
+ /* --- Header --- */
119
+ .header {
120
+ display: flex;
121
+ align-items: center;
122
+ justify-content: space-between;
123
+ margin-bottom: 28px;
124
+ padding: 20px 24px;
125
+ background: var(--mn-surface);
126
+ border: 1px solid var(--mn-border);
127
+ border-radius: 12px;
128
+ position: relative;
129
+ overflow: hidden;
130
+ }
131
+
132
+ .header::before {
133
+ content: '';
134
+ position: absolute;
135
+ top: -60%;
136
+ left: -10%;
137
+ width: 300px;
138
+ height: 300px;
139
+ background: radial-gradient(circle, rgba(59, 59, 255, 0.12) 0%, transparent 70%);
140
+ pointer-events: none;
141
+ }
142
+
143
+ .header-left {
144
+ display: flex;
145
+ align-items: center;
146
+ gap: 16px;
147
+ position: relative;
148
+ z-index: 1;
149
+ }
150
+
151
+ .logo {
152
+ font-size: 22px;
153
+ font-weight: 700;
154
+ letter-spacing: -0.02em;
155
+ color: var(--mn-text);
156
+ }
157
+
158
+ .logo-icon {
159
+ color: var(--mn-accent);
160
+ margin-right: 4px;
161
+ }
162
+
163
+ .status-badge {
164
+ display: inline-flex;
165
+ align-items: center;
166
+ gap: 8px;
167
+ padding: 6px 14px;
168
+ border-radius: 20px;
169
+ font-size: 13px;
170
+ font-weight: 500;
171
+ background: var(--mn-surface-alt);
172
+ border: 1px solid var(--mn-border);
173
+ }
174
+
175
+ .status-dot {
176
+ width: 8px;
177
+ height: 8px;
178
+ border-radius: 50%;
179
+ flex-shrink: 0;
180
+ }
181
+
182
+ .status-dot.running { background: var(--mn-success); box-shadow: 0 0 6px var(--mn-success); }
183
+ .status-dot.stopped { background: var(--mn-error); }
184
+ .status-dot.starting, .status-dot.stopping { background: var(--mn-warning); animation: pulse 1.5s ease-in-out infinite; }
185
+ .status-dot.unknown { background: var(--mn-text-muted); }
186
+
187
+ .header-actions {
188
+ display: flex;
189
+ gap: 8px;
190
+ position: relative;
191
+ z-index: 1;
192
+ }
193
+
194
+ .btn {
195
+ display: inline-flex;
196
+ align-items: center;
197
+ gap: 6px;
198
+ padding: 8px 16px;
199
+ border-radius: 8px;
200
+ border: 1px solid var(--mn-border);
201
+ background: var(--mn-surface-alt);
202
+ color: var(--mn-text);
203
+ font-family: 'Inter', sans-serif;
204
+ font-size: 13px;
205
+ font-weight: 500;
206
+ cursor: pointer;
207
+ transition: all 0.15s ease-out;
208
+ }
209
+
210
+ .btn:hover {
211
+ border-color: var(--mn-border-bright);
212
+ background: var(--mn-border);
213
+ }
214
+
215
+ .btn-primary {
216
+ background: var(--mn-accent);
217
+ border-color: var(--mn-accent);
218
+ color: var(--mn-text);
219
+ }
220
+
221
+ .btn-primary:hover {
222
+ background: var(--mn-accent-hover);
223
+ border-color: var(--mn-accent-hover);
224
+ }
225
+
226
+ .btn-danger {
227
+ border-color: rgba(239, 68, 68, 0.3);
228
+ color: var(--mn-error);
229
+ }
230
+
231
+ .btn-danger:hover {
232
+ background: rgba(239, 68, 68, 0.1);
233
+ border-color: var(--mn-error);
234
+ }
235
+
236
+ /* --- Cards Grid --- */
237
+ .cards-grid {
238
+ display: grid;
239
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
240
+ gap: 16px;
241
+ margin-bottom: 16px;
242
+ }
243
+
244
+ .card {
245
+ background: var(--mn-surface);
246
+ border: 1px solid var(--mn-border);
247
+ border-radius: 12px;
248
+ padding: 20px;
249
+ transition: border-color 0.2s ease-out, box-shadow 0.2s ease-out;
250
+ position: relative;
251
+ }
252
+
253
+ .card:hover {
254
+ border-color: var(--mn-border-bright);
255
+ box-shadow: 0 4px 24px var(--mn-accent-glow);
256
+ }
257
+
258
+ .card:hover::after {
259
+ content: '';
260
+ position: absolute;
261
+ inset: 0;
262
+ border-radius: 12px;
263
+ background: var(--mn-gradient-card);
264
+ pointer-events: none;
265
+ }
266
+
267
+ .card-header {
268
+ display: flex;
269
+ align-items: center;
270
+ justify-content: space-between;
271
+ margin-bottom: 16px;
272
+ }
273
+
274
+ .card-title {
275
+ display: flex;
276
+ align-items: center;
277
+ gap: 10px;
278
+ font-size: 14px;
279
+ font-weight: 600;
280
+ color: var(--mn-text-secondary);
281
+ text-transform: uppercase;
282
+ letter-spacing: 0.05em;
283
+ }
284
+
285
+ .card-health-dot {
286
+ width: 8px;
287
+ height: 8px;
288
+ border-radius: 50%;
289
+ }
290
+
291
+ .card-health-dot.healthy { background: var(--mn-success); box-shadow: 0 0 6px var(--mn-success); }
292
+ .card-health-dot.unhealthy { background: var(--mn-error); box-shadow: 0 0 6px var(--mn-error); }
293
+
294
+ .stat-value {
295
+ font-family: 'JetBrains Mono', monospace;
296
+ font-size: 28px;
297
+ font-weight: 600;
298
+ color: var(--mn-text);
299
+ line-height: 1.2;
300
+ }
301
+
302
+ .stat-label {
303
+ font-size: 12px;
304
+ color: var(--mn-text-muted);
305
+ margin-top: 2px;
306
+ }
307
+
308
+ .stat-row {
309
+ display: flex;
310
+ align-items: baseline;
311
+ justify-content: space-between;
312
+ padding: 8px 0;
313
+ }
314
+
315
+ .stat-row:not(:last-child) {
316
+ border-bottom: 1px solid var(--mn-border);
317
+ }
318
+
319
+ .stat-row-label {
320
+ font-size: 13px;
321
+ color: var(--mn-text-secondary);
322
+ }
323
+
324
+ .stat-row-value {
325
+ font-family: 'JetBrains Mono', monospace;
326
+ font-size: 14px;
327
+ color: var(--mn-text);
328
+ }
329
+
330
+ /* --- Gauge --- */
331
+ .gauge-container {
332
+ margin-top: 12px;
333
+ }
334
+
335
+ .gauge-bar {
336
+ width: 100%;
337
+ height: 6px;
338
+ background: var(--mn-surface-alt);
339
+ border-radius: 3px;
340
+ overflow: hidden;
341
+ }
342
+
343
+ .gauge-fill {
344
+ height: 100%;
345
+ border-radius: 3px;
346
+ transition: width 0.3s ease-out, background 0.3s ease-out;
347
+ }
348
+
349
+ .gauge-label {
350
+ display: flex;
351
+ justify-content: space-between;
352
+ margin-top: 6px;
353
+ font-size: 11px;
354
+ color: var(--mn-text-muted);
355
+ }
356
+
357
+ /* --- Wallet Card (full-width) --- */
358
+ .full-width {
359
+ grid-column: 1 / -1;
360
+ }
361
+
362
+ .wallet-grid {
363
+ display: grid;
364
+ grid-template-columns: 1fr 1fr 1fr;
365
+ gap: 20px;
366
+ }
367
+
368
+ .wallet-address {
369
+ grid-column: 1 / -1;
370
+ font-family: 'JetBrains Mono', monospace;
371
+ font-size: 13px;
372
+ color: var(--mn-text-secondary);
373
+ overflow: hidden;
374
+ text-overflow: ellipsis;
375
+ white-space: nowrap;
376
+ padding: 10px 14px;
377
+ background: var(--mn-surface-alt);
378
+ border-radius: 8px;
379
+ border: 1px solid var(--mn-border);
380
+ }
381
+
382
+ .balance-item {
383
+ text-align: center;
384
+ }
385
+
386
+ .balance-value {
387
+ font-family: 'JetBrains Mono', monospace;
388
+ font-size: 22px;
389
+ font-weight: 600;
390
+ color: var(--mn-text);
391
+ }
392
+
393
+ .balance-label {
394
+ font-size: 12px;
395
+ color: var(--mn-text-muted);
396
+ margin-top: 2px;
397
+ }
398
+
399
+ /* --- Response Chart --- */
400
+ .chart-section {
401
+ margin-bottom: 16px;
402
+ }
403
+
404
+ .sparkline-row {
405
+ display: flex;
406
+ align-items: center;
407
+ gap: 16px;
408
+ padding: 12px 0;
409
+ }
410
+
411
+ .sparkline-row:not(:last-child) {
412
+ border-bottom: 1px solid var(--mn-border);
413
+ }
414
+
415
+ .sparkline-label {
416
+ width: 100px;
417
+ font-size: 13px;
418
+ color: var(--mn-text-secondary);
419
+ flex-shrink: 0;
420
+ }
421
+
422
+ .sparkline-svg {
423
+ flex: 1;
424
+ height: 32px;
425
+ }
426
+
427
+ .sparkline-value {
428
+ width: 60px;
429
+ text-align: right;
430
+ font-family: 'JetBrains Mono', monospace;
431
+ font-size: 12px;
432
+ color: var(--mn-text-muted);
433
+ flex-shrink: 0;
434
+ }
435
+
436
+ /* --- Log Viewer --- */
437
+ .log-viewer {
438
+ background: var(--mn-surface);
439
+ border: 1px solid var(--mn-border);
440
+ border-radius: 12px;
441
+ overflow: hidden;
442
+ }
443
+
444
+ .log-toolbar {
445
+ display: flex;
446
+ align-items: center;
447
+ gap: 8px;
448
+ padding: 12px 16px;
449
+ border-bottom: 1px solid var(--mn-border);
450
+ background: var(--mn-surface-alt);
451
+ }
452
+
453
+ .log-select {
454
+ padding: 6px 10px;
455
+ border-radius: 6px;
456
+ border: 1px solid var(--mn-border);
457
+ background: var(--mn-surface);
458
+ color: var(--mn-text);
459
+ font-family: 'Inter', sans-serif;
460
+ font-size: 12px;
461
+ cursor: pointer;
462
+ outline: none;
463
+ }
464
+
465
+ .log-select:focus {
466
+ border-color: var(--mn-accent);
467
+ }
468
+
469
+ .log-search {
470
+ flex: 1;
471
+ padding: 6px 10px;
472
+ border-radius: 6px;
473
+ border: 1px solid var(--mn-border);
474
+ background: var(--mn-surface);
475
+ color: var(--mn-text);
476
+ font-family: 'Inter', sans-serif;
477
+ font-size: 12px;
478
+ outline: none;
479
+ }
480
+
481
+ .log-search::placeholder {
482
+ color: var(--mn-text-muted);
483
+ }
484
+
485
+ .log-search:focus {
486
+ border-color: var(--mn-accent);
487
+ }
488
+
489
+ .log-entries {
490
+ max-height: 320px;
491
+ overflow-y: auto;
492
+ padding: 8px 0;
493
+ scroll-behavior: smooth;
494
+ }
495
+
496
+ .log-entries::-webkit-scrollbar {
497
+ width: 6px;
498
+ }
499
+
500
+ .log-entries::-webkit-scrollbar-track {
501
+ background: transparent;
502
+ }
503
+
504
+ .log-entries::-webkit-scrollbar-thumb {
505
+ background: var(--mn-border-bright);
506
+ border-radius: 3px;
507
+ }
508
+
509
+ .log-line {
510
+ display: flex;
511
+ align-items: flex-start;
512
+ gap: 8px;
513
+ padding: 3px 16px;
514
+ font-family: 'JetBrains Mono', monospace;
515
+ font-size: 12px;
516
+ line-height: 1.6;
517
+ }
518
+
519
+ .log-line:hover {
520
+ background: var(--mn-surface-alt);
521
+ }
522
+
523
+ .log-tag {
524
+ display: inline-block;
525
+ padding: 1px 6px;
526
+ border-radius: 4px;
527
+ font-size: 10px;
528
+ font-weight: 600;
529
+ text-transform: uppercase;
530
+ letter-spacing: 0.04em;
531
+ flex-shrink: 0;
532
+ min-width: 52px;
533
+ text-align: center;
534
+ }
535
+
536
+ .log-tag.node { background: rgba(59, 59, 255, 0.15); color: #7070ff; }
537
+ .log-tag.indexer { background: rgba(99, 59, 255, 0.15); color: #9070ff; }
538
+ .log-tag.proof { background: rgba(139, 59, 255, 0.15); color: #b070ff; }
539
+
540
+ .log-level {
541
+ font-size: 10px;
542
+ font-weight: 500;
543
+ min-width: 40px;
544
+ flex-shrink: 0;
545
+ }
546
+
547
+ .log-level.info { color: var(--mn-text-muted); }
548
+ .log-level.warn { color: var(--mn-warning); }
549
+ .log-level.error { color: var(--mn-error); }
550
+
551
+ .log-message {
552
+ color: var(--mn-text-secondary);
553
+ word-break: break-all;
554
+ }
555
+
556
+ /* --- Connection Overlay --- */
557
+ .connection-overlay {
558
+ position: fixed;
559
+ inset: 0;
560
+ background: rgba(9, 9, 15, 0.85);
561
+ display: flex;
562
+ align-items: center;
563
+ justify-content: center;
564
+ z-index: 100;
565
+ backdrop-filter: blur(4px);
566
+ }
567
+
568
+ .connection-message {
569
+ text-align: center;
570
+ color: var(--mn-text-secondary);
571
+ }
572
+
573
+ .connection-message h2 {
574
+ font-size: 18px;
575
+ margin-bottom: 8px;
576
+ color: var(--mn-text);
577
+ }
578
+
579
+ .connection-message p {
580
+ font-size: 14px;
581
+ }
582
+
583
+ .connection-spinner {
584
+ width: 24px;
585
+ height: 24px;
586
+ border: 2px solid var(--mn-border);
587
+ border-top-color: var(--mn-accent);
588
+ border-radius: 50%;
589
+ animation: spin 0.8s linear infinite;
590
+ margin: 0 auto 16px;
591
+ }
592
+
593
+ @keyframes spin {
594
+ to { transform: rotate(360deg); }
595
+ }
596
+
597
+ /* --- Toast --- */
598
+ .toast-container {
599
+ position: fixed;
600
+ top: 16px;
601
+ right: 16px;
602
+ z-index: 200;
603
+ display: flex;
604
+ flex-direction: column;
605
+ gap: 8px;
606
+ }
607
+
608
+ .toast {
609
+ padding: 12px 20px;
610
+ border-radius: 8px;
611
+ font-size: 13px;
612
+ font-weight: 500;
613
+ border: 1px solid var(--mn-border);
614
+ background: var(--mn-surface);
615
+ color: var(--mn-text);
616
+ animation: slideInRight 0.2s ease-out;
617
+ min-width: 240px;
618
+ box-shadow: 0 8px 32px rgba(9, 9, 15, 0.5);
619
+ }
620
+
621
+ .toast.success { border-left: 3px solid var(--mn-success); }
622
+ .toast.error { border-left: 3px solid var(--mn-error); }
623
+ .toast.fade-out { animation: fadeOut 0.3s ease-out forwards; }
624
+
625
+ /* --- Responsive --- */
626
+ @media (max-width: 768px) {
627
+ #app { padding: 12px; }
628
+ .header { flex-direction: column; gap: 12px; padding: 16px; }
629
+ .wallet-grid { grid-template-columns: 1fr; }
630
+ .sparkline-row { flex-direction: column; align-items: flex-start; gap: 4px; }
631
+ .sparkline-label { width: auto; }
632
+ .sparkline-value { width: auto; text-align: left; }
633
+ }
634
+ </style>
635
+ </head>
636
+ <body>
637
+ <div id="app"></div>
638
+
639
+ <script type="module">
640
+ import { h, render } from 'preact';
641
+ import { useState, useEffect, useRef, useCallback } from 'preact/hooks';
642
+ import { html } from 'htm/preact';
643
+
644
+ // --- Lucide icon SVGs (inline, stroke-based) ---
645
+ // Using simple inline SVG paths instead of full lucide library for performance
646
+ const icons = {
647
+ play: html\`<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="6 3 20 12 6 21 6 3"/></svg>\`,
648
+ square: html\`<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/></svg>\`,
649
+ box: html\`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/></svg>\`,
650
+ database: html\`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5V19A9 3 0 0 0 21 19V5"/><path d="M3 12A9 3 0 0 0 21 12"/></svg>\`,
651
+ shield: html\`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/></svg>\`,
652
+ wallet: html\`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1"/><path d="M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4"/></svg>\`,
653
+ activity: html\`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"/></svg>\`,
654
+ terminal: html\`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/></svg>\`,
655
+ search: html\`<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>\`,
656
+ };
657
+
658
+ // --- WebSocket URL (injected at generation time) ---
659
+ const WS_URL = ${safeJsonInScript(wsUrl)};
660
+
661
+ // --- Utility functions ---
662
+ function formatNumber(n) {
663
+ if (n == null) return '--';
664
+ return n.toLocaleString();
665
+ }
666
+
667
+ function formatBalance(val) {
668
+ if (val == null || val === '0') return '0';
669
+ const num = BigInt(val);
670
+ return num.toLocaleString();
671
+ }
672
+
673
+ function truncateAddress(addr) {
674
+ if (!addr) return '--';
675
+ if (addr.length <= 20) return addr;
676
+ return addr.slice(0, 10) + '...' + addr.slice(-8);
677
+ }
678
+
679
+ function formatMs(ms) {
680
+ if (ms == null) return '--';
681
+ return ms < 1000 ? ms.toFixed(0) + 'ms' : (ms / 1000).toFixed(2) + 's';
682
+ }
683
+
684
+ function gaugeColor(pct) {
685
+ if (pct >= 90) return 'var(--mn-error)';
686
+ if (pct >= 70) return 'var(--mn-warning)';
687
+ return 'var(--mn-success)';
688
+ }
689
+
690
+ // --- Default state ---
691
+ const defaultState = {
692
+ node: { chain: null, name: null, version: null, blockHeight: null, avgBlockTime: null, peers: null, syncing: null },
693
+ indexer: { ready: false, responseTime: null },
694
+ proofServer: { version: null, ready: false, jobsProcessing: null, jobsPending: null, jobCapacity: null, proofVersions: null },
695
+ wallet: { address: null, connected: false, unshielded: '0', shielded: '0', dust: '0' },
696
+ health: {
697
+ node: { status: 'unhealthy', history: [] },
698
+ indexer: { status: 'unhealthy', history: [] },
699
+ proofServer: { status: 'unhealthy', history: [] },
700
+ },
701
+ containers: [],
702
+ logs: [],
703
+ networkStatus: 'unknown',
704
+ };
705
+
706
+ // --- Toast component ---
707
+ function ToastContainer({ toasts, onRemove }) {
708
+ return html\`
709
+ <div class="toast-container">
710
+ \${toasts.map(t => html\`
711
+ <div key=\${t.id} class="toast \${t.type} \${t.fading ? 'fade-out' : ''}"
712
+ onAnimationEnd=\${() => t.fading && onRemove(t.id)}>
713
+ \${t.message}
714
+ </div>
715
+ \`)}
716
+ </div>
717
+ \`;
718
+ }
719
+
720
+ // --- ConnectionStatus ---
721
+ function ConnectionStatus({ connected }) {
722
+ if (connected) return null;
723
+ return html\`
724
+ <div class="connection-overlay">
725
+ <div class="connection-message">
726
+ <div class="connection-spinner"></div>
727
+ <h2>Reconnecting...</h2>
728
+ <p>Attempting to connect to dashboard server</p>
729
+ </div>
730
+ </div>
731
+ \`;
732
+ }
733
+
734
+ // --- Header ---
735
+ function Header({ networkStatus, onStart, onStop }) {
736
+ const statusLabel = networkStatus.charAt(0).toUpperCase() + networkStatus.slice(1);
737
+ return html\`
738
+ <div class="header fade-in">
739
+ <div class="header-left">
740
+ <div class="logo"><span class="logo-icon">◈</span> Midnight Devnet</div>
741
+ <div class="status-badge">
742
+ <span class="status-dot \${networkStatus}"></span>
743
+ \${statusLabel}
744
+ </div>
745
+ </div>
746
+ <div class="header-actions">
747
+ <button class="btn btn-primary" onClick=\${onStart}>\${icons.play} Start</button>
748
+ <button class="btn btn-danger" onClick=\${onStop}>\${icons.square} Stop</button>
749
+ </div>
750
+ </div>
751
+ \`;
752
+ }
753
+
754
+ // --- NodeCard ---
755
+ function NodeCard({ node, health }) {
756
+ const blockTimeStr = node.avgBlockTime != null ? (node.avgBlockTime / 1000).toFixed(1) + 's' : '--';
757
+ return html\`
758
+ <div class="card fade-in" style="animation-delay: 0ms">
759
+ <div class="card-header">
760
+ <div class="card-title">
761
+ \${icons.box}
762
+ Node
763
+ </div>
764
+ <span class="card-health-dot \${health.status}"></span>
765
+ </div>
766
+ <div class="stat-value mono">\${formatNumber(node.blockHeight)}</div>
767
+ <div class="stat-label">Block Height</div>
768
+ <div style="margin-top: 14px">
769
+ <div class="stat-row">
770
+ <span class="stat-row-label">Avg Block Time</span>
771
+ <span class="stat-row-value">\${blockTimeStr}</span>
772
+ </div>
773
+ <div class="stat-row">
774
+ <span class="stat-row-label">Peers</span>
775
+ <span class="stat-row-value">\${formatNumber(node.peers)}</span>
776
+ </div>
777
+ <div class="stat-row">
778
+ <span class="stat-row-label">Version</span>
779
+ <span class="stat-row-value">\${node.version || '--'}</span>
780
+ </div>
781
+ </div>
782
+ </div>
783
+ \`;
784
+ }
785
+
786
+ // --- IndexerCard ---
787
+ function IndexerCard({ indexer, health }) {
788
+ return html\`
789
+ <div class="card fade-in" style="animation-delay: 150ms">
790
+ <div class="card-header">
791
+ <div class="card-title">
792
+ \${icons.database}
793
+ Indexer
794
+ </div>
795
+ <span class="card-health-dot \${health.status}"></span>
796
+ </div>
797
+ <div class="stat-value mono" style="font-size: 22px; color: \${indexer.ready ? 'var(--mn-success)' : 'var(--mn-error)'}">\${indexer.ready ? 'Ready' : 'Not Ready'}</div>
798
+ <div class="stat-label">Service Status</div>
799
+ <div style="margin-top: 14px">
800
+ <div class="stat-row">
801
+ <span class="stat-row-label">Response Time</span>
802
+ <span class="stat-row-value">\${formatMs(indexer.responseTime)}</span>
803
+ </div>
804
+ </div>
805
+ </div>
806
+ \`;
807
+ }
808
+
809
+ // --- ProofServerCard ---
810
+ function ProofServerCard({ proofServer, health }) {
811
+ const capacity = proofServer.jobCapacity || 1;
812
+ const processing = proofServer.jobsProcessing || 0;
813
+ const pct = Math.min(100, (processing / capacity) * 100);
814
+ return html\`
815
+ <div class="card fade-in" style="animation-delay: 300ms">
816
+ <div class="card-header">
817
+ <div class="card-title">
818
+ \${icons.shield}
819
+ Proof Server
820
+ </div>
821
+ <span class="card-health-dot \${health.status}"></span>
822
+ </div>
823
+ <div class="stat-value mono" style="font-size: 22px; color: \${proofServer.ready ? 'var(--mn-success)' : 'var(--mn-error)'}">\${proofServer.ready ? 'Ready' : 'Not Ready'}</div>
824
+ <div class="stat-label">Service Status</div>
825
+ <div class="gauge-container">
826
+ <div class="gauge-bar">
827
+ <div class="gauge-fill" style="width: \${pct}%; background: \${gaugeColor(pct)}"></div>
828
+ </div>
829
+ <div class="gauge-label">
830
+ <span>Jobs: \${formatNumber(processing)} / \${formatNumber(capacity)}</span>
831
+ <span>Pending: \${formatNumber(proofServer.jobsPending)}</span>
832
+ </div>
833
+ </div>
834
+ <div style="margin-top: 10px">
835
+ <div class="stat-row">
836
+ <span class="stat-row-label">Version</span>
837
+ <span class="stat-row-value">\${proofServer.version || '--'}</span>
838
+ </div>
839
+ </div>
840
+ </div>
841
+ \`;
842
+ }
843
+
844
+ // --- WalletCard ---
845
+ function WalletCard({ wallet }) {
846
+ return html\`
847
+ <div class="card full-width fade-in" style="animation-delay: 450ms">
848
+ <div class="card-header">
849
+ <div class="card-title">
850
+ \${icons.wallet}
851
+ Wallet
852
+ </div>
853
+ <span class="card-health-dot \${wallet.connected ? 'healthy' : 'unhealthy'}"></span>
854
+ </div>
855
+ <div class="wallet-grid">
856
+ <div class="wallet-address" title=\${wallet.address || ''}>\${wallet.address ? truncateAddress(wallet.address) : 'No wallet connected'}</div>
857
+ <div class="balance-item">
858
+ <div class="balance-value">\${formatBalance(wallet.unshielded)}</div>
859
+ <div class="balance-label">NIGHT (Unshielded)</div>
860
+ </div>
861
+ <div class="balance-item">
862
+ <div class="balance-value">\${formatBalance(wallet.shielded)}</div>
863
+ <div class="balance-label">NIGHT (Shielded)</div>
864
+ </div>
865
+ <div class="balance-item">
866
+ <div class="balance-value">\${formatBalance(wallet.dust)}</div>
867
+ <div class="balance-label">DUST</div>
868
+ </div>
869
+ </div>
870
+ </div>
871
+ \`;
872
+ }
873
+
874
+ // --- Sparkline ---
875
+ function Sparkline({ data, color, width, height }) {
876
+ if (!data || data.length < 2) {
877
+ return html\`<svg class="sparkline-svg" viewBox="0 0 \${width} \${height}" preserveAspectRatio="none">
878
+ <line x1="0" y1=\${height / 2} x2=\${width} y2=\${height / 2} stroke="var(--mn-border)" stroke-width="1" />
879
+ </svg>\`;
880
+ }
881
+ const max = Math.max(...data, 1);
882
+ const min = Math.min(...data, 0);
883
+ const range = max - min || 1;
884
+ const step = width / (data.length - 1);
885
+ const points = data.map((v, i) => {
886
+ const x = i * step;
887
+ const y = height - ((v - min) / range) * (height - 4) - 2;
888
+ return x.toFixed(1) + ',' + y.toFixed(1);
889
+ }).join(' ');
890
+
891
+ return html\`
892
+ <svg class="sparkline-svg" viewBox="0 0 \${width} \${height}" preserveAspectRatio="none">
893
+ <polyline points=\${points} fill="none" stroke=\${color} stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
894
+ </svg>
895
+ \`;
896
+ }
897
+
898
+ // --- ResponseChart ---
899
+ function ResponseChart({ health }) {
900
+ const lastVal = (arr) => arr.length > 0 ? formatMs(arr[arr.length - 1]) : '--';
901
+ return html\`
902
+ <div class="card full-width chart-section fade-in" style="animation-delay: 600ms">
903
+ <div class="card-header">
904
+ <div class="card-title">
905
+ \${icons.activity}
906
+ Response Times
907
+ </div>
908
+ </div>
909
+ <div class="sparkline-row">
910
+ <span class="sparkline-label">Node</span>
911
+ <\${Sparkline} data=\${health.node.history} color="var(--mn-accent)" width=\${400} height=\${32} />
912
+ <span class="sparkline-value">\${lastVal(health.node.history)}</span>
913
+ </div>
914
+ <div class="sparkline-row">
915
+ <span class="sparkline-label">Indexer</span>
916
+ <\${Sparkline} data=\${health.indexer.history} color="#6b3bff" width=\${400} height=\${32} />
917
+ <span class="sparkline-value">\${lastVal(health.indexer.history)}</span>
918
+ </div>
919
+ <div class="sparkline-row">
920
+ <span class="sparkline-label">Proof Server</span>
921
+ <\${Sparkline} data=\${health.proofServer.history} color="#9b3bff" width=\${400} height=\${32} />
922
+ <span class="sparkline-value">\${lastVal(health.proofServer.history)}</span>
923
+ </div>
924
+ </div>
925
+ \`;
926
+ }
927
+
928
+ // --- LogViewer ---
929
+ function LogViewer({ logs }) {
930
+ const [serviceFilter, setServiceFilter] = useState('all');
931
+ const [levelFilter, setLevelFilter] = useState('all');
932
+ const [searchText, setSearchText] = useState('');
933
+ const [pinBottom, setPinBottom] = useState(true);
934
+ const entriesRef = useRef(null);
935
+
936
+ const filteredLogs = logs.filter(log => {
937
+ if (serviceFilter !== 'all' && log.service !== serviceFilter) return false;
938
+ if (levelFilter !== 'all' && log.level !== levelFilter) return false;
939
+ if (searchText && !log.message.toLowerCase().includes(searchText.toLowerCase()) && !(log.raw || '').toLowerCase().includes(searchText.toLowerCase())) return false;
940
+ return true;
941
+ });
942
+
943
+ useEffect(() => {
944
+ if (pinBottom && entriesRef.current) {
945
+ entriesRef.current.scrollTop = entriesRef.current.scrollHeight;
946
+ }
947
+ }, [filteredLogs.length, pinBottom]);
948
+
949
+ const handleScroll = useCallback((e) => {
950
+ const el = e.target;
951
+ const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 40;
952
+ setPinBottom(atBottom);
953
+ }, []);
954
+
955
+ return html\`
956
+ <div class="log-viewer fade-in" style="animation-delay: 750ms">
957
+ <div class="log-toolbar">
958
+ \${icons.terminal}
959
+ <select class="log-select" value=\${serviceFilter} onChange=\${e => setServiceFilter(e.target.value)}>
960
+ <option value="all">All Services</option>
961
+ <option value="node">Node</option>
962
+ <option value="indexer">Indexer</option>
963
+ <option value="proof-server">Proof Server</option>
964
+ </select>
965
+ <select class="log-select" value=\${levelFilter} onChange=\${e => setLevelFilter(e.target.value)}>
966
+ <option value="all">All Levels</option>
967
+ <option value="info">Info</option>
968
+ <option value="warn">Warning</option>
969
+ <option value="error">Error</option>
970
+ </select>
971
+ <input class="log-search" type="text" placeholder="Search logs..." value=\${searchText} onInput=\${e => setSearchText(e.target.value)} />
972
+ </div>
973
+ <div class="log-entries" ref=\${entriesRef} onScroll=\${handleScroll}>
974
+ \${filteredLogs.map((log, i) => html\`
975
+ <div class="log-line" key=\${log.service + '-' + i + '-' + (log.message || '').slice(0, 20)}>
976
+ <span class="log-tag \${(log.service || '').replace('-', '')}">\${log.service || 'sys'}</span>
977
+ <span class="log-level \${log.level}">\${(log.level || 'info').toUpperCase()}</span>
978
+ <span class="log-message">\${log.message || log.raw || ''}</span>
979
+ </div>
980
+ \`)}
981
+ \${filteredLogs.length === 0 && html\`
982
+ <div class="log-line"><span class="log-message" style="color: var(--mn-text-muted)">No log entries</span></div>
983
+ \`}
984
+ </div>
985
+ </div>
986
+ \`;
987
+ }
988
+
989
+ // --- App ---
990
+ function App() {
991
+ const [state, setState] = useState(defaultState);
992
+ const [connected, setConnected] = useState(false);
993
+ const [toasts, setToasts] = useState([]);
994
+ const wsRef = useRef(null);
995
+ const reconnectTimer = useRef(null);
996
+ const backoff = useRef(1000);
997
+ const toastId = useRef(0);
998
+
999
+ const addToast = useCallback((message, type) => {
1000
+ const id = ++toastId.current;
1001
+ setToasts(prev => [...prev, { id, message, type, fading: false }]);
1002
+ setTimeout(() => {
1003
+ setToasts(prev => prev.map(t => t.id === id ? { ...t, fading: true } : t));
1004
+ }, 2700);
1005
+ setTimeout(() => {
1006
+ setToasts(prev => prev.filter(t => t.id !== id));
1007
+ }, 3000);
1008
+ }, []);
1009
+
1010
+ const removeToast = useCallback((id) => {
1011
+ setToasts(prev => prev.filter(t => t.id !== id));
1012
+ }, []);
1013
+
1014
+ const sendCommand = useCallback((action) => {
1015
+ if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
1016
+ wsRef.current.send(JSON.stringify({ type: 'command', action }));
1017
+ }
1018
+ }, []);
1019
+
1020
+ useEffect(() => {
1021
+ function connect() {
1022
+ const ws = new WebSocket(WS_URL);
1023
+ wsRef.current = ws;
1024
+
1025
+ ws.addEventListener('open', () => {
1026
+ setConnected(true);
1027
+ backoff.current = 1000;
1028
+ });
1029
+
1030
+ ws.addEventListener('message', (event) => {
1031
+ try {
1032
+ const msg = JSON.parse(event.data);
1033
+ if (msg.type === 'state') {
1034
+ setState(msg.data);
1035
+ } else if (msg.type === 'result') {
1036
+ addToast(msg.message || (msg.success ? 'Command succeeded' : 'Command failed'), msg.success ? 'success' : 'error');
1037
+ }
1038
+ } catch (e) {
1039
+ // ignore malformed messages
1040
+ }
1041
+ });
1042
+
1043
+ ws.addEventListener('close', () => {
1044
+ setConnected(false);
1045
+ scheduleReconnect();
1046
+ });
1047
+
1048
+ ws.addEventListener('error', () => {
1049
+ ws.close();
1050
+ });
1051
+ }
1052
+
1053
+ function scheduleReconnect() {
1054
+ if (reconnectTimer.current) clearTimeout(reconnectTimer.current);
1055
+ reconnectTimer.current = setTimeout(() => {
1056
+ backoff.current = Math.min(backoff.current * 2, 30000);
1057
+ connect();
1058
+ }, backoff.current);
1059
+ }
1060
+
1061
+ connect();
1062
+
1063
+ return () => {
1064
+ if (wsRef.current) wsRef.current.close();
1065
+ if (reconnectTimer.current) clearTimeout(reconnectTimer.current);
1066
+ };
1067
+ }, [addToast]);
1068
+
1069
+ return html\`
1070
+ <\${ConnectionStatus} connected=\${connected} />
1071
+ <\${ToastContainer} toasts=\${toasts} onRemove=\${removeToast} />
1072
+ <\${Header} networkStatus=\${state.networkStatus} onStart=\${() => sendCommand('start')} onStop=\${() => sendCommand('stop')} />
1073
+ <div class="cards-grid">
1074
+ <\${NodeCard} node=\${state.node} health=\${state.health.node} />
1075
+ <\${IndexerCard} indexer=\${state.indexer} health=\${state.health.indexer} />
1076
+ <\${ProofServerCard} proofServer=\${state.proofServer} health=\${state.health.proofServer} />
1077
+ </div>
1078
+ <div class="cards-grid">
1079
+ <\${WalletCard} wallet=\${state.wallet} />
1080
+ </div>
1081
+ <div class="cards-grid">
1082
+ <\${ResponseChart} health=\${state.health} />
1083
+ </div>
1084
+ <\${LogViewer} logs=\${state.logs} />
1085
+ \`;
1086
+ }
1087
+
1088
+ render(html\`<\${App} />\`, document.getElementById('app'));
1089
+ </script>
1090
+
1091
+ <!-- lucide-style icons rendered as inline SVGs above -->
1092
+ </body>
1093
+ </html>`;
1094
+ }
1095
+ //# sourceMappingURL=html.js.map