ciscollm-cli 1.1.4 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +137 -17
  3. package/dist/core/agent/AgentLoop.js +1 -0
  4. package/dist/core/agent/AgentLoop.js.map +1 -1
  5. package/dist/core/agent/AutoHealer.d.ts +27 -0
  6. package/dist/core/agent/AutoHealer.js +387 -0
  7. package/dist/core/agent/AutoHealer.js.map +1 -0
  8. package/dist/core/agent/HierarchicalAgentManager.js +5 -5
  9. package/dist/core/agent/MultiAgentCoordinator.d.ts +6 -1
  10. package/dist/core/agent/MultiAgentCoordinator.js +23 -1
  11. package/dist/core/agent/MultiAgentCoordinator.js.map +1 -1
  12. package/dist/core/agent/PromptEngine.js +67 -67
  13. package/dist/core/guardrails/AuditLogger.js +4 -4
  14. package/dist/core/guardrails/CommandFirewall.d.ts +2 -0
  15. package/dist/core/guardrails/CommandFirewall.js +112 -12
  16. package/dist/core/guardrails/CommandFirewall.js.map +1 -1
  17. package/dist/core/rollback/TransactionManager.js +17 -0
  18. package/dist/core/rollback/TransactionManager.js.map +1 -1
  19. package/dist/index.js +142 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/infrastructure/llm/LLMClient.js +0 -1
  22. package/dist/infrastructure/llm/LLMClient.js.map +1 -1
  23. package/dist/infrastructure/protocols/BaseSession.d.ts +2 -1
  24. package/dist/infrastructure/protocols/BaseSession.js +2 -1
  25. package/dist/infrastructure/protocols/BaseSession.js.map +1 -1
  26. package/dist/infrastructure/protocols/PlinkSerial.d.ts +1 -0
  27. package/dist/infrastructure/protocols/PlinkSerial.js +67 -12
  28. package/dist/infrastructure/protocols/PlinkSerial.js.map +1 -1
  29. package/dist/server/dashboard.js +324 -224
  30. package/dist/server/dashboard.js.map +1 -1
  31. package/dist/server/index.js +8 -8
  32. package/dist/server/shell-simulator.d.ts +29 -1
  33. package/dist/server/shell-simulator.js +394 -59
  34. package/dist/server/shell-simulator.js.map +1 -1
  35. package/dist/server/ssh.js +31 -20
  36. package/dist/server/ssh.js.map +1 -1
  37. package/dist/server/telnet.js +11 -0
  38. package/dist/server/telnet.js.map +1 -1
  39. package/dist/shared/constants.js +1 -1
  40. package/dist/shared/constants.js.map +1 -1
  41. package/package.json +54 -52
  42. package/dist/infrastructure/llm/LocalLLMClient.d.ts +0 -7
  43. package/dist/infrastructure/llm/LocalLLMClient.js +0 -39
  44. package/dist/infrastructure/llm/LocalLLMClient.js.map +0 -1
@@ -16,7 +16,7 @@ function startDashboardServer(coordinator, port) {
16
16
  }
17
17
  catch { }
18
18
  }
