ciscollm-cli 1.3.0 → 1.3.2

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 (47) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +242 -242
  3. package/dist/cli/commands/dashboardCommand.d.ts +1 -0
  4. package/dist/cli/commands/dashboardCommand.js +16 -0
  5. package/dist/cli/commands/dashboardCommand.js.map +1 -0
  6. package/dist/cli/commands/monitorCommand.d.ts +4 -0
  7. package/dist/cli/commands/monitorCommand.js +132 -0
  8. package/dist/cli/commands/monitorCommand.js.map +1 -0
  9. package/dist/cli/commands/runCommand.d.ts +6 -0
  10. package/dist/cli/commands/runCommand.js +635 -0
  11. package/dist/cli/commands/runCommand.js.map +1 -0
  12. package/dist/cli/commands/serverCommand.d.ts +1 -0
  13. package/dist/cli/commands/serverCommand.js +11 -0
  14. package/dist/cli/commands/serverCommand.js.map +1 -0
  15. package/dist/cli/commands/shellCommand.d.ts +1 -0
  16. package/dist/cli/commands/shellCommand.js +44 -0
  17. package/dist/cli/commands/shellCommand.js.map +1 -0
  18. package/dist/core/agent/AgentLoop.d.ts +0 -4
  19. package/dist/core/agent/AgentLoop.js +1 -158
  20. package/dist/core/agent/AgentLoop.js.map +1 -1
  21. package/dist/core/agent/AutoHealer.d.ts +12 -0
  22. package/dist/core/agent/AutoHealer.js +129 -26
  23. package/dist/core/agent/AutoHealer.js.map +1 -1
  24. package/dist/core/agent/HierarchicalAgentManager.d.ts +1 -1
  25. package/dist/core/agent/HierarchicalAgentManager.js +21 -5
  26. package/dist/core/agent/HierarchicalAgentManager.js.map +1 -1
  27. package/dist/core/agent/PromptEngine.js +33 -68
  28. package/dist/core/agent/PromptEngine.js.map +1 -1
  29. package/dist/core/guardrails/AuditLogger.js +4 -4
  30. package/dist/core/guardrails/CommandFirewall.js +15 -0
  31. package/dist/core/guardrails/CommandFirewall.js.map +1 -1
  32. package/dist/index.js +24 -903
  33. package/dist/index.js.map +1 -1
  34. package/dist/infrastructure/llm/LLMClient.js +102 -4
  35. package/dist/infrastructure/llm/LLMClient.js.map +1 -1
  36. package/dist/infrastructure/llm/ToolDefinitions.d.ts +0 -136
  37. package/dist/infrastructure/llm/ToolDefinitions.js +0 -102
  38. package/dist/infrastructure/llm/ToolDefinitions.js.map +1 -1
  39. package/dist/infrastructure/protocols/PlinkSerial.js +1 -1
  40. package/dist/infrastructure/protocols/PlinkSerial.js.map +1 -1
  41. package/dist/server/dashboard.js +1033 -1033
  42. package/dist/server/index.js +8 -8
  43. package/dist/server/shell-simulator.d.ts +28 -1
  44. package/dist/server/shell-simulator.js +599 -73
  45. package/dist/server/shell-simulator.js.map +1 -1
  46. package/dist/server/ssh.js +20 -20
  47. package/package.json +54 -54
@@ -111,1039 +111,1039 @@ function startDashboardServer(coordinator, port) {
111
111
  return server;
112
112
  }
