agent-relay 1.0.8 → 1.0.9

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 (113) hide show
  1. package/README.md +158 -0
  2. package/dist/bridge/config.d.ts +41 -0
  3. package/dist/bridge/config.d.ts.map +1 -0
  4. package/dist/bridge/config.js +143 -0
  5. package/dist/bridge/config.js.map +1 -0
  6. package/dist/bridge/index.d.ts +10 -0
  7. package/dist/bridge/index.d.ts.map +1 -0
  8. package/dist/bridge/index.js +10 -0
  9. package/dist/bridge/index.js.map +1 -0
  10. package/dist/bridge/multi-project-client.d.ts +99 -0
  11. package/dist/bridge/multi-project-client.d.ts.map +1 -0
  12. package/dist/bridge/multi-project-client.js +386 -0
  13. package/dist/bridge/multi-project-client.js.map +1 -0
  14. package/dist/bridge/spawner.d.ts +46 -0
  15. package/dist/bridge/spawner.d.ts.map +1 -0
  16. package/dist/bridge/spawner.js +223 -0
  17. package/dist/bridge/spawner.js.map +1 -0
  18. package/dist/bridge/types.d.ts +55 -0
  19. package/dist/bridge/types.d.ts.map +1 -0
  20. package/dist/bridge/types.js +6 -0
  21. package/dist/bridge/types.js.map +1 -0
  22. package/dist/bridge/utils.d.ts +30 -0
  23. package/dist/bridge/utils.d.ts.map +1 -0
  24. package/dist/bridge/utils.js +54 -0
  25. package/dist/bridge/utils.js.map +1 -0
  26. package/dist/cli/index.js +564 -5
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/daemon/agent-registry.d.ts.map +1 -1
  29. package/dist/daemon/agent-registry.js +6 -1
  30. package/dist/daemon/agent-registry.js.map +1 -1
  31. package/dist/daemon/connection.d.ts +22 -0
  32. package/dist/daemon/connection.d.ts.map +1 -1
  33. package/dist/daemon/connection.js +59 -13
  34. package/dist/daemon/connection.js.map +1 -1
  35. package/dist/daemon/router.d.ts +27 -0
  36. package/dist/daemon/router.d.ts.map +1 -1
  37. package/dist/daemon/router.js +108 -3
  38. package/dist/daemon/router.js.map +1 -1
  39. package/dist/daemon/server.d.ts +8 -0
  40. package/dist/daemon/server.d.ts.map +1 -1
  41. package/dist/daemon/server.js +95 -23
  42. package/dist/daemon/server.js.map +1 -1
  43. package/dist/dashboard/metrics.d.ts +105 -0
  44. package/dist/dashboard/metrics.d.ts.map +1 -0
  45. package/dist/dashboard/metrics.js +192 -0
  46. package/dist/dashboard/metrics.js.map +1 -0
  47. package/dist/dashboard/needs-attention.d.ts +24 -0
  48. package/dist/dashboard/needs-attention.d.ts.map +1 -0
  49. package/dist/dashboard/needs-attention.js +78 -0
  50. package/dist/dashboard/needs-attention.js.map +1 -0
  51. package/dist/dashboard/public/bridge.html +1272 -0
  52. package/dist/dashboard/public/index.html +2017 -879
  53. package/dist/dashboard/public/js/app.js +184 -0
  54. package/dist/dashboard/public/js/app.js.map +7 -0
  55. package/dist/dashboard/public/metrics.html +999 -0
  56. package/dist/dashboard/server.d.ts +13 -0
  57. package/dist/dashboard/server.d.ts.map +1 -1
  58. package/dist/dashboard/server.js +568 -13
  59. package/dist/dashboard/server.js.map +1 -1
  60. package/dist/dashboard/start.js +1 -1
  61. package/dist/dashboard/start.js.map +1 -1
  62. package/dist/dashboard-v2/index.d.ts +10 -0
  63. package/dist/dashboard-v2/index.d.ts.map +1 -0
  64. package/dist/dashboard-v2/index.js +54 -0
  65. package/dist/dashboard-v2/index.js.map +1 -0
  66. package/dist/dashboard-v2/lib/api.d.ts +95 -0
  67. package/dist/dashboard-v2/lib/api.d.ts.map +1 -0
  68. package/dist/dashboard-v2/lib/api.js +270 -0
  69. package/dist/dashboard-v2/lib/api.js.map +1 -0
  70. package/dist/dashboard-v2/lib/colors.d.ts +61 -0
  71. package/dist/dashboard-v2/lib/colors.d.ts.map +1 -0
  72. package/dist/dashboard-v2/lib/colors.js +198 -0
  73. package/dist/dashboard-v2/lib/colors.js.map +1 -0
  74. package/dist/dashboard-v2/lib/hierarchy.d.ts +74 -0
  75. package/dist/dashboard-v2/lib/hierarchy.d.ts.map +1 -0
  76. package/dist/dashboard-v2/lib/hierarchy.js +196 -0
  77. package/dist/dashboard-v2/lib/hierarchy.js.map +1 -0
  78. package/dist/dashboard-v2/types/index.d.ts +154 -0
  79. package/dist/dashboard-v2/types/index.d.ts.map +1 -0
  80. package/dist/dashboard-v2/types/index.js +6 -0
  81. package/dist/dashboard-v2/types/index.js.map +1 -0
  82. package/dist/storage/adapter.d.ts +21 -1
  83. package/dist/storage/adapter.d.ts.map +1 -1
  84. package/dist/storage/adapter.js +36 -0
  85. package/dist/storage/adapter.js.map +1 -1
  86. package/dist/storage/sqlite-adapter.d.ts +34 -0
  87. package/dist/storage/sqlite-adapter.d.ts.map +1 -1
  88. package/dist/storage/sqlite-adapter.js +253 -12
  89. package/dist/storage/sqlite-adapter.js.map +1 -1
  90. package/dist/utils/agent-config.d.ts +45 -0
  91. package/dist/utils/agent-config.d.ts.map +1 -0
  92. package/dist/utils/agent-config.js +118 -0
  93. package/dist/utils/agent-config.js.map +1 -0
  94. package/dist/wrapper/client.d.ts +8 -0
  95. package/dist/wrapper/client.d.ts.map +1 -1
  96. package/dist/wrapper/client.js +26 -0
  97. package/dist/wrapper/client.js.map +1 -1
  98. package/dist/wrapper/parser.d.ts +17 -0
  99. package/dist/wrapper/parser.d.ts.map +1 -1
  100. package/dist/wrapper/parser.js +334 -10
  101. package/dist/wrapper/parser.js.map +1 -1
  102. package/dist/wrapper/tmux-wrapper.d.ts +37 -2
  103. package/dist/wrapper/tmux-wrapper.d.ts.map +1 -1
  104. package/dist/wrapper/tmux-wrapper.js +178 -18
  105. package/dist/wrapper/tmux-wrapper.js.map +1 -1
  106. package/docs/AGENTS.md +105 -0
  107. package/docs/ARCHITECTURE_DECISIONS.md +175 -0
  108. package/docs/COMPETITIVE_ANALYSIS.md +897 -0
  109. package/docs/DESIGN_BRIDGE_STAFFING.md +878 -0
  110. package/docs/MONETIZATION.md +1679 -0
  111. package/docs/agent-relay-snippet.md +61 -0
  112. package/docs/dashboard-v2-plan.md +179 -0
  113. package/package.json +5 -2
