ciscollm-cli 1.2.0 → 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.
- package/dist/core/agent/AgentLoop.js +1 -0
- package/dist/core/agent/AgentLoop.js.map +1 -1
- package/dist/core/agent/MultiAgentCoordinator.d.ts +3 -0
- package/dist/core/agent/MultiAgentCoordinator.js +12 -0
- package/dist/core/agent/MultiAgentCoordinator.js.map +1 -1
- package/dist/core/guardrails/CommandFirewall.js +13 -4
- package/dist/core/guardrails/CommandFirewall.js.map +1 -1
- package/dist/server/dashboard.js +324 -224
- package/dist/server/dashboard.js.map +1 -1
- package/dist/server/shell-simulator.d.ts +27 -1
- package/dist/server/shell-simulator.js +329 -1
- package/dist/server/shell-simulator.js.map +1 -1
- package/dist/shared/constants.js +1 -1
- package/dist/shared/constants.js.map +1 -1
- package/package.json +1 -1
package/dist/server/dashboard.js
CHANGED
|
@@ -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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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({
|
|
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/
|
|
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 (
|
|
778
|
-
|
|
779
|
-
|
|
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
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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("
|
|
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
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|
-
|
|
893
|
+
}
|
|
842
894
|
|
|
843
|
-
|
|
844
|
-
|
|
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
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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 (!
|
|
927
|
-
|
|
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
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
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
|
-
|
|
980
|
-
console.error("Failed to load diffs", e);
|
|
981
|
-
}
|
|
1070
|
+
container.innerHTML = diffsHtml;
|
|
982
1071
|
}
|
|
983
1072
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
-
|
|
1078
|
+
lastAuditsJson = currentJson;
|
|
1079
|
+
const container = document.getElementById('audit-compare-container');
|
|
1018
1080
|
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
|