113
113
  function getHtmlContent(port) {
114
- return `<!DOCTYPE html>
115
- <html lang="en">
116
- <head>
117
- <meta charset="UTF-8">
118
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
119
- <title>CiscoLLM Control Center</title>
120
- <!-- Google Fonts -->
121
- <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
122
- <!-- Vis Network CDN -->
123
- <script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
124
- <style>
125
- :root {
126
- --bg-color: #0B0F19;
127
- --bg-deep: #070A12;
128
- --card-bg: rgba(17, 24, 39, 0.7);
129
- --border-color: rgba(255, 255, 255, 0.08);
130
- --border-glow: rgba(99, 102, 241, 0.2);
131
- --text-main: #F3F4F6;
132
- --text-muted: #9CA3AF;
133
- --accent: #6366F1;
134
- --accent-glow: rgba(99, 102, 241, 0.35);
135
- --success: #10B981;
136
- --warning: #F59E0B;
137
- --danger: #EF4444;
138
- --glass-blur: blur(12px);
139
- }
140
-
141
- * {
142
- box-sizing: border-box;
143
- margin: 0;
144
- padding: 0;
145
- }
146
-
147
- body {
148
- font-family: 'Outfit', sans-serif;
149
- background-color: var(--bg-color);
150
- background-image: radial-gradient(circle at 10% 20%, rgba(99, 102, 241, 0.05) 0%, transparent 40%),
151
- radial-gradient(circle at 90% 80%, rgba(16, 185, 129, 0.03) 0%, transparent 40%);
152
- color: var(--text-main);
153
- min-height: 100vh;
154
- display: flex;
155
- flex-direction: column;
156
- overflow-x: hidden;
157
- }
158
-
159
- header {
160
- background: rgba(11, 15, 25, 0.8);
161
- backdrop-filter: var(--glass-blur);
162
- border-bottom: 1px solid var(--border-color);
163
- padding: 1rem 1.5rem;
164
- display: flex;
165
- flex-wrap: wrap;
166
- justify-content: space-between;
167
- align-items: center;
168
- position: sticky;
169
- top: 0;
170
- z-index: 100;
171
- gap: 1rem;
172
- }
173
-
174
- .brand-container {
175
- display: flex;
176
- align-items: center;
177
- gap: 0.75rem;
178
- }
179
-
180
- .brand-logo {
181
- width: 10px;
182
- height: 10px;
183
- background-color: var(--success);
184
- border-radius: 50%;
185
- box-shadow: 0 0 12px var(--success);
186
- animation: pulse-active 2s infinite;
187
- }
188
-
189
- header h1 {
190
- font-size: 1.35rem;
191
- font-weight: 700;
192
- letter-spacing: -0.5px;
193
- background: linear-gradient(135deg, #FFF 60%, var(--accent) 100%);
194
- -webkit-background-clip: text;
195
- -webkit-text-fill-color: transparent;
196
- }
197
-
198
- header h1 span {
199
- font-weight: 400;
200
- font-size: 0.9rem;
201
- color: var(--text-muted);
202
- margin-left: 0.5rem;
203
- }
204
-
205
- .actions {
206
- display: flex;
207
- gap: 0.75rem;
208
- align-items: center;
209
- }
210
-
211
- button {
212
- padding: 0.6rem 1.2rem;
213
- border-radius: 0.5rem;
214
- border: 1px solid transparent;
215
- font-weight: 600;
216
- font-size: 0.9rem;
217
- cursor: pointer;
218
- transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
219
- font-family: inherit;
220
- display: flex;
221
- align-items: center;
222
- gap: 0.5rem;
223
- }
224
-
225
- .btn-primary {
226
- background-color: var(--accent);
227
- color: white;
228
- box-shadow: 0 4px 14px var(--accent-glow);
229
- }
230
-
231
- .btn-primary:hover {
232
- background-color: #4f46e5;
233
- transform: translateY(-1px);
234
- }
235
-
236
- .btn-danger {
237
- background: rgba(239, 68, 68, 0.1);
238
- color: var(--danger);
239
- border: 1px solid rgba(239, 68, 68, 0.3);
240
- }
241
-
242
- .btn-danger:hover {
243
- background: var(--danger);
244
- color: white;
245
- box-shadow: 0 4px 14px rgba(239, 68, 68, 0.4);
246
- transform: translateY(-1px);
247
- }
248
-
249
- /* Responsive Metric Cards Grid */
250
- .metrics-grid {
251
- display: grid;
252
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
253
- gap: 1rem;
254
- padding: 1.5rem 1.5rem 0 1.5rem;
255
- }
256
-
257
- .metric-card {
258
- background: var(--card-bg);
259
- backdrop-filter: var(--glass-blur);
260
- border: 1px solid var(--border-color);
261
- border-radius: 0.75rem;
262
- padding: 1.25rem;
263
- display: flex;
264
- align-items: center;
265
- justify-content: space-between;
266
- transition: transform 0.2s ease, border-color 0.2s ease;
267
- }
268
-
269
- .metric-card:hover {
270
- border-color: var(--border-glow);
271
- transform: translateY(-2px);
272
- }
273
-
274
- .metric-info h3 {
275
- font-size: 0.8rem;
276
- color: var(--text-muted);
277
- text-transform: uppercase;
278
- letter-spacing: 0.5px;
279
- margin-bottom: 0.25rem;
280
- }
281
-
282
- .metric-info .value {
283
- font-size: 1.75rem;
284
- font-weight: 700;
285
- }
286
-
287
- .metric-icon {
288
- font-size: 1.5rem;
289
- opacity: 0.8;
290
- color: var(--accent);
291
- }
292
-
293
- /* Tabs Navigation */
294
- .tab-bar {
295
- display: flex;
296
- padding: 0 1.5rem;
297
- margin-top: 1.5rem;
298
- border-bottom: 1px solid var(--border-color);
299
- gap: 1.5rem;
300
- overflow-x: auto;
301
- white-space: nowrap;
302
- }
303
-
304
- .tab-button {
305
- background: none;
306
- border: none;
307
- color: var(--text-muted);
308
- padding: 0.75rem 0.25rem;
309
- font-weight: 500;
310
- font-size: 0.95rem;
311
- border-bottom: 2px solid transparent;
312
- border-radius: 0;
313
- transition: all 0.2s ease;
314
- }
315
-
316
- .tab-button:hover {
317
- color: var(--text-main);
318
- }
319
-
320
- .tab-button.active {
321
- color: var(--accent);
322
- border-bottom-color: var(--accent);
323
- font-weight: 600;
324
- }
325
-
326
- /* Tab Contents Layout */
327
- .tab-content {
328
- display: none;
329
- flex: 1;
330
- padding: 1.5rem;
331
- animation: fadeIn 0.3s ease-in-out;
332
- }
333
-
334
- .tab-content.active {
335
- display: flex;
336
- flex-direction: column;
337
- }
338
-
339
- /* Panel Design */
340
- .panel-container {
341
- display: grid;
342
- grid-template-columns: 1.4fr 1fr;
343
- gap: 1.5rem;
344
- flex: 1;
345
- }
346
-
347
- .panel {
348
- background: var(--card-bg);
349
- backdrop-filter: var(--glass-blur);
350
- border: 1px solid var(--border-color);
351
- border-radius: 0.75rem;
352
- display: flex;
353
- flex-direction: column;
354
- overflow: hidden;
355
- min-height: 450px;
356
- }
357
-
358
- .panel-header {
359
- padding: 1rem 1.25rem;
360
- border-bottom: 1px solid var(--border-color);
361
- font-weight: 600;
362
- display: flex;
363
- justify-content: space-between;
364
- align-items: center;
365
- font-size: 1rem;
366
- }
367
-
368
- .panel-content {
369
- flex: 1;
370
- overflow-y: auto;
371
- padding: 1.25rem;
372
- }
373
-
374
- /* Topology Canvas style */
375
- #topology-canvas {
376
- position: absolute;
377
- top: 0;
378
- left: 0;
379
- width: 100%;
380
- height: 100%;
381
- background-color: var(--bg-deep);
382
- }
383
-
384
- /* Active Sessions cards styling */
385
- .session-grid {
386
- display: grid;
387
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
388
- gap: 1rem;
389
- }
390
-
391
- .session-card {
392
- background: rgba(30, 41, 59, 0.4);
393
- border: 1px solid var(--border-color);
394
- border-radius: 0.5rem;
395
- padding: 1rem;
396
- display: flex;
397
- flex-direction: column;
398
- gap: 0.5rem;
399
- transition: border-color 0.2s ease;
400
- }
401
-
402
- .session-card:hover {
403
- border-color: var(--accent);
404
- }
405
-
406
- .session-title {
407
- display: flex;
408
- justify-content: space-between;
409
- align-items: center;
410
- font-weight: 600;
411
- }
412
-
413
- .session-badge {
414
- font-size: 0.75rem;
415
- padding: 0.15rem 0.4rem;
416
- border-radius: 0.25rem;
417
- font-weight: 600;
418
- background: rgba(99, 102, 241, 0.15);
419
- color: #a5b4fc;
420
- }
421
-
422
- .session-field {
423
- font-size: 0.85rem;
424
- color: var(--text-muted);
425
- display: flex;
426
- justify-content: space-between;
427
- }
428
-
429
- .session-field span:last-child {
430
- color: var(--text-main);
431
- font-family: 'JetBrains Mono', monospace;
432
- }
433
-
434
- /* Log console styling */
435
- .console-controls {
436
- display: flex;
437
- flex-wrap: wrap;
438
- gap: 0.75rem;
439
- margin-bottom: 1rem;
440
- align-items: center;
441
- }
442
-
443
- .search-input {
444
- background: rgba(15, 23, 42, 0.6);
445
- border: 1px solid var(--border-color);
446
- border-radius: 0.375rem;
447
- padding: 0.5rem 0.75rem;
448
- color: var(--text-main);
449
- font-family: inherit;
450
- font-size: 0.85rem;
451
- flex: 1;
452
- min-width: 200px;
453
- }
454
-
455
- .search-input:focus {
456
- outline: none;
457
- border-color: var(--accent);
458
- }
459
-
460
- .filter-group {
461
- display: flex;
462
- gap: 0.35rem;
463
- overflow-x: auto;
464
- }
465
-
466
- .filter-btn {
467
- background: rgba(30, 41, 59, 0.5);
468
- border: 1px solid var(--border-color);
469
- color: var(--text-muted);
470
- padding: 0.35rem 0.75rem;
471
- font-size: 0.8rem;
472
- border-radius: 0.25rem;
473
- font-weight: 500;
474
- }
475
-
476
- .filter-btn.active {
477
- background: var(--accent);
478
- color: white;
479
- border-color: var(--accent);
480
- }
481
-
482
- .log-list {
483
- display: flex;
484
- flex-direction: column;
485
- gap: 0.75rem;
486
- }
487
-
488
- .log-item {
489
- background: rgba(15, 23, 42, 0.4);
490
- border: 1px solid var(--border-color);
491
- border-radius: 0.5rem;
492
- padding: 0.85rem 1rem;
493
- font-family: 'JetBrains Mono', monospace;
494
- font-size: 0.85rem;
495
- animation: slideIn 0.2s ease-out;
496
- }
497
-
498
- .log-header {
499
- display: flex;
500
- justify-content: space-between;
501
- margin-bottom: 0.35rem;
502
- flex-wrap: wrap;
503
- gap: 0.5rem;
504
- }
505
-
506
- .status-badge {
507
- display: inline-block;
508
- padding: 0.15rem 0.4rem;
509
- border-radius: 0.25rem;
510
- font-size: 0.725rem;
511
- font-weight: 700;
512
- text-transform: uppercase;
513
- }
514
-
515
- .status-success { background: rgba(16, 185, 129, 0.15); color: var(--success); }
516
- .status-rollback { background: rgba(245, 158, 11, 0.15); color: var(--warning); }
517
- .status-blocked { background: rgba(239, 68, 68, 0.15); color: var(--danger); }
518
-
519
- .log-thought {
520
- color: var(--text-muted);
521
- font-style: italic;
522
- margin-top: 0.35rem;
523
- font-family: 'Outfit', sans-serif;
524
- font-size: 0.8rem;
525
- border-left: 2px solid var(--border-glow);
526
- padding-left: 0.5rem;
527
- }
528
-
529
- /* Diff styling */
530
- .diff-added { color: var(--success); font-family: 'JetBrains Mono', monospace; }
531
- .diff-removed { color: var(--danger); font-family: 'JetBrains Mono', monospace; }
532
- .diff-modified { color: var(--warning); font-family: 'JetBrains Mono', monospace; }
533
-
534
- /* Comparative Audit Styling */
535
- .audit-visualizer {
536
- display: grid;
537
- grid-template-columns: 1fr 1fr;
538
- gap: 1.5rem;
539
- }
540
-
541
- .audit-card {
542
- background: rgba(30, 41, 59, 0.3);
543
- border: 1px solid var(--border-color);
544
- border-radius: 0.5rem;
545
- padding: 1rem;
546
- }
547
-
548
- .audit-card h4 {
549
- font-size: 0.9rem;
550
- text-transform: uppercase;
551
- letter-spacing: 0.5px;
552
- color: var(--text-muted);
553
- margin-bottom: 0.75rem;
554
- border-bottom: 1px solid var(--border-color);
555
- padding-bottom: 0.35rem;
556
- }
557
-
558
- .audit-metric {
559
- display: flex;
560
- justify-content: space-between;
561
- padding: 0.5rem 0;
562
- border-bottom: 1px dashed rgba(255,255,255,0.03);
563
- font-size: 0.9rem;
564
- }
565
-
566
- .audit-metric:last-child {
567
- border-bottom: none;
568
- }
569
-
570
- /* Connection Status Overlay */
571
- .disconnect-overlay {
572
- position: fixed;
573
- top: 0;
574
- left: 0;
575
- width: 100vw;
576
- height: 100vh;
577
- background: rgba(7, 10, 18, 0.9);
578
- backdrop-filter: blur(8px);
579
- z-index: 1000;
580
- display: flex;
581
- flex-direction: column;
582
- justify-content: center;
583
- align-items: center;
584
- opacity: 0;
585
- pointer-events: none;
586
- transition: opacity 0.4s ease;
587
- }
588
-
589
- .disconnect-overlay.visible {
590
- opacity: 1;
591
- pointer-events: auto;
592
- }
593
-
594
- .disconnect-box {
595
- background: rgba(17, 24, 39, 0.85);
596
- border: 1px solid var(--danger);
597
- border-radius: 1rem;
598
- padding: 2.5rem;
599
- text-align: center;
600
- box-shadow: 0 0 30px rgba(239, 68, 68, 0.15);
601
- max-width: 450px;
602
- width: 90%;
603
- animation: slideIn 0.3s ease-out;
604
- }
605
-
606
- .disconnect-title {
607
- font-size: 1.5rem;
608
- font-weight: 700;
609
- color: var(--danger);
610
- margin-bottom: 1rem;
611
- }
612
-
613
- .disconnect-text {
614
- color: var(--text-muted);
615
- font-size: 0.95rem;
616
- line-height: 1.5;
617
- margin-bottom: 1.5rem;
618
- }
619
-
620
- .spinner {
621
- width: 40px;
622
- height: 40px;
623
- border: 4px solid rgba(255, 255, 255, 0.1);
624
- border-top: 4px solid var(--danger);
625
- border-radius: 50%;
626
- animation: spin 1s linear infinite;
627
- margin: 0 auto 1.5rem auto;
628
- }
629
-
630
- @keyframes spin {
631
- 0% { transform: rotate(0deg); }
632
- 100% { transform: rotate(360deg); }
633
- }
634
-
635
- /* Responsive Layout Overrides */
636
- @media (max-width: 1024px) {
637
- .panel-container {
638
- grid-template-columns: 1fr;
639
- }
640
- .audit-visualizer {
641
- grid-template-columns: 1fr;
642
- }
643
- }
644
-
645
- @media (max-width: 640px) {
646
- header {
647
- padding: 0.75rem 1rem;
648
- }
649
- header h1 {
650
- font-size: 1.15rem;
651
- }
652
- .metrics-grid {
653
- grid-template-columns: 1fr;
654
- padding: 1rem 1rem 0 1rem;
655
- }
656
- .tab-bar {
657
- padding: 0 1rem;
658
- margin-top: 1rem;
659
- }
660
- .tab-content {
661
- padding: 1rem;
662
- }
663
- .console-controls {
664
- flex-direction: column;
665
- align-items: stretch;
666
- }
667
- }
668
-
669
- /* Keyframes */
670
- @keyframes pulse-active {
671
- 0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); }
672
- 70% { box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); }
673
- 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
674
- }
675
-
676
- @keyframes fadeIn {
677
- from { opacity: 0; transform: translateY(3px); }
678
- to { opacity: 1; transform: translateY(0); }
679
- }
680
-
681
- @keyframes slideIn {
682
- from { transform: translateY(5px); opacity: 0; }
683
- to { transform: translateY(0); opacity: 1; }
684
- }
685
- </style>
686
- </head>
687
- <body>
688
- <header>
689
- <div class="brand-container">
690
- <div class="brand-logo"></div>
691
- <h1>CiscoLLM <span>Visual Control Dashboard v1.1.0</span></h1>
692
- </div>
693
- <div class="actions">
694
- <button class="btn-primary" onclick="reloadData()">Refresh</button>
695
- <button class="btn-danger" onclick="triggerRollback()">Emergency Rollback</button>
696
- </div>
697
- </header>
698
-
699
- <!-- Disconnect Overlay -->
700
- <div id="disconnect-overlay" class="disconnect-overlay">
701
- <div class="disconnect-box">
702
- <div class="spinner"></div>
703
- <div class="disconnect-title">Connection Lost</div>
704
- <div class="disconnect-text">
705
- Disconnected from the CiscoLLM Swarm. Ensure your local CLI execution is active and the API server is running.
706
- </div>
707
- </div>
708
- </div>
709
-
710
- <!-- Top Metrics Overview -->
711
- <div class="metrics-grid">
712
- <div class="metric-card">
713
- <div class="metric-info">
714
- <h3>Devices Swarm</h3>
715
- <div class="value" id="count-devices">0</div>
716
- </div>
717
- <div class="metric-icon">💻</div>
718
- </div>
719
- <div class="metric-card">
720
- <div class="metric-info">
721
- <h3>Topology Links</h3>
722
- <div class="value" id="count-links">0</div>
723
- </div>
724
- <div class="metric-icon">🔗</div>
725
- </div>
726
- <div class="metric-card">
727
- <div class="metric-info">
728
- <h3>Action Logs</h3>
729
- <div class="value" id="count-logs">0</div>
730
- </div>
731
- <div class="metric-icon">📜</div>
732
- </div>
733
- <div class="metric-card">
734
- <div class="metric-info">
735
- <h3>Safety Audits</h3>
736
- <div class="value" style="color: var(--success);">Passed</div>
737
- </div>
738
- <div class="metric-icon">🛡️</div>
739
- </div>
740
- </div>
741
-
742
- <!-- Tab Bar -->
743
- <div class="tab-bar">
744
- <button class="tab-button active" onclick="switchTab('topology-tab')">Network Topology</button>
745
- <button class="tab-button" onclick="switchTab('sessions-tab')">Device Sessions</button>
746
- <button class="tab-button" onclick="switchTab('logs-tab')">Agent Action Logs</button>
747
- <button class="tab-button" onclick="switchTab('diffs-tab')">Config Diffs</button>
748
- </div>
749
-
750
- <!-- Tab Contents -->
751
- <!-- 1. Topology -->
752
- <div id="topology-tab" class="tab-content active">
753
- <div class="panel-container">
754
- <div class="panel">
755
- <div class="panel-header">Interactive Swarm Topology Map</div>
756
- <div style="flex: 1; overflow: hidden; position: relative;">
757
- <div id="topology-canvas"></div>
758
- </div>
759
- </div>
760
- <div class="panel">
761
- <div class="panel-header">Pre/Post Flight Audits Compare</div>
762
- <div class="panel-content" id="audit-compare-container">
763
- <div style="color: var(--text-muted);">No comparison snapshot captured. Trigger commands to evaluate audits.</div>
764
- </div>
765
- </div>
766
- </div>
767
- </div>
768
-
769
- <!-- 2. Device Sessions -->
770
- <div id="sessions-tab" class="tab-content">
771
- <div class="session-grid" id="sessions-container">
772
- <div style="color: var(--text-muted); grid-column: 1/-1;">No connected device sessions found.</div>
773
- </div>
774
- </div>
775
-
776
- <!-- 3. Agent Logs -->
777
- <div id="logs-tab" class="tab-content">
778
- <div class="panel" style="min-height: 480px;">
779
- <div class="panel-header">Real-time Swarm Command Firewalls & Logs</div>
780
- <div class="panel-content">
781
- <div class="console-controls">
782
- <input type="text" id="log-search" class="search-input" placeholder="Search commands, thoughts, or error logs..." oninput="filterLogs()">
783
- <div class="filter-group">
784
- <button class="filter-btn active" id="filter-all" onclick="setLogFilter('ALL')">All</button>
785
- <button class="filter-btn" id="filter-success" onclick="setLogFilter('SUCCESS')">Success</button>
786
- <button class="filter-btn" id="filter-blocked" onclick="setLogFilter('BLOCKED')">Blocked</button>
787
- <button class="filter-btn" id="filter-rollback" onclick="setLogFilter('ROLLBACK')">Rollback</button>
788
- </div>
789
- </div>
790
- <div class="log-list" id="logs-container">
791
- <div style="color: var(--text-muted);">No log entries recorded yet.</div>
792
- </div>
793
- </div>
794
- </div>
795
- </div>
796
-
797
- <!-- 4. Diffs -->
798
- <div id="diffs-tab" class="tab-content">
799
- <div class="panel" style="min-height: 480px;">
800
- <div class="panel-header">Dynamic Configuration Diffs (Before & After Snapshots)</div>
801
- <div class="panel-content" id="diffs-container">
802
- <div style="color: var(--text-muted);">No configuration diffs captured yet. Run changes to see differences.</div>
803
- </div>
804
- </div>
805
- </div>
806
-
807
- <script>
808
- let network = null;
809
- let activeTab = 'topology-tab';
810
- let rawLogs = [];
811
- let logFilter = 'ALL';
812
- let isConnected = true;
813
-
814
- let lastTopologyJson = '';
815
- let lastSessionsJson = '';
816
- let lastLogsJson = '';
817
- let lastDiffsJson = '';
818
- let lastAuditsJson = '';
819
-
820
- function switchTab(tabId) {
821
- document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
822
- document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
823
-
824
- const selectedBtn = Array.from(document.querySelectorAll('.tab-button')).find(btn => btn.getAttribute('onclick').includes(tabId));
825
- if (selectedBtn) selectedBtn.classList.add('active');
826
-
827
- document.getElementById(tabId).classList.add('active');
828
- activeTab = tabId;
829
-
830
- if (tabId === 'topology-tab' && network) {
831
- setTimeout(() => network.fit(), 200);
832
- }
833
- }
834
-
835
- async function reloadData() {
836
- try {
837
- const res = await fetch('/api/state');
838
- if (!res.ok) throw new Error("HTTP " + res.status);
839
- const data = await res.json();
840
-
841
- if (!isConnected) {
842
- isConnected = true;
843
- document.getElementById('disconnect-overlay').classList.remove('visible');
844
- }
845
-
846
- // Update UI sections
847
- updateTopologyUI(data.topology);
848
- updateSessionsUI(data.sessions);
849
- updateLogsUI(data.logs);
850
- updateDiffsUI(data.diffs);
851
- updateAuditsUI(data.audits);
852
-
853
- } catch (e) {
854
- console.error("Connection failed: ", e);
855
- if (isConnected) {
856
- isConnected = false;
857
- document.getElementById('disconnect-overlay').classList.add('visible');
858
- }
859
- }
860
- }
861
-
862
- function updateTopologyUI(data) {
863
- const stableData = {
864
- nodes: (data && data.nodes) ? data.nodes : [],
865
- links: (data && data.links) ? data.links : []
866
- };
867
- const currentJson = JSON.stringify(stableData);
868
- if (currentJson === lastTopologyJson) {
869
- return;
870
- }
871
- lastTopologyJson = currentJson;
872
-
873
- const container = document.getElementById('topology-canvas');
874
- const nodes = [];
875
- const edges = [];
876
-
877
- if (data && data.nodes) {
878
- document.getElementById('count-devices').innerText = data.nodes.length;
879
- data.nodes.forEach(node => {
880
- const isSwitch = node.toLowerCase().includes('switch');
881
- nodes.push({
882
- id: node,
883
- label: node,
884
- shape: 'box',
885
- margin: 12,
886
- color: {
887
- background: isSwitch ? '#1E293B' : '#4F46E5',
888
- border: '#6366F1'
889
- },
890
- font: { color: '#ffffff', size: 14, face: 'Outfit' }
891
- });
892
- });
893
- }
894
-
895
- if (data && data.links) {
896
- document.getElementById('count-links').innerText = data.links.length;
897
- data.links.forEach((link, idx) => {
898
- edges.push({
899
- id: 'e' + idx,
900
- from: link.localDeviceId,
901
- to: link.remoteDeviceId,
902
- label: link.localInterface + ' ↔ ' + link.remoteInterface,
903
- font: { color: '#9CA3AF', size: 10, strokeWidth: 0, face: 'Outfit' },
904
- color: { color: '#4B5563' }
905
- });
906
- });
907
- }
908
-
909
- const visData = {
910
- nodes: new vis.DataSet(nodes),
911
- edges: new vis.DataSet(edges)
912
- };
913
-
914
- const options = {
915
- physics: { enabled: true, solver: 'repulsion', repulsion: { nodeDistance: 150 } },
916
- layout: { randomSeed: 42 },
917
- interaction: { keyboard: false }
918
- };
919
-
920
- if (network) network.destroy();
921
- network = new vis.Network(container, visData, options);
922
- }
923
-
924
- function updateSessionsUI(data) {
925
- const currentJson = JSON.stringify(data);
926
- if (currentJson === lastSessionsJson) {
927
- return;
928
- }
929
- lastSessionsJson = currentJson;
930
- const container = document.getElementById('sessions-container');
931
-
932
- const keys = Object.keys(data);
933
- if (keys.length === 0) {
934
- container.innerHTML = '<div style="color: var(--text-muted); grid-column: 1/-1;">No connected device sessions found.</div>';
935
- return;
936
- }
937
-
938
- let cards = '';
939
- keys.forEach(id => {
940
- const session = data[id];
941
- cards += '<div class="session-card">';
942
- cards += '<div class="session-title">';
943
- cards += '<span>' + (session.hostname || id) + '</span>';
944
- cards += '<span class="session-badge">' + session.currentMode + '</span>';
945
- cards += '</div>';
946
- cards += '<div class="session-field"><span>Target URI</span><span>' + id + '</span></div>';
947
- cards += '<div class="session-field"><span>Prompt</span><span>' + session.prompt + '</span></div>';
948
- cards += '<div class="session-field"><span>Status</span><span style="color: var(--success);">● Active</span></div>';
949
- cards += '</div>';
950
- });
951
- container.innerHTML = cards;
952
- }
953
-
954
- function updateLogsUI(logs) {
955
- rawLogs = logs;
956
- const currentJson = JSON.stringify(rawLogs);
957
- if (currentJson === lastLogsJson) {
958
- return;
959
- }
960
- lastLogsJson = currentJson;
961
-
962
- document.getElementById('count-logs').innerText = rawLogs.length;
963
- filterLogs();
964
- }
965
-
966
- function setLogFilter(filter) {
967
- logFilter = filter;
968
- document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
969
- const filterId = 'filter-' + filter.toLowerCase();
970
- const btn = document.getElementById(filterId);
971
- if (btn) btn.classList.add('active');
972
- filterLogs();
973
- }
974
-
975
- function filterLogs() {
976
- const container = document.getElementById('logs-container');
977
- const searchVal = document.getElementById('log-search').value.toLowerCase().trim();
978
-
979
- const filtered = rawLogs.filter(log => {
980
- if (logFilter !== 'ALL' && log.status !== logFilter) return false;
981
- if (searchVal) {
982
- const matchCommand = log.command && log.command.toLowerCase().includes(searchVal);
983
- const matchThought = log.thought && log.thought.toLowerCase().includes(searchVal);
984
- const matchReason = log.reason && log.reason.toLowerCase().includes(searchVal);
985
- const matchDevice = log.deviceId && log.deviceId.toLowerCase().includes(searchVal);
986
- return matchCommand || matchThought || matchReason || matchDevice;
987
- }
988
- return true;
989
- });
990
-
991
- if (filtered.length === 0) {
992
- container.innerHTML = '<div style="color: var(--text-muted);">No matching log entries found.</div>';
993
- return;
994
- }
995
-
996
- container.innerHTML = filtered.map(log => {
997
- const statusClass = 'status-' + log.status.toLowerCase();
998
- let itemHtml = '<div class="log-item">';
999
- itemHtml += '<div class="log-header">';
1000
- itemHtml += '<span><strong>[' + log.deviceId + ']</strong> ' + log.command + '</span>';
1001
- itemHtml += '<span class="status-badge ' + statusClass + '">' + log.status + '</span>';
1002
- itemHtml += '</div>';
1003
- if (log.thought) {
1004
- itemHtml += '<div class="log-thought">AI: ' + log.thought + '</div>';
1005
- }
1006
- if (log.reason) {
1007
- itemHtml += '<div style="color: var(--danger); margin-top: 0.35rem;">Rule Violation: ' + log.reason + '</div>';
1008
- }
1009
- itemHtml += '</div>';
1010
- return itemHtml;
1011
- }).join('');
1012
- }
1013
-
1014
- function updateDiffsUI(data) {
1015
- const currentJson = JSON.stringify(data);
1016
- if (currentJson === lastDiffsJson) {
1017
- return;
1018
- }
1019
- lastDiffsJson = currentJson;
1020
- const container = document.getElementById('diffs-container');
1021
-
1022
- if (!data || data.length === 0) {
1023
- container.innerHTML = '<div style="color: var(--text-muted);">No configuration diffs captured yet. Apply modifications via CLI.</div>';
1024
- return;
1025
- }
1026
-
1027
- let diffsHtml = data.map(item => {
1028
- let diffHtml = '';
1029
- const diff = item.diff;
1030
-
1031
- if (diff.hostnameChanged) {
1032
- diffHtml += '<div class="diff-modified">Hostname Changed: "' + diff.hostnameChanged.before + '" ➔ "' + diff.hostnameChanged.after + '"</div>';
1033
- }
1034
- if (diff.modifiedInterfaces && diff.modifiedInterfaces.length > 0) {
1035
- diff.modifiedInterfaces.forEach(inf => {
1036
- diffHtml += '<div class="diff-modified">Interface ' + inf.name + ' changes:</div>';
1037
- inf.changes.forEach(c => {
1038
- diffHtml += '<div style="padding-left: 1rem;">- ' + c.field + ': "' + c.before + '" ➔ "' + c.after + '"</div>';
1039
- });
1040
- });
1041
- }
1042
- if (diff.addedRoutes && diff.addedRoutes.length > 0) {
1043
- diff.addedRoutes.forEach(r => {
1044
- diffHtml += '<div class="diff-added">+ ip route ' + r.network + ' ' + r.mask + ' ' + (r.nextHop || '') + '</div>';
1045
- });
1046
- }
1047
- if (diff.removedRoutes && diff.removedRoutes.length > 0) {
1048
- diff.removedRoutes.forEach(r => {
1049
- diffHtml += '<div class="diff-removed">- ip route ' + r.network + ' ' + r.mask + ' ' + (r.nextHop || '') + '</div>';
1050
- });
1051
- }
1052
- if (diff.addedVlans && diff.addedVlans.length > 0) {
1053
- diffHtml += '<div class="diff-added">+ VLANs Added: ' + diff.addedVlans.join(', ') + '</div>';
1054
- }
1055
- if (diff.removedVlans && diff.removedVlans.length > 0) {
1056
- diffHtml += '<div class="diff-removed">- VLANs Removed: ' + diff.removedVlans.join(', ') + '</div>';
1057
- }
1058
-
1059
- if (!diffHtml) {
1060
- diffHtml = '<div style="color: var(--text-muted);">No configuration changes made in this step.</div>';
1061
- }
1062
-
1063
- let wrapperHtml = '<div style="border-bottom: 1px solid var(--border-color); padding: 0.5rem 0;">';
1064
- wrapperHtml += '<div style="font-size: 0.8rem; color: var(--text-muted);">' + new Date(item.timestamp).toLocaleTimeString() + ' - Device: ' + item.deviceId + '</div>';
1065
- wrapperHtml += diffHtml;
1066
- wrapperHtml += '</div>';
1067
- return wrapperHtml;
1068
- }).join('');
1069
-
1070
- container.innerHTML = diffsHtml;
1071
- }
1072
-
1073
- function updateAuditsUI(audits) {
1074
- const currentJson = JSON.stringify(audits);
1075
- if (currentJson === lastAuditsJson) {
1076
- return;
1077
- }
1078
- lastAuditsJson = currentJson;
1079
- const container = document.getElementById('audit-compare-container');
1080
-
1081
- if (!audits || audits.length === 0) {
1082
- container.innerHTML = '<div style="color: var(--text-muted);">No comparison snapshot captured. Trigger commands to evaluate audits.</div>';
1083
- return;
1084
- }
1085
-
1086
- let html = '';
1087
- audits.forEach(audit => {
1088
- const pre = audit.pre;
1089
- const post = audit.post;
1090
-
1091
- html += '<div style="margin-bottom: 2rem; border-bottom: 1px solid var(--border-color); padding-bottom: 1.5rem;">';
1092
- html += '<div style="font-size: 0.9rem; color: var(--text-muted); margin-bottom: 0.75rem;">Device Target: <strong>' + audit.deviceId + '</strong> (' + new Date(audit.timestamp).toLocaleTimeString() + ')</div>';
1093
- html += '<div class="audit-visualizer">';
1094
-
1095
- // Pre-flight Card
1096
- html += '<div class="audit-card">';
1097
- html += '<h4>Pre-Flight Inspection</h4>';
1098
- html += '<div class="audit-metric"><span>Gateway Reachability</span>' + (pre.pingReachability ? '<span style="color: var(--success);">● Reachable</span>' : '<span style="color: var(--danger);">● Unreachable</span>') + '</div>';
1099
- html += '<div class="audit-metric"><span>Down Interfaces</span><span>' + pre.downInterfacesCount + ' down</span></div>';
1100
- html += '<div class="audit-metric"><span>Dynamic Routes</span><span>' + pre.dynamicRoutesCount + ' routes</span></div>';
1101
- html += '<div class="audit-metric"><span>Routing Adjacencies</span><span>' + pre.routingAdjacenciesCount + ' peers</span></div>';
1102
- html += '</div>';
1103
-
1104
- // Post-flight Card
1105
- html += '<div class="audit-card">';
1106
- html += '<h4>Post-Flight Inspection</h4>';
1107
- html += '<div class="audit-metric"><span>Gateway Reachability</span>' + (post.pingReachability ? '<span style="color: var(--success);">● Reachable</span>' : '<span style="color: var(--danger);">● Unreachable</span>') + '</div>';
1108
- html += '<div class="audit-metric"><span>Down Interfaces</span><span>' + post.downInterfacesCount + ' down</span></div>';
1109
- html += '<div class="audit-metric"><span>Dynamic Routes</span><span>' + post.dynamicRoutesCount + ' routes</span></div>';
1110
- html += '<div class="audit-metric"><span>Routing Adjacencies</span><span>' + post.routingAdjacenciesCount + ' peers</span></div>';
1111
- html += '</div>';
1112
-
1113
- html += '</div>'; // End audit-visualizer
1114
-
1115
- // Status message
1116
- if (pre.pingReachability && !post.pingReachability) {
1117
- html += '<div style="color: var(--danger); margin-top: 1rem; font-weight: 600; font-size: 0.9rem;">[!] WARNING: Network gateway reachability was LOST during this configuration window!</div>';
1118
- } else if (!pre.pingReachability && post.pingReachability) {
1119
- html += '<div style="color: var(--success); margin-top: 1rem; font-weight: 600; font-size: 0.9rem;">[+] SUCCESS: Network gateway reachability was RESTORED during this configuration window!</div>';
1120
- } else {
1121
- html += '<div style="color: var(--success); margin-top: 1rem; font-weight: 600; font-size: 0.9rem;">[+] Audit check: Network gateway reachability is stable.</div>';
1122
- }
1123
-
1124
- html += '</div>'; // End outer wrapper
1125
- });
1126
-
1127
- container.innerHTML = html;
1128
- }
1129
-
1130
- async function triggerRollback() {
1131
- if (confirm('Are you sure you want to perform an Emergency Configuration Rollback on all devices?')) {
1132
- try {
1133
- const res = await fetch('/api/rollback', { method: 'POST' });
1134
- const result = await res.json();
1135
- alert(result.message || 'Rollback triggered.');
1136
- reloadData();
1137
- } catch (e) {
1138
- alert('Rollback failed: ' + e.message);
1139
- }
1140
- }
1141
- }
1142
-
1143
- setInterval(reloadData, 2500);
1144
- reloadData();
1145
- </script>
1146
- </body>
114
+ return `<!DOCTYPE html>
115
+ <html lang="en">
116
+ <head>
117
+ <meta charset="UTF-8">
118
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
119
+ <title>CiscoLLM Control Center</title>
120
+ <!-- Google Fonts -->
121
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
122
+ <!-- Vis Network CDN -->
123
+ <script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
124
+ <style>
125
+ :root {
126
+ --bg-color: #0B0F19;
127
+ --bg-deep: #070A12;
128
+ --card-bg: rgba(17, 24, 39, 0.7);
129
+ --border-color: rgba(255, 255, 255, 0.08);
130
+ --border-glow: rgba(99, 102, 241, 0.2);
131
+ --text-main: #F3F4F6;
132
+ --text-muted: #9CA3AF;
133
+ --accent: #6366F1;
134
+ --accent-glow: rgba(99, 102, 241, 0.35);
135
+ --success: #10B981;
136
+ --warning: #F59E0B;
137
+ --danger: #EF4444;
138
+ --glass-blur: blur(12px);
139
+ }
140
+
141
+ * {
142
+ box-sizing: border-box;
143
+ margin: 0;
144
+ padding: 0;
145
+ }
146
+
147
+ body {
148
+ font-family: 'Outfit', sans-serif;
149
+ background-color: var(--bg-color);
150
+ background-image: radial-gradient(circle at 10% 20%, rgba(99, 102, 241, 0.05) 0%, transparent 40%),
151
+ radial-gradient(circle at 90% 80%, rgba(16, 185, 129, 0.03) 0%, transparent 40%);
152
+ color: var(--text-main);
153
+ min-height: 100vh;
154
+ display: flex;
155
+ flex-direction: column;
156
+ overflow-x: hidden;
157
+ }
158
+
159
+ header {
160
+ background: rgba(11, 15, 25, 0.8);
161
+ backdrop-filter: var(--glass-blur);
162
+ border-bottom: 1px solid var(--border-color);
163
+ padding: 1rem 1.5rem;
164
+ display: flex;
165
+ flex-wrap: wrap;
166
+ justify-content: space-between;
167
+ align-items: center;
168
+ position: sticky;
169
+ top: 0;
170
+ z-index: 100;
171
+ gap: 1rem;
172
+ }
173
+
174
+ .brand-container {
175
+ display: flex;
176
+ align-items: center;
177
+ gap: 0.75rem;
178
+ }
179
+
180
+ .brand-logo {
181
+ width: 10px;
182
+ height: 10px;
183
+ background-color: var(--success);
184
+ border-radius: 50%;
185
+ box-shadow: 0 0 12px var(--success);
186
+ animation: pulse-active 2s infinite;
187
+ }
188
+
189
+ header h1 {
190
+ font-size: 1.35rem;
191
+ font-weight: 700;
192
+ letter-spacing: -0.5px;
193
+ background: linear-gradient(135deg, #FFF 60%, var(--accent) 100%);
194
+ -webkit-background-clip: text;
195
+ -webkit-text-fill-color: transparent;
196
+ }
197
+
198
+ header h1 span {
199
+ font-weight: 400;
200
+ font-size: 0.9rem;
201
+ color: var(--text-muted);
202
+ margin-left: 0.5rem;
203
+ }
204
+
205
+ .actions {
206
+ display: flex;
207
+ gap: 0.75rem;
208
+ align-items: center;
209
+ }
210
+
211
+ button {
212
+ padding: 0.6rem 1.2rem;
213
+ border-radius: 0.5rem;
214
+ border: 1px solid transparent;
215
+ font-weight: 600;
216
+ font-size: 0.9rem;
217
+ cursor: pointer;
218
+ transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
219
+ font-family: inherit;
220
+ display: flex;
221
+ align-items: center;
222
+ gap: 0.5rem;
223
+ }
224
+
225
+ .btn-primary {
226
+ background-color: var(--accent);
227
+ color: white;
228
+ box-shadow: 0 4px 14px var(--accent-glow);
229
+ }
230
+
231
+ .btn-primary:hover {
232
+ background-color: #4f46e5;
233
+ transform: translateY(-1px);
234
+ }
235
+
236
+ .btn-danger {
237
+ background: rgba(239, 68, 68, 0.1);
238
+ color: var(--danger);
239
+ border: 1px solid rgba(239, 68, 68, 0.3);
240
+ }
241
+
242
+ .btn-danger:hover {
243
+ background: var(--danger);
244
+ color: white;
245
+ box-shadow: 0 4px 14px rgba(239, 68, 68, 0.4);
246
+ transform: translateY(-1px);
247
+ }
248
+
249
+ /* Responsive Metric Cards Grid */
250
+ .metrics-grid {
251
+ display: grid;
252
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
253
+ gap: 1rem;
254
+ padding: 1.5rem 1.5rem 0 1.5rem;
255
+ }
256
+
257
+ .metric-card {
258
+ background: var(--card-bg);
259
+ backdrop-filter: var(--glass-blur);
260
+ border: 1px solid var(--border-color);
261
+ border-radius: 0.75rem;
262
+ padding: 1.25rem;
263
+ display: flex;
264
+ align-items: center;
265
+ justify-content: space-between;
266
+ transition: transform 0.2s ease, border-color 0.2s ease;
267
+ }
268
+
269
+ .metric-card:hover {
270
+ border-color: var(--border-glow);
271
+ transform: translateY(-2px);
272
+ }
273
+
274
+ .metric-info h3 {
275
+ font-size: 0.8rem;
276
+ color: var(--text-muted);
277
+ text-transform: uppercase;
278
+ letter-spacing: 0.5px;
279
+ margin-bottom: 0.25rem;
280
+ }
281
+
282
+ .metric-info .value {
283
+ font-size: 1.75rem;
284
+ font-weight: 700;
285
+ }
286
+
287
+ .metric-icon {
288
+ font-size: 1.5rem;
289
+ opacity: 0.8;
290
+ color: var(--accent);
291
+ }
292
+
293
+ /* Tabs Navigation */
294
+ .tab-bar {
295
+ display: flex;
296
+ padding: 0 1.5rem;
297
+ margin-top: 1.5rem;
298
+ border-bottom: 1px solid var(--border-color);
299
+ gap: 1.5rem;
300
+ overflow-x: auto;
301
+ white-space: nowrap;
302
+ }
303
+
304
+ .tab-button {
305
+ background: none;
306
+ border: none;
307
+ color: var(--text-muted);
308
+ padding: 0.75rem 0.25rem;
309
+ font-weight: 500;
310
+ font-size: 0.95rem;
311
+ border-bottom: 2px solid transparent;
312
+ border-radius: 0;
313
+ transition: all 0.2s ease;
314
+ }
315
+
316
+ .tab-button:hover {
317
+ color: var(--text-main);
318
+ }
319
+
320
+ .tab-button.active {
321
+ color: var(--accent);
322
+ border-bottom-color: var(--accent);
323
+ font-weight: 600;
324
+ }
325
+
326
+ /* Tab Contents Layout */
327
+ .tab-content {
328
+ display: none;
329
+ flex: 1;
330
+ padding: 1.5rem;
331
+ animation: fadeIn 0.3s ease-in-out;
332
+ }
333
+
334
+ .tab-content.active {
335
+ display: flex;
336
+ flex-direction: column;
337
+ }
338
+
339
+ /* Panel Design */
340
+ .panel-container {
341
+ display: grid;
342
+ grid-template-columns: 1.4fr 1fr;
343
+ gap: 1.5rem;
344
+ flex: 1;
345
+ }
346
+
347
+ .panel {
348
+ background: var(--card-bg);
349
+ backdrop-filter: var(--glass-blur);
350
+ border: 1px solid var(--border-color);
351
+ border-radius: 0.75rem;
352
+ display: flex;
353
+ flex-direction: column;
354
+ overflow: hidden;
355
+ min-height: 450px;
356
+ }
357
+
358
+ .panel-header {
359
+ padding: 1rem 1.25rem;
360
+ border-bottom: 1px solid var(--border-color);
361
+ font-weight: 600;
362
+ display: flex;
363
+ justify-content: space-between;
364
+ align-items: center;
365
+ font-size: 1rem;
366
+ }
367
+
368
+ .panel-content {
369
+ flex: 1;
370
+ overflow-y: auto;
371
+ padding: 1.25rem;
372
+ }
373
+
374
+ /* Topology Canvas style */
375
+ #topology-canvas {
376
+ position: absolute;
377
+ top: 0;
378
+ left: 0;
379
+ width: 100%;
380
+ height: 100%;
381
+ background-color: var(--bg-deep);
382
+ }
383
+
384
+ /* Active Sessions cards styling */
385
+ .session-grid {
386
+ display: grid;
387
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
388
+ gap: 1rem;
389
+ }
390
+
391
+ .session-card {
392
+ background: rgba(30, 41, 59, 0.4);
393
+ border: 1px solid var(--border-color);
394
+ border-radius: 0.5rem;
395
+ padding: 1rem;
396
+ display: flex;
397
+ flex-direction: column;
398
+ gap: 0.5rem;
399
+ transition: border-color 0.2s ease;
400
+ }
401
+
402
+ .session-card:hover {
403
+ border-color: var(--accent);
404
+ }
405
+
406
+ .session-title {
407
+ display: flex;
408
+ justify-content: space-between;
409
+ align-items: center;
410
+ font-weight: 600;
411
+ }
412
+
413
+ .session-badge {
414
+ font-size: 0.75rem;
415
+ padding: 0.15rem 0.4rem;
416
+ border-radius: 0.25rem;
417
+ font-weight: 600;
418
+ background: rgba(99, 102, 241, 0.15);
419
+ color: #a5b4fc;
420
+ }
421
+
422
+ .session-field {
423
+ font-size: 0.85rem;
424
+ color: var(--text-muted);
425
+ display: flex;
426
+ justify-content: space-between;
427
+ }
428
+
429
+ .session-field span:last-child {
430
+ color: var(--text-main);
431
+ font-family: 'JetBrains Mono', monospace;
432
+ }
433
+
434
+ /* Log console styling */
435
+ .console-controls {
436
+ display: flex;
437
+ flex-wrap: wrap;
438
+ gap: 0.75rem;
439
+ margin-bottom: 1rem;
440
+ align-items: center;
441
+ }
442
+
443
+ .search-input {
444
+ background: rgba(15, 23, 42, 0.6);
445
+ border: 1px solid var(--border-color);
446
+ border-radius: 0.375rem;
447
+ padding: 0.5rem 0.75rem;
448
+ color: var(--text-main);
449
+ font-family: inherit;
450
+ font-size: 0.85rem;
451
+ flex: 1;
452
+ min-width: 200px;
453
+ }
454
+
455
+ .search-input:focus {
456
+ outline: none;
457
+ border-color: var(--accent);
458
+ }
459
+
460
+ .filter-group {
461
+ display: flex;
462
+ gap: 0.35rem;
463
+ overflow-x: auto;
464
+ }
465
+
466
+ .filter-btn {
467
+ background: rgba(30, 41, 59, 0.5);
468
+ border: 1px solid var(--border-color);
469
+ color: var(--text-muted);
470
+ padding: 0.35rem 0.75rem;
471
+ font-size: 0.8rem;
472
+ border-radius: 0.25rem;
473
+ font-weight: 500;
474
+ }
475
+
476
+ .filter-btn.active {
477
+ background: var(--accent);
478
+ color: white;
479
+ border-color: var(--accent);
480
+ }
481
+
482
+ .log-list {
483
+ display: flex;
484
+ flex-direction: column;
485
+ gap: 0.75rem;
486
+ }
487
+
488
+ .log-item {
489
+ background: rgba(15, 23, 42, 0.4);
490
+ border: 1px solid var(--border-color);
491
+ border-radius: 0.5rem;
492
+ padding: 0.85rem 1rem;
493
+ font-family: 'JetBrains Mono', monospace;
494
+ font-size: 0.85rem;
495
+ animation: slideIn 0.2s ease-out;
496
+ }
497
+
498
+ .log-header {
499
+ display: flex;
500
+ justify-content: space-between;
501
+ margin-bottom: 0.35rem;
502
+ flex-wrap: wrap;
503
+ gap: 0.5rem;
504
+ }
505
+
506
+ .status-badge {
507
+ display: inline-block;
508
+ padding: 0.15rem 0.4rem;
509
+ border-radius: 0.25rem;
510
+ font-size: 0.725rem;
511
+ font-weight: 700;
512
+ text-transform: uppercase;
513
+ }
514
+
515
+ .status-success { background: rgba(16, 185, 129, 0.15); color: var(--success); }
516
+ .status-rollback { background: rgba(245, 158, 11, 0.15); color: var(--warning); }
517
+ .status-blocked { background: rgba(239, 68, 68, 0.15); color: var(--danger); }
518
+
519
+ .log-thought {
520
+ color: var(--text-muted);
521
+ font-style: italic;
522
+ margin-top: 0.35rem;
523
+ font-family: 'Outfit', sans-serif;
524
+ font-size: 0.8rem;
525
+ border-left: 2px solid var(--border-glow);
526
+ padding-left: 0.5rem;
527
+ }
528
+
529
+ /* Diff styling */
530
+ .diff-added { color: var(--success); font-family: 'JetBrains Mono', monospace; }
531
+ .diff-removed { color: var(--danger); font-family: 'JetBrains Mono', monospace; }
532
+ .diff-modified { color: var(--warning); font-family: 'JetBrains Mono', monospace; }
533
+
534
+ /* Comparative Audit Styling */
535
+ .audit-visualizer {
536
+ display: grid;
537
+ grid-template-columns: 1fr 1fr;
538
+ gap: 1.5rem;
539
+ }
540
+
541
+ .audit-card {
542
+ background: rgba(30, 41, 59, 0.3);
543
+ border: 1px solid var(--border-color);
544
+ border-radius: 0.5rem;
545
+ padding: 1rem;
546
+ }
547
+
548
+ .audit-card h4 {
549
+ font-size: 0.9rem;
550
+ text-transform: uppercase;
551
+ letter-spacing: 0.5px;
552
+ color: var(--text-muted);
553
+ margin-bottom: 0.75rem;
554
+ border-bottom: 1px solid var(--border-color);
555
+ padding-bottom: 0.35rem;
556
+ }
557
+
558
+ .audit-metric {
559
+ display: flex;
560
+ justify-content: space-between;
561
+ padding: 0.5rem 0;
562
+ border-bottom: 1px dashed rgba(255,255,255,0.03);
563
+ font-size: 0.9rem;
564
+ }
565
+
566
+ .audit-metric:last-child {
567
+ border-bottom: none;
568
+ }
569
+
570
+ /* Connection Status Overlay */
571
+ .disconnect-overlay {
572
+ position: fixed;
573
+ top: 0;
574
+ left: 0;
575
+ width: 100vw;
576
+ height: 100vh;
577
+ background: rgba(7, 10, 18, 0.9);
578
+ backdrop-filter: blur(8px);
579
+ z-index: 1000;
580
+ display: flex;
581
+ flex-direction: column;
582
+ justify-content: center;
583
+ align-items: center;
584
+ opacity: 0;
585
+ pointer-events: none;
586
+ transition: opacity 0.4s ease;
587
+ }
588
+
589
+ .disconnect-overlay.visible {
590
+ opacity: 1;
591
+ pointer-events: auto;
592
+ }
593
+
594
+ .disconnect-box {
595
+ background: rgba(17, 24, 39, 0.85);
596
+ border: 1px solid var(--danger);
597
+ border-radius: 1rem;
598
+ padding: 2.5rem;
599
+ text-align: center;
600
+ box-shadow: 0 0 30px rgba(239, 68, 68, 0.15);
601
+ max-width: 450px;
602
+ width: 90%;
603
+ animation: slideIn 0.3s ease-out;
604
+ }
605
+
606
+ .disconnect-title {
607
+ font-size: 1.5rem;
608
+ font-weight: 700;
609
+ color: var(--danger);
610
+ margin-bottom: 1rem;
611
+ }
612
+
613
+ .disconnect-text {
614
+ color: var(--text-muted);
615
+ font-size: 0.95rem;
616
+ line-height: 1.5;
617
+ margin-bottom: 1.5rem;
618
+ }
619
+
620
+ .spinner {
621
+ width: 40px;
622
+ height: 40px;
623
+ border: 4px solid rgba(255, 255, 255, 0.1);
624
+ border-top: 4px solid var(--danger);
625
+ border-radius: 50%;
626
+ animation: spin 1s linear infinite;
627
+ margin: 0 auto 1.5rem auto;
628
+ }
629
+
630
+ @keyframes spin {
631
+ 0% { transform: rotate(0deg); }
632
+ 100% { transform: rotate(360deg); }
633
+ }
634
+
635
+ /* Responsive Layout Overrides */
636
+ @media (max-width: 1024px) {
637
+ .panel-container {
638
+ grid-template-columns: 1fr;
639
+ }
640
+ .audit-visualizer {
641
+ grid-template-columns: 1fr;
642
+ }
643
+ }
644
+
645
+ @media (max-width: 640px) {
646
+ header {
647
+ padding: 0.75rem 1rem;
648
+ }
649
+ header h1 {
650
+ font-size: 1.15rem;
651
+ }
652
+ .metrics-grid {
653
+ grid-template-columns: 1fr;
654
+ padding: 1rem 1rem 0 1rem;
655
+ }
656
+ .tab-bar {
657
+ padding: 0 1rem;
658
+ margin-top: 1rem;
659
+ }
660
+ .tab-content {
661
+ padding: 1rem;
662
+ }
663
+ .console-controls {
664
+ flex-direction: column;
665
+ align-items: stretch;
666
+ }
667
+ }
668
+
669
+ /* Keyframes */
670
+ @keyframes pulse-active {
671
+ 0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); }
672
+ 70% { box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); }
673
+ 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
674
+ }
675
+
676
+ @keyframes fadeIn {
677
+ from { opacity: 0; transform: translateY(3px); }
678
+ to { opacity: 1; transform: translateY(0); }
679
+ }
680
+
681
+ @keyframes slideIn {
682
+ from { transform: translateY(5px); opacity: 0; }
683
+ to { transform: translateY(0); opacity: 1; }
684
+ }
685
+ </style>
686
+ </head>
687
+ <body>
688
+ <header>
689
+ <div class="brand-container">
690
+ <div class="brand-logo"></div>
691
+ <h1>CiscoLLM <span>Visual Control Dashboard v1.1.0</span></h1>
692
+ </div>
693
+ <div class="actions">
694
+ <button class="btn-primary" onclick="reloadData()">Refresh</button>
695
+ <button class="btn-danger" onclick="triggerRollback()">Emergency Rollback</button>
696
+ </div>
697
+ </header>
698
+
699
+ <!-- Disconnect Overlay -->
700
+ <div id="disconnect-overlay" class="disconnect-overlay">
701
+ <div class="disconnect-box">
702
+ <div class="spinner"></div>
703
+ <div class="disconnect-title">Connection Lost</div>
704
+ <div class="disconnect-text">
705
+ Disconnected from the CiscoLLM Swarm. Ensure your local CLI execution is active and the API server is running.
706
+ </div>
707
+ </div>
708
+ </div>
709
+
710
+ <!-- Top Metrics Overview -->
711
+ <div class="metrics-grid">
712
+ <div class="metric-card">
713
+ <div class="metric-info">
714
+ <h3>Devices Swarm</h3>
715
+ <div class="value" id="count-devices">0</div>
716
+ </div>
717
+ <div class="metric-icon">💻</div>
718
+ </div>
719
+ <div class="metric-card">
720
+ <div class="metric-info">
721
+ <h3>Topology Links</h3>
722
+ <div class="value" id="count-links">0</div>
723
+ </div>
724
+ <div class="metric-icon">🔗</div>
725
+ </div>
726
+ <div class="metric-card">
727
+ <div class="metric-info">
728
+ <h3>Action Logs</h3>
729
+ <div class="value" id="count-logs">0</div>
730
+ </div>
731
+ <div class="metric-icon">📜</div>
732
+ </div>
733
+ <div class="metric-card">
734
+ <div class="metric-info">
735
+ <h3>Safety Audits</h3>
736
+ <div class="value" style="color: var(--success);">Passed</div>
737
+ </div>
738
+ <div class="metric-icon">🛡️</div>
739
+ </div>
740
+ </div>
741
+
742
+ <!-- Tab Bar -->
743
+ <div class="tab-bar">
744
+ <button class="tab-button active" onclick="switchTab('topology-tab')">Network Topology</button>
745
+ <button class="tab-button" onclick="switchTab('sessions-tab')">Device Sessions</button>
746
+ <button class="tab-button" onclick="switchTab('logs-tab')">Agent Action Logs</button>
747
+ <button class="tab-button" onclick="switchTab('diffs-tab')">Config Diffs</button>
748
+ </div>
749
+
750
+ <!-- Tab Contents -->
751
+ <!-- 1. Topology -->
752
+ <div id="topology-tab" class="tab-content active">
753
+ <div class="panel-container">
754
+ <div class="panel">
755
+ <div class="panel-header">Interactive Swarm Topology Map</div>
756
+ <div style="flex: 1; overflow: hidden; position: relative;">
757
+ <div id="topology-canvas"></div>
758
+ </div>
759
+ </div>
760
+ <div class="panel">
761
+ <div class="panel-header">Pre/Post Flight Audits Compare</div>
762
+ <div class="panel-content" id="audit-compare-container">
763
+ <div style="color: var(--text-muted);">No comparison snapshot captured. Trigger commands to evaluate audits.</div>
764
+ </div>
765
+ </div>
766
+ </div>
767
+ </div>
768
+
769
+ <!-- 2. Device Sessions -->
770
+ <div id="sessions-tab" class="tab-content">
771
+ <div class="session-grid" id="sessions-container">
772
+ <div style="color: var(--text-muted); grid-column: 1/-1;">No connected device sessions found.</div>
773
+ </div>
774
+ </div>
775
+
776
+ <!-- 3. Agent Logs -->
777
+ <div id="logs-tab" class="tab-content">
778
+ <div class="panel" style="min-height: 480px;">
779
+ <div class="panel-header">Real-time Swarm Command Firewalls & Logs</div>
780
+ <div class="panel-content">
781
+ <div class="console-controls">
782
+ <input type="text" id="log-search" class="search-input" placeholder="Search commands, thoughts, or error logs..." oninput="filterLogs()">
783
+ <div class="filter-group">
784
+ <button class="filter-btn active" id="filter-all" onclick="setLogFilter('ALL')">All</button>
785
+ <button class="filter-btn" id="filter-success" onclick="setLogFilter('SUCCESS')">Success</button>
786
+ <button class="filter-btn" id="filter-blocked" onclick="setLogFilter('BLOCKED')">Blocked</button>
787
+ <button class="filter-btn" id="filter-rollback" onclick="setLogFilter('ROLLBACK')">Rollback</button>
788
+ </div>
789
+ </div>
790
+ <div class="log-list" id="logs-container">
791
+ <div style="color: var(--text-muted);">No log entries recorded yet.</div>
792
+ </div>
793
+ </div>
794
+ </div>
795
+ </div>
796
+
797
+ <!-- 4. Diffs -->
798
+ <div id="diffs-tab" class="tab-content">
799
+ <div class="panel" style="min-height: 480px;">
800
+ <div class="panel-header">Dynamic Configuration Diffs (Before & After Snapshots)</div>
801
+ <div class="panel-content" id="diffs-container">
802
+ <div style="color: var(--text-muted);">No configuration diffs captured yet. Run changes to see differences.</div>
803
+ </div>
804
+ </div>
805
+ </div>
806
+
807
+ <script>
808
+ let network = null;
809
+ let activeTab = 'topology-tab';
810
+ let rawLogs = [];
811
+ let logFilter = 'ALL';
812
+ let isConnected = true;
813
+
814
+ let lastTopologyJson = '';
815
+ let lastSessionsJson = '';
816
+ let lastLogsJson = '';
817
+ let lastDiffsJson = '';
818
+ let lastAuditsJson = '';
819
+
820
+ function switchTab(tabId) {
821
+ document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
822
+ document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
823
+
824
+ const selectedBtn = Array.from(document.querySelectorAll('.tab-button')).find(btn => btn.getAttribute('onclick').includes(tabId));
825
+ if (selectedBtn) selectedBtn.classList.add('active');
826
+
827
+ document.getElementById(tabId).classList.add('active');
828
+ activeTab = tabId;
829
+
830
+ if (tabId === 'topology-tab' && network) {
831
+ setTimeout(() => network.fit(), 200);
832
+ }
833
+ }
834
+
835
+ async function reloadData() {
836
+ try {
837
+ const res = await fetch('/api/state');
838
+ if (!res.ok) throw new Error("HTTP " + res.status);
839
+ const data = await res.json();
840
+
841
+ if (!isConnected) {
842
+ isConnected = true;
843
+ document.getElementById('disconnect-overlay').classList.remove('visible');
844
+ }
845
+
846
+ // Update UI sections
847
+ updateTopologyUI(data.topology);
848
+ updateSessionsUI(data.sessions);
849
+ updateLogsUI(data.logs);
850
+ updateDiffsUI(data.diffs);
851
+ updateAuditsUI(data.audits);
852
+
853
+ } catch (e) {
854
+ console.error("Connection failed: ", e);
855
+ if (isConnected) {
856
+ isConnected = false;
857
+ document.getElementById('disconnect-overlay').classList.add('visible');
858
+ }
859
+ }
860
+ }
861
+
862
+ function updateTopologyUI(data) {
863
+ const stableData = {
864
+ nodes: (data && data.nodes) ? data.nodes : [],
865
+ links: (data && data.links) ? data.links : []
866
+ };
867
+ const currentJson = JSON.stringify(stableData);
868
+ if (currentJson === lastTopologyJson) {
869
+ return;
870
+ }
871
+ lastTopologyJson = currentJson;
872
+
873
+ const container = document.getElementById('topology-canvas');
874
+ const nodes = [];
875
+ const edges = [];
876
+
877
+ if (data && data.nodes) {
878
+ document.getElementById('count-devices').innerText = data.nodes.length;
879
+ data.nodes.forEach(node => {
880
+ const isSwitch = node.toLowerCase().includes('switch');
881
+ nodes.push({
882
+ id: node,
883
+ label: node,
884
+ shape: 'box',
885
+ margin: 12,
886
+ color: {
887
+ background: isSwitch ? '#1E293B' : '#4F46E5',
888
+ border: '#6366F1'
889
+ },
890
+ font: { color: '#ffffff', size: 14, face: 'Outfit' }
891
+ });
892
+ });
893
+ }
894
+
895
+ if (data && data.links) {
896
+ document.getElementById('count-links').innerText = data.links.length;
897
+ data.links.forEach((link, idx) => {
898
+ edges.push({
899
+ id: 'e' + idx,
900
+ from: link.localDeviceId,
901
+ to: link.remoteDeviceId,
902
+ label: link.localInterface + ' ↔ ' + link.remoteInterface,
903
+ font: { color: '#9CA3AF', size: 10, strokeWidth: 0, face: 'Outfit' },
904
+ color: { color: '#4B5563' }
905
+ });
906
+ });
907
+ }
908
+
909
+ const visData = {
910
+ nodes: new vis.DataSet(nodes),
911
+ edges: new vis.DataSet(edges)
912
+ };
913
+
914
+ const options = {
915
+ physics: { enabled: true, solver: 'repulsion', repulsion: { nodeDistance: 150 } },
916
+ layout: { randomSeed: 42 },
917
+ interaction: { keyboard: false }
918
+ };
919
+
920
+ if (network) network.destroy();
921
+ network = new vis.Network(container, visData, options);
922
+ }
923
+
924
+ function updateSessionsUI(data) {
925
+ const currentJson = JSON.stringify(data);
926
+ if (currentJson === lastSessionsJson) {
927
+ return;
928
+ }
929
+ lastSessionsJson = currentJson;
930
+ const container = document.getElementById('sessions-container');
931
+
932
+ const keys = Object.keys(data);
933
+ if (keys.length === 0) {
934
+ container.innerHTML = '<div style="color: var(--text-muted); grid-column: 1/-1;">No connected device sessions found.</div>';
935
+ return;
936
+ }
937
+
938
+ let cards = '';
939
+ keys.forEach(id => {
940
+ const session = data[id];
941
+ cards += '<div class="session-card">';
942
+ cards += '<div class="session-title">';
943
+ cards += '<span>' + (session.hostname || id) + '</span>';
944
+ cards += '<span class="session-badge">' + session.currentMode + '</span>';
945
+ cards += '</div>';
946
+ cards += '<div class="session-field"><span>Target URI</span><span>' + id + '</span></div>';
947
+ cards += '<div class="session-field"><span>Prompt</span><span>' + session.prompt + '</span></div>';
948
+ cards += '<div class="session-field"><span>Status</span><span style="color: var(--success);">● Active</span></div>';
949
+ cards += '</div>';
950
+ });
951
+ container.innerHTML = cards;
952
+ }
953
+
954
+ function updateLogsUI(logs) {
955
+ rawLogs = logs;
956
+ const currentJson = JSON.stringify(rawLogs);
957
+ if (currentJson === lastLogsJson) {
958
+ return;
959
+ }
960
+ lastLogsJson = currentJson;
961
+
962
+ document.getElementById('count-logs').innerText = rawLogs.length;
963
+ filterLogs();
964
+ }
965
+
966
+ function setLogFilter(filter) {
967
+ logFilter = filter;
968
+ document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
969
+ const filterId = 'filter-' + filter.toLowerCase();
970
+ const btn = document.getElementById(filterId);
971
+ if (btn) btn.classList.add('active');
972
+ filterLogs();
973
+ }
974
+
975
+ function filterLogs() {
976
+ const container = document.getElementById('logs-container');
977
+ const searchVal = document.getElementById('log-search').value.toLowerCase().trim();
978
+
979
+ const filtered = rawLogs.filter(log => {
980
+ if (logFilter !== 'ALL' && log.status !== logFilter) return false;
981
+ if (searchVal) {
982
+ const matchCommand = log.command && log.command.toLowerCase().includes(searchVal);
983
+ const matchThought = log.thought && log.thought.toLowerCase().includes(searchVal);
984
+ const matchReason = log.reason && log.reason.toLowerCase().includes(searchVal);
985
+ const matchDevice = log.deviceId && log.deviceId.toLowerCase().includes(searchVal);
986
+ return matchCommand || matchThought || matchReason || matchDevice;
987
+ }
988
+ return true;
989
+ });
990
+
991
+ if (filtered.length === 0) {
992
+ container.innerHTML = '<div style="color: var(--text-muted);">No matching log entries found.</div>';
993
+ return;
994
+ }
995
+
996
+ container.innerHTML = filtered.map(log => {
997
+ const statusClass = 'status-' + log.status.toLowerCase();
998
+ let itemHtml = '<div class="log-item">';
999
+ itemHtml += '<div class="log-header">';
1000
+ itemHtml += '<span><strong>[' + log.deviceId + ']</strong> ' + log.command + '</span>';
1001
+ itemHtml += '<span class="status-badge ' + statusClass + '">' + log.status + '</span>';
1002
+ itemHtml += '</div>';
1003
+ if (log.thought) {
1004
+ itemHtml += '<div class="log-thought">AI: ' + log.thought + '</div>';
1005
+ }
1006
+ if (log.reason) {
1007
+ itemHtml += '<div style="color: var(--danger); margin-top: 0.35rem;">Rule Violation: ' + log.reason + '</div>';
1008
+ }
1009
+ itemHtml += '</div>';
1010
+ return itemHtml;
1011
+ }).join('');
1012
+ }
1013
+
1014
+ function updateDiffsUI(data) {
1015
+ const currentJson = JSON.stringify(data);
1016
+ if (currentJson === lastDiffsJson) {
1017
+ return;
1018
+ }
1019
+ lastDiffsJson = currentJson;
1020
+ const container = document.getElementById('diffs-container');
1021
+
1022
+ if (!data || data.length === 0) {
1023
+ container.innerHTML = '<div style="color: var(--text-muted);">No configuration diffs captured yet. Apply modifications via CLI.</div>';
1024
+ return;
1025
+ }
1026
+
1027
+ let diffsHtml = data.map(item => {
1028
+ let diffHtml = '';
1029
+ const diff = item.diff;
1030
+
1031
+ if (diff.hostnameChanged) {
1032
+ diffHtml += '<div class="diff-modified">Hostname Changed: "' + diff.hostnameChanged.before + '" ➔ "' + diff.hostnameChanged.after + '"</div>';
1033
+ }
1034
+ if (diff.modifiedInterfaces && diff.modifiedInterfaces.length > 0) {
1035
+ diff.modifiedInterfaces.forEach(inf => {
1036
+ diffHtml += '<div class="diff-modified">Interface ' + inf.name + ' changes:</div>';
1037
+ inf.changes.forEach(c => {
1038
+ diffHtml += '<div style="padding-left: 1rem;">- ' + c.field + ': "' + c.before + '" ➔ "' + c.after + '"</div>';
1039
+ });
1040
+ });
1041
+ }
1042
+ if (diff.addedRoutes && diff.addedRoutes.length > 0) {
1043
+ diff.addedRoutes.forEach(r => {
1044
+ diffHtml += '<div class="diff-added">+ ip route ' + r.network + ' ' + r.mask + ' ' + (r.nextHop || '') + '</div>';
1045
+ });
1046
+ }
1047
+ if (diff.removedRoutes && diff.removedRoutes.length > 0) {
1048
+ diff.removedRoutes.forEach(r => {
1049
+ diffHtml += '<div class="diff-removed">- ip route ' + r.network + ' ' + r.mask + ' ' + (r.nextHop || '') + '</div>';
1050
+ });
1051
+ }
1052
+ if (diff.addedVlans && diff.addedVlans.length > 0) {
1053
+ diffHtml += '<div class="diff-added">+ VLANs Added: ' + diff.addedVlans.join(', ') + '</div>';
1054
+ }
1055
+ if (diff.removedVlans && diff.removedVlans.length > 0) {
1056
+ diffHtml += '<div class="diff-removed">- VLANs Removed: ' + diff.removedVlans.join(', ') + '</div>';
1057
+ }
1058
+
1059
+ if (!diffHtml) {
1060
+ diffHtml = '<div style="color: var(--text-muted);">No configuration changes made in this step.</div>';
1061
+ }
1062
+
1063
+ let wrapperHtml = '<div style="border-bottom: 1px solid var(--border-color); padding: 0.5rem 0;">';
1064
+ wrapperHtml += '<div style="font-size: 0.8rem; color: var(--text-muted);">' + new Date(item.timestamp).toLocaleTimeString() + ' - Device: ' + item.deviceId + '</div>';
1065
+ wrapperHtml += diffHtml;
1066
+ wrapperHtml += '</div>';
1067
+ return wrapperHtml;
1068
+ }).join('');
1069
+
1070
+ container.innerHTML = diffsHtml;
1071
+ }
1072
+
1073
+ function updateAuditsUI(audits) {
1074
+ const currentJson = JSON.stringify(audits);
1075
+ if (currentJson === lastAuditsJson) {
1076
+ return;
1077
+ }
1078
+ lastAuditsJson = currentJson;
1079
+ const container = document.getElementById('audit-compare-container');
1080
+
1081
+ if (!audits || audits.length === 0) {
1082
+ container.innerHTML = '<div style="color: var(--text-muted);">No comparison snapshot captured. Trigger commands to evaluate audits.</div>';
1083
+ return;
1084
+ }
1085
+
1086
+ let html = '';
1087
+ audits.forEach(audit => {
1088
+ const pre = audit.pre;
1089
+ const post = audit.post;
1090
+
1091
+ html += '<div style="margin-bottom: 2rem; border-bottom: 1px solid var(--border-color); padding-bottom: 1.5rem;">';
1092
+ html += '<div style="font-size: 0.9rem; color: var(--text-muted); margin-bottom: 0.75rem;">Device Target: <strong>' + audit.deviceId + '</strong> (' + new Date(audit.timestamp).toLocaleTimeString() + ')</div>';
1093
+ html += '<div class="audit-visualizer">';
1094
+
1095
+ // Pre-flight Card
1096
+ html += '<div class="audit-card">';
1097
+ html += '<h4>Pre-Flight Inspection</h4>';
1098
+ html += '<div class="audit-metric"><span>Gateway Reachability</span>' + (pre.pingReachability ? '<span style="color: var(--success);">● Reachable</span>' : '<span style="color: var(--danger);">● Unreachable</span>') + '</div>';
1099
+ html += '<div class="audit-metric"><span>Down Interfaces</span><span>' + pre.downInterfacesCount + ' down</span></div>';
1100
+ html += '<div class="audit-metric"><span>Dynamic Routes</span><span>' + pre.dynamicRoutesCount + ' routes</span></div>';
1101
+ html += '<div class="audit-metric"><span>Routing Adjacencies</span><span>' + pre.routingAdjacenciesCount + ' peers</span></div>';
1102
+ html += '</div>';
1103
+
1104
+ // Post-flight Card
1105
+ html += '<div class="audit-card">';
1106
+ html += '<h4>Post-Flight Inspection</h4>';
1107
+ html += '<div class="audit-metric"><span>Gateway Reachability</span>' + (post.pingReachability ? '<span style="color: var(--success);">● Reachable</span>' : '<span style="color: var(--danger);">● Unreachable</span>') + '</div>';
1108
+ html += '<div class="audit-metric"><span>Down Interfaces</span><span>' + post.downInterfacesCount + ' down</span></div>';
1109
+ html += '<div class="audit-metric"><span>Dynamic Routes</span><span>' + post.dynamicRoutesCount + ' routes</span></div>';
1110
+ html += '<div class="audit-metric"><span>Routing Adjacencies</span><span>' + post.routingAdjacenciesCount + ' peers</span></div>';
1111
+ html += '</div>';
1112
+
1113
+ html += '</div>'; // End audit-visualizer
1114
+
1115
+ // Status message
1116
+ if (pre.pingReachability && !post.pingReachability) {
1117
+ html += '<div style="color: var(--danger); margin-top: 1rem; font-weight: 600; font-size: 0.9rem;">[!] WARNING: Network gateway reachability was LOST during this configuration window!</div>';
1118
+ } else if (!pre.pingReachability && post.pingReachability) {
1119
+ html += '<div style="color: var(--success); margin-top: 1rem; font-weight: 600; font-size: 0.9rem;">[+] SUCCESS: Network gateway reachability was RESTORED during this configuration window!</div>';
1120
+ } else {
1121
+ html += '<div style="color: var(--success); margin-top: 1rem; font-weight: 600; font-size: 0.9rem;">[+] Audit check: Network gateway reachability is stable.</div>';
1122
+ }
1123
+
1124
+ html += '</div>'; // End outer wrapper
1125
+ });
1126
+
1127
+ container.innerHTML = html;
1128
+ }
1129
+
1130
+ async function triggerRollback() {
1131
+ if (confirm('Are you sure you want to perform an Emergency Configuration Rollback on all devices?')) {
1132
+ try {
1133
+ const res = await fetch('/api/rollback', { method: 'POST' });
1134
+ const result = await res.json();
1135
+ alert(result.message || 'Rollback triggered.');
1136
+ reloadData();
1137
+ } catch (e) {
1138
+ alert('Rollback failed: ' + e.message);
1139
+ }
1140
+ }
1141
+ }
1142
+
1143
+ setInterval(reloadData, 2500);
1144
+ reloadData();
1145
+ </script>
1146
+ </body>
1147
1147
  </html>`;
1148
1148
  }
1149
1149
  //# sourceMappingURL=dashboard.js.map