@@ -0,0 +1,999 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Metrics | Agent Relay</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
10
+ <style>
11
+ :root {
12
+ --bg-deep: #0a0c0f;
13
+ --bg-panel: #0f1318;
14
+ --bg-card: #141a21;
15
+ --bg-elevated: #1a2129;
16
+
17
+ --border-dim: rgba(255, 255, 255, 0.06);
18
+ --border-subtle: rgba(255, 255, 255, 0.1);
19
+ --border-glow: rgba(0, 255, 200, 0.3);
20
+
21
+ --text-primary: #e8eaed;
22
+ --text-secondary: #9aa0a6;
23
+ --text-muted: #5f6368;
24
+
25
+ --accent-cyan: #00ffc8;
26
+ --accent-blue: #4a9eff;
27
+ --accent-purple: #b388ff;
28
+ --accent-orange: #ff9e40;
29
+ --accent-red: #ff5c5c;
30
+ --accent-green: #00e676;
31
+
32
+ --glow-cyan: 0 0 20px rgba(0, 255, 200, 0.3);
33
+ --glow-green: 0 0 15px rgba(0, 230, 118, 0.4);
34
+ --glow-red: 0 0 15px rgba(255, 92, 92, 0.4);
35
+
36
+ --font-display: 'Space Grotesk', sans-serif;
37
+ --font-mono: 'IBM Plex Mono', monospace;
38
+ }
39
+
40
+ *, *::before, *::after {
41
+ box-sizing: border-box;
42
+ margin: 0;
43
+ padding: 0;
44
+ }
45
+
46
+ html, body {
47
+ height: 100%;
48
+ overflow-x: hidden;
49
+ }
50
+
51
+ body {
52
+ font-family: var(--font-display);
53
+ background: var(--bg-deep);
54
+ color: var(--text-primary);
55
+ line-height: 1.5;
56
+ -webkit-font-smoothing: antialiased;
57
+ }
58
+
59
+ /* Subtle grid background */
60
+ body::before {
61
+ content: '';
62
+ position: fixed;
63
+ inset: 0;
64
+ background-image:
65
+ linear-gradient(rgba(0, 255, 200, 0.02) 1px, transparent 1px),
66
+ linear-gradient(90deg, rgba(0, 255, 200, 0.02) 1px, transparent 1px);
67
+ background-size: 40px 40px;
68
+ pointer-events: none;
69
+ z-index: 0;
70
+ }
71
+
72
+ /* Header */
73
+ .header {
74
+ position: sticky;
75
+ top: 0;
76
+ z-index: 100;
77
+ background: linear-gradient(to bottom, var(--bg-deep) 0%, transparent 100%);
78
+ padding: 24px 32px 48px;
79
+ }
80
+
81
+ .header-content {
82
+ max-width: 1400px;
83
+ margin: 0 auto;
84
+ display: flex;
85
+ align-items: center;
86
+ justify-content: space-between;
87
+ }
88
+
89
+ .header-left {
90
+ display: flex;
91
+ align-items: center;
92
+ gap: 16px;
93
+ }
94
+
95
+ .back-link {
96
+ display: flex;
97
+ align-items: center;
98
+ gap: 8px;
99
+ color: var(--text-secondary);
100
+ text-decoration: none;
101
+ font-size: 14px;
102
+ font-weight: 500;
103
+ padding: 8px 12px;
104
+ border-radius: 6px;
105
+ transition: all 0.2s;
106
+ }
107
+
108
+ .back-link:hover {
109
+ color: var(--accent-cyan);
110
+ background: rgba(0, 255, 200, 0.1);
111
+ }
112
+
113
+ .logo {
114
+ display: flex;
115
+ align-items: center;
116
+ gap: 12px;
117
+ }
118
+
119
+ .logo-icon {
120
+ width: 32px;
121
+ height: 32px;
122
+ background: linear-gradient(135deg, var(--accent-cyan), var(--accent-blue));
123
+ border-radius: 8px;
124
+ display: flex;
125
+ align-items: center;
126
+ justify-content: center;
127
+ }
128
+
129
+ .logo-text {
130
+ font-size: 20px;
131
+ font-weight: 700;
132
+ letter-spacing: -0.5px;
133
+ }
134
+
135
+ .logo-text span {
136
+ color: var(--accent-cyan);
137
+ }
138
+
139
+ .live-indicator {
140
+ display: flex;
141
+ align-items: center;
142
+ gap: 8px;
143
+ padding: 6px 12px;
144
+ background: rgba(0, 230, 118, 0.1);
145
+ border: 1px solid rgba(0, 230, 118, 0.3);
146
+ border-radius: 20px;
147
+ font-family: var(--font-mono);
148
+ font-size: 12px;
149
+ font-weight: 500;
150
+ color: var(--accent-green);
151
+ }
152
+
153
+ .live-dot {
154
+ width: 8px;
155
+ height: 8px;
156
+ background: var(--accent-green);
157
+ border-radius: 50%;
158
+ animation: pulse 2s ease-in-out infinite;
159
+ box-shadow: var(--glow-green);
160
+ }
161
+
162
+ @keyframes pulse {
163
+ 0%, 100% { opacity: 1; transform: scale(1); }
164
+ 50% { opacity: 0.5; transform: scale(0.9); }
165
+ }
166
+
167
+ /* Main content */
168
+ .main {
169
+ position: relative;
170
+ z-index: 1;
171
+ max-width: 1400px;
172
+ margin: 0 auto;
173
+ padding: 0 32px 48px;
174
+ }
175
+
176
+ /* Stats overview */
177
+ .stats-grid {
178
+ display: grid;
179
+ grid-template-columns: repeat(4, 1fr);
180
+ gap: 20px;
181
+ margin-bottom: 32px;
182
+ }
183
+
184
+ .stat-card {
185
+ background: var(--bg-card);
186
+ border: 1px solid var(--border-dim);
187
+ border-radius: 12px;
188
+ padding: 24px;
189
+ position: relative;
190
+ overflow: hidden;
191
+ transition: all 0.3s ease;
192
+ }
193
+
194
+ .stat-card::before {
195
+ content: '';
196
+ position: absolute;
197
+ top: 0;
198
+ left: 0;
199
+ right: 0;
200
+ height: 2px;
201
+ background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent);
202
+ opacity: 0;
203
+ transition: opacity 0.3s;
204
+ }
205
+
206
+ .stat-card:hover {
207
+ border-color: var(--border-glow);
208
+ transform: translateY(-2px);
209
+ }
210
+
211
+ .stat-card:hover::before {
212
+ opacity: 1;
213
+ }
214
+
215
+ .stat-label {
216
+ font-size: 12px;
217
+ font-weight: 500;
218
+ text-transform: uppercase;
219
+ letter-spacing: 1px;
220
+ color: var(--text-muted);
221
+ margin-bottom: 8px;
222
+ }
223
+
224
+ .stat-value {
225
+ font-family: var(--font-mono);
226
+ font-size: 36px;
227
+ font-weight: 700;
228
+ color: var(--text-primary);
229
+ line-height: 1;
230
+ }
231
+
232
+ .stat-value.accent-cyan { color: var(--accent-cyan); }
233
+ .stat-value.accent-green { color: var(--accent-green); }
234
+ .stat-value.accent-orange { color: var(--accent-orange); }
235
+ .stat-value.accent-blue { color: var(--accent-blue); }
236
+
237
+ .stat-subtext {
238
+ font-size: 13px;
239
+ color: var(--text-secondary);
240
+ margin-top: 8px;
241
+ font-family: var(--font-mono);
242
+ }
243
+
244
+ /* Sections */
245
+ .section {
246
+ margin-bottom: 32px;
247
+ }
248
+
249
+ .section-header {
250
+ display: flex;
251
+ align-items: center;
252
+ justify-content: space-between;
253
+ margin-bottom: 16px;
254
+ }
255
+
256
+ .section-title {
257
+ font-size: 14px;
258
+ font-weight: 600;
259
+ text-transform: uppercase;
260
+ letter-spacing: 1.5px;
261
+ color: var(--text-secondary);
262
+ display: flex;
263
+ align-items: center;
264
+ gap: 10px;
265
+ }
266
+
267
+ .section-title::before {
268
+ content: '';
269
+ width: 3px;
270
+ height: 16px;
271
+ background: var(--accent-cyan);
272
+ border-radius: 2px;
273
+ }
274
+
275
+ /* Throughput panel */
276
+ .throughput-panel {
277
+ background: var(--bg-card);
278
+ border: 1px solid var(--border-dim);
279
+ border-radius: 12px;
280
+ padding: 28px;
281
+ }
282
+
283
+ .throughput-grid {
284
+ display: grid;
285
+ grid-template-columns: repeat(4, 1fr);
286
+ gap: 32px;
287
+ }
288
+
289
+ .throughput-item {
290
+ text-align: center;
291
+ }
292
+
293
+ .throughput-value {
294
+ font-family: var(--font-mono);
295
+ font-size: 48px;
296
+ font-weight: 700;
297
+ color: var(--accent-cyan);
298
+ line-height: 1;
299
+ text-shadow: var(--glow-cyan);
300
+ }
301
+
302
+ .throughput-label {
303
+ font-size: 13px;
304
+ color: var(--text-secondary);
305
+ margin-top: 8px;
306
+ text-transform: uppercase;
307
+ letter-spacing: 0.5px;
308
+ }
309
+
310
+ .throughput-bar {
311
+ height: 4px;
312
+ background: var(--bg-elevated);
313
+ border-radius: 2px;
314
+ margin-top: 12px;
315
+ overflow: hidden;
316
+ }
317
+
318
+ .throughput-bar-fill {
319
+ height: 100%;
320
+ background: linear-gradient(90deg, var(--accent-cyan), var(--accent-blue));
321
+ border-radius: 2px;
322
+ transition: width 0.5s ease;
323
+ }
324
+
325
+ /* Agent table */
326
+ .agents-table-container {
327
+ background: var(--bg-card);
328
+ border: 1px solid var(--border-dim);
329
+ border-radius: 12px;
330
+ overflow: hidden;
331
+ }
332
+
333
+ .agents-table {
334
+ width: 100%;
335
+ border-collapse: collapse;
336
+ }
337
+
338
+ .agents-table th,
339
+ .agents-table td {
340
+ padding: 16px 20px;
341
+ text-align: left;
342
+ }
343
+
344
+ .agents-table th {
345
+ background: var(--bg-elevated);
346
+ font-size: 11px;
347
+ font-weight: 600;
348
+ text-transform: uppercase;
349
+ letter-spacing: 1px;
350
+ color: var(--text-muted);
351
+ border-bottom: 1px solid var(--border-dim);
352
+ }
353
+
354
+ .agents-table tr {
355
+ border-bottom: 1px solid var(--border-dim);
356
+ transition: background 0.2s;
357
+ }
358
+
359
+ .agents-table tr:last-child {
360
+ border-bottom: none;
361
+ }
362
+
363
+ .agents-table tr:hover {
364
+ background: rgba(0, 255, 200, 0.02);
365
+ }
366
+
367
+ .agent-name {
368
+ display: flex;
369
+ align-items: center;
370
+ gap: 12px;
371
+ }
372
+
373
+ .agent-avatar {
374
+ width: 32px;
375
+ height: 32px;
376
+ border-radius: 6px;
377
+ display: flex;
378
+ align-items: center;
379
+ justify-content: center;
380
+ font-weight: 600;
381
+ font-size: 12px;
382
+ color: white;
383
+ }
384
+
385
+ .agent-name-text {
386
+ font-weight: 600;
387
+ font-family: var(--font-mono);
388
+ }
389
+
390
+ .status-badge {
391
+ display: inline-flex;
392
+ align-items: center;
393
+ gap: 6px;
394
+ padding: 4px 10px;
395
+ border-radius: 12px;
396
+ font-size: 12px;
397
+ font-weight: 500;
398
+ }
399
+
400
+ .status-badge.online {
401
+ background: rgba(0, 230, 118, 0.15);
402
+ color: var(--accent-green);
403
+ }
404
+
405
+ .status-badge.offline {
406
+ background: rgba(255, 92, 92, 0.15);
407
+ color: var(--accent-red);
408
+ }
409
+
410
+ .status-badge::before {
411
+ content: '';
412
+ width: 6px;
413
+ height: 6px;
414
+ border-radius: 50%;
415
+ background: currentColor;
416
+ }
417
+
418
+ .metric-cell {
419
+ font-family: var(--font-mono);
420
+ font-size: 14px;
421
+ }
422
+
423
+ .metric-cell.sent {
424
+ color: var(--accent-blue);
425
+ }
426
+
427
+ .metric-cell.received {
428
+ color: var(--accent-purple);
429
+ }
430
+
431
+ .uptime-cell {
432
+ font-family: var(--font-mono);
433
+ font-size: 13px;
434
+ color: var(--text-secondary);
435
+ }
436
+
437
+ /* Session lifecycle panel */
438
+ .lifecycle-panel {
439
+ background: var(--bg-card);
440
+ border: 1px solid var(--border-dim);
441
+ border-radius: 12px;
442
+ padding: 28px;
443
+ }
444
+
445
+ .lifecycle-grid {
446
+ display: grid;
447
+ grid-template-columns: repeat(5, 1fr);
448
+ gap: 24px;
449
+ }
450
+
451
+ .lifecycle-item {
452
+ text-align: center;
453
+ }
454
+
455
+ .lifecycle-value {
456
+ font-family: var(--font-mono);
457
+ font-size: 36px;
458
+ font-weight: 700;
459
+ line-height: 1;
460
+ }
461
+
462
+ .lifecycle-value.accent-green { color: var(--accent-green); }
463
+ .lifecycle-value.accent-blue { color: var(--accent-blue); }
464
+ .lifecycle-value.accent-orange { color: var(--accent-orange); }
465
+ .lifecycle-value.accent-red { color: var(--accent-red); }
466
+ .lifecycle-value.accent-purple { color: var(--accent-purple); }
467
+
468
+ .lifecycle-label {
469
+ font-size: 12px;
470
+ color: var(--text-secondary);
471
+ margin-top: 8px;
472
+ text-transform: uppercase;
473
+ letter-spacing: 0.5px;
474
+ }
475
+
476
+ .error-rate-indicator {
477
+ display: inline-flex;
478
+ align-items: center;
479
+ gap: 6px;
480
+ padding: 4px 10px;
481
+ border-radius: 12px;
482
+ font-size: 12px;
483
+ font-weight: 600;
484
+ font-family: var(--font-mono);
485
+ }
486
+
487
+ .error-rate-indicator.healthy {
488
+ background: rgba(0, 230, 118, 0.15);
489
+ color: var(--accent-green);
490
+ }
491
+
492
+ .error-rate-indicator.warning {
493
+ background: rgba(255, 158, 64, 0.15);
494
+ color: var(--accent-orange);
495
+ }
496
+
497
+ .error-rate-indicator.critical {
498
+ background: rgba(255, 92, 92, 0.15);
499
+ color: var(--accent-red);
500
+ }
501
+
502
+ .sessions-table {
503
+ width: 100%;
504
+ border-collapse: collapse;
505
+ margin-top: 20px;
506
+ }
507
+
508
+ .sessions-table th,
509
+ .sessions-table td {
510
+ padding: 12px 16px;
511
+ text-align: left;
512
+ }
513
+
514
+ .sessions-table th {
515
+ background: var(--bg-elevated);
516
+ font-size: 11px;
517
+ font-weight: 600;
518
+ text-transform: uppercase;
519
+ letter-spacing: 1px;
520
+ color: var(--text-muted);
521
+ border-bottom: 1px solid var(--border-dim);
522
+ }
523
+
524
+ .sessions-table tr {
525
+ border-bottom: 1px solid var(--border-dim);
526
+ }
527
+
528
+ .sessions-table tr:last-child {
529
+ border-bottom: none;
530
+ }
531
+
532
+ .closed-badge {
533
+ display: inline-flex;
534
+ align-items: center;
535
+ gap: 4px;
536
+ padding: 2px 8px;
537
+ border-radius: 10px;
538
+ font-size: 11px;
539
+ font-weight: 500;
540
+ }
541
+
542
+ .closed-badge.agent {
543
+ background: rgba(0, 230, 118, 0.15);
544
+ color: var(--accent-green);
545
+ }
546
+
547
+ .closed-badge.disconnect {
548
+ background: rgba(255, 158, 64, 0.15);
549
+ color: var(--accent-orange);
550
+ }
551
+
552
+ .closed-badge.error {
553
+ background: rgba(255, 92, 92, 0.15);
554
+ color: var(--accent-red);
555
+ }
556
+
557
+ .closed-badge.active {
558
+ background: rgba(74, 158, 255, 0.15);
559
+ color: var(--accent-blue);
560
+ }
561
+
562
+ @media (max-width: 1200px) {
563
+ .lifecycle-grid {
564
+ grid-template-columns: repeat(3, 1fr);
565
+ }
566
+ }
567
+
568
+ @media (max-width: 768px) {
569
+ .lifecycle-grid {
570
+ grid-template-columns: repeat(2, 1fr);
571
+ }
572
+ }
573
+
574
+ /* Empty state */
575
+ .empty-state {
576
+ padding: 64px 32px;
577
+ text-align: center;
578
+ }
579
+
580
+ .empty-state-icon {
581
+ width: 64px;
582
+ height: 64px;
583
+ margin: 0 auto 16px;
584
+ color: var(--text-muted);
585
+ opacity: 0.5;
586
+ }
587
+
588
+ .empty-state-text {
589
+ color: var(--text-muted);
590
+ font-size: 14px;
591
+ }
592
+
593
+ /* Last updated */
594
+ .last-updated {
595
+ text-align: center;
596
+ padding: 24px;
597
+ font-size: 12px;
598
+ color: var(--text-muted);
599
+ font-family: var(--font-mono);
600
+ }
601
+
602
+ /* Loading state */
603
+ .loading {
604
+ display: flex;
605
+ align-items: center;
606
+ justify-content: center;
607
+ padding: 64px;
608
+ }
609
+
610
+ .spinner {
611
+ width: 32px;
612
+ height: 32px;
613
+ border: 2px solid var(--border-subtle);
614
+ border-top-color: var(--accent-cyan);
615
+ border-radius: 50%;
616
+ animation: spin 1s linear infinite;
617
+ }
618
+
619
+ @keyframes spin {
620
+ to { transform: rotate(360deg); }
621
+ }
622
+
623
+ /* Responsive */
624
+ @media (max-width: 1200px) {
625
+ .stats-grid {
626
+ grid-template-columns: repeat(2, 1fr);
627
+ }
628
+ .throughput-grid {
629
+ grid-template-columns: repeat(2, 1fr);
630
+ gap: 24px;
631
+ }
632
+ }
633
+
634
+ @media (max-width: 768px) {
635
+ .header {
636
+ padding: 16px 20px 32px;
637
+ }
638
+ .main {
639
+ padding: 0 20px 32px;
640
+ }
641
+ .stats-grid {
642
+ grid-template-columns: 1fr;
643
+ }
644
+ .throughput-grid {
645
+ grid-template-columns: 1fr;
646
+ }
647
+ .agents-table th,
648
+ .agents-table td {
649
+ padding: 12px;
650
+ }
651
+ }
652
+
653
+ /* Animation on load */
654
+ .stat-card, .throughput-panel, .lifecycle-panel, .agents-table-container {
655
+ animation: fadeIn 0.5s ease forwards;
656
+ opacity: 0;
657
+ }
658
+
659
+ .stat-card:nth-child(1) { animation-delay: 0.1s; }
660
+ .stat-card:nth-child(2) { animation-delay: 0.15s; }
661
+ .stat-card:nth-child(3) { animation-delay: 0.2s; }
662
+ .stat-card:nth-child(4) { animation-delay: 0.25s; }
663
+ .throughput-panel { animation-delay: 0.3s; }
664
+ .lifecycle-panel { animation-delay: 0.35s; }
665
+ .agents-table-container { animation-delay: 0.45s; }
666
+
667
+ @keyframes fadeIn {
668
+ from {
669
+ opacity: 0;
670
+ transform: translateY(10px);
671
+ }
672
+ to {
673
+ opacity: 1;
674
+ transform: translateY(0);
675
+ }
676
+ }
677
+ </style>
678
+ </head>
679
+ <body>
680
+ <header class="header">
681
+ <div class="header-content">
682
+ <div class="header-left">
683
+ <a href="/" class="back-link">
684
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
685
+ <path d="M19 12H5M12 19l-7-7 7-7"/>
686
+ </svg>
687
+ Dashboard
688
+ </a>
689
+ <div class="logo">
690
+ <div class="logo-icon">
691
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
692
+ <path d="M3 3v18h18"/>
693
+ <path d="M18 17V9"/>
694
+ <path d="M13 17V5"/>
695
+ <path d="M8 17v-3"/>
696
+ </svg>
697
+ </div>
698
+ <div class="logo-text">Agent <span>Metrics</span></div>
699
+ </div>
700
+ </div>
701
+ <div class="live-indicator">
702
+ <span class="live-dot"></span>
703
+ LIVE
704
+ </div>
705
+ </div>
706
+ </header>
707
+
708
+ <main class="main">
709
+ <div id="content">
710
+ <div class="loading">
711
+ <div class="spinner"></div>
712
+ </div>
713
+ </div>
714
+ </main>
715
+
716
+ <script>
717
+ const REFRESH_INTERVAL = 5000;
718
+ const COLORS = ['#4a9eff', '#b388ff', '#ff9e40', '#00e676', '#ff5c5c', '#00ffc8'];
719
+
720
+ function getAvatarColor(name) {
721
+ let hash = 0;
722
+ for (let i = 0; i < name.length; i++) {
723
+ hash = name.charCodeAt(i) + ((hash << 5) - hash);
724
+ }
725
+ return COLORS[Math.abs(hash) % COLORS.length];
726
+ }
727
+
728
+ function getInitials(name) {
729
+ return name.slice(0, 2).toUpperCase();
730
+ }
731
+
732
+ function formatDuration(seconds) {
733
+ if (seconds < 60) return `${seconds}s`;
734
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
735
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
736
+ return `${Math.floor(seconds / 86400)}d ${Math.floor((seconds % 86400) / 3600)}h`;
737
+ }
738
+
739
+ function formatTime(isoString) {
740
+ return new Date(isoString).toLocaleTimeString('en-US', {
741
+ hour: '2-digit',
742
+ minute: '2-digit',
743
+ second: '2-digit',
744
+ hour12: false
745
+ });
746
+ }
747
+
748
+ function getErrorRateClass(rate) {
749
+ if (rate <= 1) return 'healthy';
750
+ if (rate <= 5) return 'warning';
751
+ return 'critical';
752
+ }
753
+
754
+ function getClosedByLabel(closedBy) {
755
+ if (!closedBy) return 'Active';
756
+ switch (closedBy) {
757
+ case 'agent': return 'Clean';
758
+ case 'disconnect': return 'Disconnect';
759
+ case 'error': return 'Error';
760
+ default: return closedBy;
761
+ }
762
+ }
763
+
764
+ function render(metrics) {
765
+ const content = document.getElementById('content');
766
+
767
+ // Calculate max throughput for bar scaling
768
+ const maxThroughput = Math.max(
769
+ metrics.throughput.messagesLastMinute * 60,
770
+ metrics.throughput.messagesLastHour,
771
+ metrics.throughput.messagesLast24Hours / 24,
772
+ 1
773
+ );
774
+
775
+ content.innerHTML = `
776
+ <!-- Stats Overview -->
777
+ <div class="stats-grid">
778
+ <div class="stat-card">
779
+ <div class="stat-label">Total Agents</div>
780
+ <div class="stat-value accent-cyan">${metrics.totalAgents}</div>
781
+ <div class="stat-subtext">${metrics.onlineAgents} online / ${metrics.offlineAgents} offline</div>
782
+ </div>
783
+ <div class="stat-card">
784
+ <div class="stat-label">Online Now</div>
785
+ <div class="stat-value accent-green">${metrics.onlineAgents}</div>
786
+ <div class="stat-subtext">${metrics.totalAgents > 0 ? Math.round((metrics.onlineAgents / metrics.totalAgents) * 100) : 0}% availability</div>
787
+ </div>
788
+ <div class="stat-card">
789
+ <div class="stat-label">Total Messages</div>
790
+ <div class="stat-value accent-blue">${metrics.totalMessages.toLocaleString()}</div>
791
+ <div class="stat-subtext">all time</div>
792
+ </div>
793
+ <div class="stat-card">
794
+ <div class="stat-label">Avg. Throughput</div>
795
+ <div class="stat-value accent-orange">${metrics.throughput.avgMessagesPerMinute}</div>
796
+ <div class="stat-subtext">messages / minute</div>
797
+ </div>
798
+ </div>
799
+
800
+ <!-- Throughput Section -->
801
+ <section class="section">
802
+ <div class="section-header">
803
+ <h2 class="section-title">Message Throughput</h2>
804
+ </div>
805
+ <div class="throughput-panel">
806
+ <div class="throughput-grid">
807
+ <div class="throughput-item">
808
+ <div class="throughput-value">${metrics.throughput.messagesLastMinute}</div>
809
+ <div class="throughput-label">Last Minute</div>
810
+ <div class="throughput-bar">
811
+ <div class="throughput-bar-fill" style="width: ${Math.min((metrics.throughput.messagesLastMinute / 10) * 100, 100)}%"></div>
812
+ </div>
813
+ </div>
814
+ <div class="throughput-item">
815
+ <div class="throughput-value">${metrics.throughput.messagesLastHour}</div>
816
+ <div class="throughput-label">Last Hour</div>
817
+ <div class="throughput-bar">
818
+ <div class="throughput-bar-fill" style="width: ${Math.min((metrics.throughput.messagesLastHour / 100) * 100, 100)}%"></div>
819
+ </div>
820
+ </div>
821
+ <div class="throughput-item">
822
+ <div class="throughput-value">${metrics.throughput.messagesLast24Hours}</div>
823
+ <div class="throughput-label">Last 24 Hours</div>
824
+ <div class="throughput-bar">
825
+ <div class="throughput-bar-fill" style="width: ${Math.min((metrics.throughput.messagesLast24Hours / 1000) * 100, 100)}%"></div>
826
+ </div>
827
+ </div>
828
+ <div class="throughput-item">
829
+ <div class="throughput-value">${metrics.throughput.avgMessagesPerMinute}</div>
830
+ <div class="throughput-label">Avg / Min</div>
831
+ <div class="throughput-bar">
832
+ <div class="throughput-bar-fill" style="width: ${Math.min((metrics.throughput.avgMessagesPerMinute / 5) * 100, 100)}%"></div>
833
+ </div>
834
+ </div>
835
+ </div>
836
+ </div>
837
+ </section>
838
+
839
+ <!-- Session Lifecycle Section -->
840
+ <section class="section">
841
+ <div class="section-header">
842
+ <h2 class="section-title">Session Lifecycle</h2>
843
+ <span class="error-rate-indicator ${getErrorRateClass(metrics.sessions?.errorRate || 0)}">
844
+ ${(metrics.sessions?.errorRate || 0).toFixed(1)}% error rate
845
+ </span>
846
+ </div>
847
+ <div class="lifecycle-panel">
848
+ <div class="lifecycle-grid">
849
+ <div class="lifecycle-item">
850
+ <div class="lifecycle-value accent-purple">${metrics.sessions?.totalSessions || 0}</div>
851
+ <div class="lifecycle-label">Total Sessions</div>
852
+ </div>
853
+ <div class="lifecycle-item">
854
+ <div class="lifecycle-value accent-blue">${metrics.sessions?.activeSessions || 0}</div>
855
+ <div class="lifecycle-label">Active</div>
856
+ </div>
857
+ <div class="lifecycle-item">
858
+ <div class="lifecycle-value accent-green">${metrics.sessions?.closedByAgent || 0}</div>
859
+ <div class="lifecycle-label">Clean Close</div>
860
+ </div>
861
+ <div class="lifecycle-item">
862
+ <div class="lifecycle-value accent-orange">${metrics.sessions?.closedByDisconnect || 0}</div>
863
+ <div class="lifecycle-label">Disconnect</div>
864
+ </div>
865
+ <div class="lifecycle-item">
866
+ <div class="lifecycle-value accent-red">${metrics.sessions?.closedByError || 0}</div>
867
+ <div class="lifecycle-label">Error</div>
868
+ </div>
869
+ </div>
870
+ ${metrics.sessions?.recentSessions?.length > 0 ? `
871
+ <table class="sessions-table">
872
+ <thead>
873
+ <tr>
874
+ <th>Agent</th>
875
+ <th>Status</th>
876
+ <th>Messages</th>
877
+ <th>Started</th>
878
+ <th>Duration</th>
879
+ </tr>
880
+ </thead>
881
+ <tbody>
882
+ ${metrics.sessions.recentSessions.slice(0, 5).map(session => {
883
+ const started = new Date(session.startedAt);
884
+ const ended = session.endedAt ? new Date(session.endedAt) : new Date();
885
+ const durationSec = Math.floor((ended - started) / 1000);
886
+ const closedClass = session.closedBy || 'active';
887
+ return `
888
+ <tr>
889
+ <td>
890
+ <div class="agent-name">
891
+ <div class="agent-avatar" style="background: ${getAvatarColor(session.agentName)}">
892
+ ${getInitials(session.agentName)}
893
+ </div>
894
+ <span class="agent-name-text">${session.agentName}</span>
895
+ </div>
896
+ </td>
897
+ <td>
898
+ <span class="closed-badge ${closedClass}">
899
+ ${getClosedByLabel(session.closedBy)}
900
+ </span>
901
+ </td>
902
+ <td class="metric-cell">${session.messageCount}</td>
903
+ <td class="uptime-cell">${formatTime(session.startedAt)}</td>
904
+ <td class="uptime-cell">${formatDuration(durationSec)}</td>
905
+ </tr>
906
+ `;
907
+ }).join('')}
908
+ </tbody>
909
+ </table>
910
+ ` : ''}
911
+ </div>
912
+ </section>
913
+
914
+ <!-- Agents Section -->
915
+ <section class="section">
916
+ <div class="section-header">
917
+ <h2 class="section-title">Agent Health</h2>
918
+ </div>
919
+ <div class="agents-table-container">
920
+ ${metrics.agents.length === 0 ? `
921
+ <div class="empty-state">
922
+ <svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
923
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
924
+ <circle cx="12" cy="7" r="4"/>
925
+ </svg>
926
+ <p class="empty-state-text">No agents registered yet</p>
927
+ </div>
928
+ ` : `
929
+ <table class="agents-table">
930
+ <thead>
931
+ <tr>
932
+ <th>Agent</th>
933
+ <th>Status</th>
934
+ <th>Messages Sent</th>
935
+ <th>Messages Received</th>
936
+ <th>Uptime</th>
937
+ <th>Last Seen</th>
938
+ </tr>
939
+ </thead>
940
+ <tbody>
941
+ ${metrics.agents.map(agent => `
942
+ <tr>
943
+ <td>
944
+ <div class="agent-name">
945
+ <div class="agent-avatar" style="background: ${getAvatarColor(agent.name)}">
946
+ ${getInitials(agent.name)}
947
+ </div>
948
+ <span class="agent-name-text">${agent.name}</span>
949
+ </div>
950
+ </td>
951
+ <td>
952
+ <span class="status-badge ${agent.isOnline ? 'online' : 'offline'}">
953
+ ${agent.isOnline ? 'Online' : 'Offline'}
954
+ </span>
955
+ </td>
956
+ <td class="metric-cell sent">${agent.messagesSent.toLocaleString()}</td>
957
+ <td class="metric-cell received">${agent.messagesReceived.toLocaleString()}</td>
958
+ <td class="uptime-cell">${formatDuration(agent.uptimeSeconds)}</td>
959
+ <td class="uptime-cell">${formatTime(agent.lastSeen)}</td>
960
+ </tr>
961
+ `).join('')}
962
+ </tbody>
963
+ </table>
964
+ `}
965
+ </div>
966
+ </section>
967
+
968
+ <div class="last-updated">
969
+ Last updated: ${formatTime(metrics.timestamp)}
970
+ </div>
971
+ `;
972
+ }
973
+
974
+ async function fetchMetrics() {
975
+ try {
976
+ const response = await fetch('/api/metrics');
977
+ if (!response.ok) throw new Error('Failed to fetch metrics');
978
+ const metrics = await response.json();
979
+ render(metrics);
980
+ } catch (err) {
981
+ console.error('Error fetching metrics:', err);
982
+ document.getElementById('content').innerHTML = `
983
+ <div class="empty-state">
984
+ <svg class="empty-state-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
985
+ <circle cx="12" cy="12" r="10"/>
986
+ <path d="M12 8v4M12 16h.01"/>
987
+ </svg>
988
+ <p class="empty-state-text">Failed to load metrics. Retrying...</p>
989
+ </div>
990
+ `;
991
+ }
992
+ }
993
+
994
+ // Initial fetch and set up refresh
995
+ fetchMetrics();
996
+ setInterval(fetchMetrics, REFRESH_INTERVAL);
997
+ </script>
998
+ </body>
999
+ </html>