19
- const server = http_1.default.createServer((req, res) => {
19
+ const server = http_1.default.createServer(async (req, res) => {
20
20
  const url = req.url || '';
21
21
  const method = req.method || 'GET';
22
22
  res.setHeader('Access-Control-Allow-Origin', '*');
@@ -32,6 +32,17 @@ function startDashboardServer(coordinator, port) {
32
32
  res.end(getHtmlContent(port));
33
33
  return;
34
34
  }
35
+ if (method === 'GET' && url === '/api/state') {
36
+ res.writeHead(200, { 'Content-Type': 'application/json' });
37
+ res.end(JSON.stringify({
38
+ topology: coordinator.getTopology(),
39
+ sessions: coordinator.getAllStates(),
40
+ logs: AuditLogger_1.AuditLogger.getEntries(),
41
+ diffs: StateDiff_1.StateDiff.getDiffHistory(),
42
+ audits: coordinator.getAuditHistory ? coordinator.getAuditHistory() : []
43
+ }));
44
+ return;
45
+ }
35
46
  if (method === 'GET' && url === '/api/topology') {
36
47
  res.writeHead(200, { 'Content-Type': 'application/json' });
37
48
  res.end(JSON.stringify(coordinator.getTopology()));
@@ -52,18 +63,33 @@ function startDashboardServer(coordinator, port) {
52
63
  res.end(JSON.stringify(StateDiff_1.StateDiff.getDiffHistory()));
53
64
  return;
54
65
  }
66
+ if (method === 'GET' && url === '/api/audits') {
67
+ res.writeHead(200, { 'Content-Type': 'application/json' });
68
+ res.end(JSON.stringify(coordinator.getAuditHistory ? coordinator.getAuditHistory() : []));
69
+ return;
70
+ }
55
71
  if (method === 'POST' && url === '/api/rollback') {
56
72
  const sessions = coordinator.getSessions();
73
+ const promises = Array.from(sessions.entries()).map(async ([id, session]) => {
74
+ await session.execute('configure replace flash:backup-agent.cfg force');
75
+ });
57
76
  let count = 0;
58
- for (const [id, session] of sessions.entries()) {
59
- try {
60
- session.execute('configure replace flash:backup-agent.cfg force');
77
+ let errorMessage = '';
78
+ const results = await Promise.allSettled(promises);
79
+ results.forEach((res, idx) => {
80
+ if (res.status === 'fulfilled') {
61
81
  count++;
62
82
  }
63
- catch { }
64
- }
83
+ else {
84
+ const devId = Array.from(sessions.keys())[idx];
85
+ errorMessage += `${devId}: ${res.reason?.message || res.reason}; `;
86
+ }
87
+ });
65
88
  res.writeHead(200, { 'Content-Type': 'application/json' });
66
- res.end(JSON.stringify({ status: 'success', message: `Triggered configuration replace on ${count} devices.` }));
89
+ res.end(JSON.stringify({
90
+ status: count > 0 ? 'success' : 'failed',
91
+ message: `Rollback completed on ${count} of ${sessions.size} devices.${errorMessage ? ` Errors: ${errorMessage}` : ''}`
92
+ }));
67
93
  return;
68
94
  }
69
95
  res.writeHead(404, { 'Content-Type': 'application/json' });
@@ -541,6 +567,71 @@ function getHtmlContent(port) {
541
567
  border-bottom: none;
542
568
  }
543
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
+
544
635
  /* Responsive Layout Overrides */
545
636
  @media (max-width: 1024px) {
546
637
  .panel-container {
@@ -605,6 +696,17 @@ function getHtmlContent(port) {
605
696
  </div>
606
697
  </header>
607
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
+
608
710
  <!-- Top Metrics Overview -->
609
711
  <div class="metrics-grid">
610
712
  <div class="metric-card">
@@ -707,11 +809,13 @@ function getHtmlContent(port) {
707
809
  let activeTab = 'topology-tab';
708
810
  let rawLogs = [];
709
811
  let logFilter = 'ALL';
812
+ let isConnected = true;
710
813
 
711
814
  let lastTopologyJson = '';
712
815
  let lastSessionsJson = '';
713
816
  let lastLogsJson = '';
714
817
  let lastDiffsJson = '';
818
+ let lastAuditsJson = '';
715
819
 
716
820
  function switchTab(tabId) {
717
821
  document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
@@ -729,138 +833,134 @@ function getHtmlContent(port) {
729
833
  }
730
834
 
731
835
  async function reloadData() {
732
- await Promise.all([
733
- loadTopology(),
734
- loadSessions(),
735
- loadLogs(),
736
- loadDiffs()
737
- ]);
738
- }
739
-
740
- async function loadTopology() {
741
836
  try {
742
- const res = await fetch('/api/topology');
837
+ const res = await fetch('/api/state');
838
+ if (!res.ok) throw new Error("HTTP " + res.status);
743
839
  const data = await res.json();
744
-
745
- const stableData = {
746
- nodes: (data && data.nodes) ? data.nodes : [],
747
- links: (data && data.links) ? data.links : []
748
- };
749
- const currentJson = JSON.stringify(stableData);
750
- if (currentJson === lastTopologyJson) {
751
- return;
752
- }
753
- lastTopologyJson = currentJson;
754
-
755
- const container = document.getElementById('topology-canvas');
756
- const nodes = [];
757
- const edges = [];
758
-
759
- if (data && data.nodes) {
760
- document.getElementById('count-devices').innerText = data.nodes.length;
761
- data.nodes.forEach(node => {
762
- const isSwitch = node.toLowerCase().includes('switch');
763
- nodes.push({
764
- id: node,
765
- label: node,
766
- shape: 'box',
767
- margin: 12,
768
- color: {
769
- background: isSwitch ? '#1E293B' : '#4F46E5',
770
- border: '#6366F1'
771
- },
772
- font: { color: '#ffffff', size: 14, face: 'Outfit' }
773
- });
774
- });
775
- }
776
840
 
777
- if (data && data.links) {
778
- document.getElementById('count-links').innerText = data.links.length;
779
- data.links.forEach((link, idx) => {
780
- edges.push({
781
- id: 'e' + idx,
782
- from: link.localDeviceId,
783
- to: link.remoteDeviceId,
784
- label: link.localInterface + ' ↔ ' + link.remoteInterface,
785
- font: { color: '#9CA3AF', size: 10, strokeWidth: 0, face: 'Outfit' },
786
- color: { color: '#4B5563' }
787
- });
788
- });
841
+ if (!isConnected) {
842
+ isConnected = true;
843
+ document.getElementById('disconnect-overlay').classList.remove('visible');
789
844
  }
790
845
 
791
- const visData = {
792
- nodes: new vis.DataSet(nodes),
793
- edges: new vis.DataSet(edges)
794
- };
795
-
796
- const options = {
797
- physics: { enabled: true, solver: 'repulsion', repulsion: { nodeDistance: 150 } },
798
- layout: { randomSeed: 42 },
799
- interaction: { keyboard: false }
800
- };
801
-
802
- if (network) network.destroy();
803
- network = new vis.Network(container, visData, options);
846
+ // Update UI sections
847
+ updateTopologyUI(data.topology);
848
+ updateSessionsUI(data.sessions);
849
+ updateLogsUI(data.logs);
850
+ updateDiffsUI(data.diffs);
851
+ updateAuditsUI(data.audits);
804
852
 
805
853
  } catch (e) {
806
- console.error("Failed to load topology", e);
854
+ console.error("Connection failed: ", e);
855
+ if (isConnected) {
856
+ isConnected = false;
857
+ document.getElementById('disconnect-overlay').classList.add('visible');
858
+ }
807
859
  }
808
860
  }
809
861
 
810
- async function loadSessions() {
811
- try {
812
- const res = await fetch('/api/sessions');
813
- const data = await res.json();
814
-
815
- const currentJson = JSON.stringify(data);
816
- if (currentJson === lastSessionsJson) {
817
- return;
818
- }
819
- lastSessionsJson = currentJson;
820
- const container = document.getElementById('sessions-container');
821
-
822
- const keys = Object.keys(data);
823
- if (keys.length === 0) {
824
- container.innerHTML = '<div style="color: var(--text-muted); grid-column: 1/-1;">No connected device sessions found.</div>';
825
- return;
826
- }
827
-
828
- let cards = '';
829
- keys.forEach(id => {
830
- const session = data[id];
831
- cards += '<div class="session-card">';
832
- cards += '<div class="session-title">';
833
- cards += '<span>' + (session.hostname || id) + '</span>';
834
- cards += '<span class="session-badge">' + session.currentMode + '</span>';
835
- cards += '</div>';
836
- cards += '<div class="session-field"><span>Target URI</span><span>' + id + '</span></div>';
837
- cards += '<div class="session-field"><span>Prompt</span><span>' + session.prompt + '</span></div>';
838
- cards += '<div class="session-field"><span>Status</span><span style="color: var(--success);">● Active</span></div>';
839
- cards += '</div>';
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
+ });
840
892
  });
841
- container.innerHTML = cards;
893
+ }
842
894
 
843
- } catch (e) {
844
- console.error("Failed to load sessions", e);
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
+ });
845
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);
846
922
  }
847
923
 
848
- async function loadLogs() {
849
- try {
850
- const res = await fetch('/api/logs');
851
- rawLogs = await res.json();
852
-
853
- const currentJson = JSON.stringify(rawLogs);
854
- if (currentJson === lastLogsJson) {
855
- return;
856
- }
857
- lastLogsJson = currentJson;
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');
858
931
 
859
- document.getElementById('count-logs').innerText = rawLogs.length;
860
- filterLogs();
861
- } catch (e) {
862
- console.error("Failed to load logs", e);
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;
863
959
  }
960
+ lastLogsJson = currentJson;
961
+
962
+ document.getElementById('count-logs').innerText = rawLogs.length;
963
+ filterLogs();
864
964
  }
865
965
 
866
966
  function setLogFilter(filter) {
@@ -911,119 +1011,119 @@ function getHtmlContent(port) {
911
1011
  }).join('');
912
1012
  }
913
1013
 
914
- async function loadDiffs() {
915
- try {
916
- const res = await fetch('/api/diffs');
917
- const data = await res.json();
918
-
919
- const currentJson = JSON.stringify(data);
920
- if (currentJson === lastDiffsJson) {
921
- return;
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>';
922
1057
  }
923
- lastDiffsJson = currentJson;
924
- const container = document.getElementById('diffs-container');
925
1058
 
926
- if (!data || data.length === 0) {
927
- container.innerHTML = '<div style="color: var(--text-muted);">No configuration diffs captured yet. Apply modifications via CLI.</div>';
928
- return;
1059
+ if (!diffHtml) {
1060
+ diffHtml = '<div style="color: var(--text-muted);">No configuration changes made in this step.</div>';
929
1061
  }
930
1062
 
931
- let diffsHtml = data.map(item => {
932
- let diffHtml = '';
933
- const diff = item.diff;
934
-
935
- if (diff.hostnameChanged) {
936
- diffHtml += '<div class="diff-modified">Hostname Changed: "' + diff.hostnameChanged.before + '" ➔ "' + diff.hostnameChanged.after + '"</div>';
937
- }
938
- if (diff.modifiedInterfaces && diff.modifiedInterfaces.length > 0) {
939
- diff.modifiedInterfaces.forEach(inf => {
940
- diffHtml += '<div class="diff-modified">Interface ' + inf.name + ' changes:</div>';
941
- inf.changes.forEach(c => {
942
- diffHtml += '<div style="padding-left: 1rem;">- ' + c.field + ': "' + c.before + '" ➔ "' + c.after + '"</div>';
943
- });
944
- });
945
- }
946
- if (diff.addedRoutes && diff.addedRoutes.length > 0) {
947
- diff.addedRoutes.forEach(r => {
948
- diffHtml += '<div class="diff-added">+ ip route ' + r.network + ' ' + r.mask + ' ' + (r.nextHop || '') + '</div>';
949
- });
950
- }
951
- if (diff.removedRoutes && diff.removedRoutes.length > 0) {
952
- diff.removedRoutes.forEach(r => {
953
- diffHtml += '<div class="diff-removed">- ip route ' + r.network + ' ' + r.mask + ' ' + (r.nextHop || '') + '</div>';
954
- });
955
- }
956
- if (diff.addedVlans && diff.addedVlans.length > 0) {
957
- diffHtml += '<div class="diff-added">+ VLANs Added: ' + diff.addedVlans.join(', ') + '</div>';
958
- }
959
- if (diff.removedVlans && diff.removedVlans.length > 0) {
960
- diffHtml += '<div class="diff-removed">- VLANs Removed: ' + diff.removedVlans.join(', ') + '</div>';
961
- }
962
-
963
- if (!diffHtml) {
964
- diffHtml = '<div style="color: var(--text-muted);">No configuration changes made in this step.</div>';
965
- }
966
-
967
- let wrapperHtml = '<div style="border-bottom: 1px solid var(--border-color); padding: 0.5rem 0;">';
968
- wrapperHtml += '<div style="font-size: 0.8rem; color: var(--text-muted);">' + new Date(item.timestamp).toLocaleTimeString() + ' - Device: ' + item.deviceId + '</div>';
969
- wrapperHtml += diffHtml;
970
- wrapperHtml += '</div>';
971
- return wrapperHtml;
972
- }).join('');
973
-
974
- container.innerHTML = diffsHtml;
975
-
976
- // Also populate visual audit compare using the same data (if pre/post flights are recorded)
977
- updateAuditCompare();
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('');
978
1069
 
979
- } catch (e) {
980
- console.error("Failed to load diffs", e);
981
- }
1070
+ container.innerHTML = diffsHtml;
982
1071
  }
983
1072
 
984
- // Generate a visual audit compare from dynamic history/mock details
985
- function updateAuditCompare() {
986
- const container = document.getElementById('audit-compare-container');
987
-
988
- // We can reconstruct pre/post flight states visually
989
- let html = '<div class="audit-visualizer">';
990
-
991
- // Pre-Flight Audits
992
- html += '<div class="audit-card">';
993
- html += '<h4>Pre-Flight Inspection</h4>';
994
- html += '<div class="audit-metric"><span>Gateway (192.168.1.254)</span><span style="color: var(--success);">● Reachable</span></div>';
995
- html += '<div class="audit-metric"><span>Down Interfaces</span><span>2 down</span></div>';
996
- html += '<div class="audit-metric"><span>Dynamic Routes (OSPF)</span><span>0 routes</span></div>';
997
- html += '<div class="audit-metric"><span>OSPF Neighbors</span><span>0 peers</span></div>';
998
- html += '</div>';
999
-
1000
- // Post-Flight Audits
1001
- html += '<div class="audit-card">';
1002
- html += '<h4>Post-Flight Inspection</h4>';
1003
-
1004
- // Deduce OSPF activation status from rawLogs
1005
- const isOspfConfigured = rawLogs.some(l => l.command && l.command.toLowerCase().includes('router ospf') && l.status === 'SUCCESS');
1006
-
1007
- html += '<div class="audit-metric"><span>Gateway (192.168.1.254)</span><span style="color: var(--success);">● Reachable</span></div>';
1008
- html += '<div class="audit-metric"><span>Down Interfaces</span><span>2 down</span></div>';
1009
-
1010
- if (isOspfConfigured) {
1011
- html += '<div class="audit-metric"><span>Dynamic Routes (OSPF)</span><span style="color: var(--success); font-weight: 600;">1 route</span></div>';
1012
- html += '<div class="audit-metric"><span>OSPF Neighbors</span><span style="color: var(--success); font-weight: 600;">1 peer</span></div>';
1013
- } else {
1014
- html += '<div class="audit-metric"><span>Dynamic Routes (OSPF)</span><span>0 routes</span></div>';
1015
- html += '<div class="audit-metric"><span>OSPF Neighbors</span><span>0 peers</span></div>';
1073
+ function updateAuditsUI(audits) {
1074
+ const currentJson = JSON.stringify(audits);
1075
+ if (currentJson === lastAuditsJson) {
1076
+ return;
1016
1077
  }
1017
- html += '</div>';
1078
+ lastAuditsJson = currentJson;
1079
+ const container = document.getElementById('audit-compare-container');
1018
1080
 
1019
- html += '</div>'; // End visualizer
1020
-
1021
- if (isOspfConfigured) {
1022
- html += '<div style="color: var(--success); margin-top: 1rem; font-weight: 600; font-size: 0.9rem;">[+] Network change window verification check is stable and OSPF neighbors are up.</div>';
1023
- } else {
1024
- html += '<div style="color: var(--warning); margin-top: 1rem; font-size: 0.95rem;">[!] Gateway reachability is stable. No new OSPF routing adjacencies have been activated yet.</div>';
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;
1025
1084
  }
1026
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
+
1027
1127
  container.innerHTML = html;
1028
1128
  }
1029
1129