Skip to content

Commit e9be99f

Browse files
Add the latest version of the webpage
1 parent 0144279 commit e9be99f

File tree

1 file changed

+205
-4
lines changed

1 file changed

+205
-4
lines changed

mcp_nexus_web/index.html

Lines changed: 205 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,61 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66
<title>Crash Dump Analyzer</title>
77
<link rel="stylesheet" href="styles.css?v=20250925193500">
8+
<style>
9+
@keyframes spin {
10+
from { transform: rotate(0deg); }
11+
to { transform: rotate(360deg); }
12+
}
13+
.spinner {
14+
display: inline-block;
15+
width: 12px;
16+
height: 12px;
17+
border: 2px solid #30363d;
18+
border-top: 2px solid #ffffff;
19+
border-radius: 50%;
20+
animation: spin 1s linear infinite;
21+
margin-right: 6px;
22+
}
23+
.status-completed { color: #3fb950; }
24+
.status-processing { color: #f85149; }
25+
.status-processing-white { color: #ffffff; }
26+
.status-error { color: #f85149; }
27+
.status-pending { color: #ffa657; }
28+
.connection-status {
29+
display: flex;
30+
align-items: center;
31+
margin-right: 12px;
32+
font-size: 12px;
33+
color: #7d8590;
34+
}
35+
.status-dot {
36+
width: 8px;
37+
height: 8px;
38+
border-radius: 50%;
39+
margin-right: 6px;
40+
background: #3fb950;
41+
animation: pulse 2s infinite;
42+
}
43+
.status-dot.disconnected {
44+
background: #f85149;
45+
animation: none;
46+
}
47+
@keyframes pulse {
48+
0% { opacity: 1; }
49+
50% { opacity: 0.5; }
50+
100% { opacity: 1; }
51+
}
52+
</style>
853
</head>
954
<body>
1055
<div class="topbar">
1156
<div class="topbar-inner">
1257
<a class="brand" href="#">Crash Dump Analyzer</a>
1358
<div class="menu">
59+
<div class="connection-status" id="connectionStatus" title="Real-time updates">
60+
<span class="status-dot"></span>
61+
<span class="status-text">Live</span>
62+
</div>
1463
<button class="upload-btn-header" id="uploadBtn">📁 Upload Dumps</button>
1564
<a href="admin.html" class="upload-btn-header" style="text-decoration: none;">⚙️ Admin</a>
1665
</div>
@@ -67,6 +116,7 @@ <h3 class="modal-title">Upload Crash Dump Files</h3>
67116
let jobs = [];
68117
let selectedJob = null;
69118
let pollInterval = null;
119+
let isPolling = false;
70120

71121
// Modal functionality
72122
const uploadModal = document.getElementById('uploadModal');
@@ -160,6 +210,10 @@ <h3 class="modal-title">Upload Crash Dump Files</h3>
160210

161211
if (result.success) {
162212
jobs = result.jobs || [];
213+
214+
// Clean up old tab preferences for jobs that no longer exist
215+
cleanupOldTabPreferences();
216+
163217
renderJobs();
164218
} else {
165219
console.error('Failed to load jobs:', result.error);
@@ -169,6 +223,28 @@ <h3 class="modal-title">Upload Crash Dump Files</h3>
169223
}
170224
}
171225

226+
function cleanupOldTabPreferences() {
227+
// Get all localStorage keys that start with 'selectedTab_'
228+
const keysToRemove = [];
229+
for (let i = 0; i < localStorage.length; i++) {
230+
const key = localStorage.key(i);
231+
if (key && key.startsWith('selectedTab_')) {
232+
const jobId = key.replace('selectedTab_', '');
233+
// Check if this job still exists
234+
const jobExists = jobs.some(job => job.id === jobId);
235+
if (!jobExists) {
236+
keysToRemove.push(key);
237+
}
238+
}
239+
}
240+
241+
// Remove old preferences
242+
keysToRemove.forEach(key => {
243+
localStorage.removeItem(key);
244+
console.log('Cleaned up old tab preference:', key);
245+
});
246+
}
247+
172248
function renderJobs() {
173249
const jobsList = document.getElementById('jobsList');
174250

@@ -213,7 +289,7 @@ <h3 class="modal-title">Upload Crash Dump Files</h3>
213289
jobStatus.className = 'job-status-detailed';
214290

215291
if (job.status === 'processing') {
216-
jobStatus.innerHTML = '<span class="spinner"></span> PROCESSING';
292+
jobStatus.innerHTML = '<span class="spinner"></span> <span class="status-processing-white">PROCESSING</span>';
217293
} else if (job.status === 'completed') {
218294
// Show individual file status indicators
219295
const indicators = [
@@ -250,6 +326,10 @@ <h3 class="modal-title">Upload Crash Dump Files</h3>
250326
// Save selection to localStorage
251327
localStorage.setItem('selectedJobId', job.id);
252328

329+
// Also save the selected tab for this job
330+
const savedTabType = localStorage.getItem(`selectedTab_${job.id}`) || 'analysis';
331+
localStorage.setItem('currentSelectedTab', savedTabType);
332+
253333
renderJobs(); // Re-render to show selection
254334

255335
const viewerTitle = document.getElementById('viewerTitle');
@@ -341,8 +421,23 @@ <h2>📊 Job Details</h2>
341421

342422
if (analysis) {
343423
console.log('Analysis files:', analysis.files);
344-
// Load the analysis report by default
345-
loadAnalysisContent(analysis, 'analysis');
424+
425+
// Restore the previously selected tab for this job, or default to 'analysis'
426+
const savedTabType = localStorage.getItem(`selectedTab_${jobName}`) ||
427+
localStorage.getItem('currentSelectedTab') ||
428+
'analysis';
429+
430+
console.log('Restoring tab:', savedTabType, 'for job:', jobName);
431+
console.log('Available tabs for this analysis:', Object.keys(analysis.files || {}));
432+
433+
// Make sure the tab exists and is available
434+
const tabExists = document.querySelector(`[data-type="${savedTabType}"]`);
435+
const tabType = (tabExists && (savedTabType === 'jobinfo' || (analysis.files && analysis.files[savedTabType])))
436+
? savedTabType
437+
: 'analysis';
438+
439+
// Load the saved or default tab
440+
loadAnalysisContent(analysis, tabType);
346441
updateTabStates(analysis);
347442
} else {
348443
console.log('No analysis found for job:', jobName);
@@ -388,6 +483,12 @@ <h2>📊 Job Details</h2>
388483
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
389484
document.querySelector('[data-type="jobinfo"]').classList.add('active');
390485

486+
// Save the selected tab for the current job
487+
if (selectedJob) {
488+
localStorage.setItem(`selectedTab_${selectedJob.id}`, 'jobinfo');
489+
localStorage.setItem('currentSelectedTab', 'jobinfo');
490+
}
491+
391492
const job = selectedJob;
392493
const createdDate = new Date(job.created);
393494
const updatedDate = new Date(job.updated);
@@ -522,6 +623,12 @@ <h3>📊 Raw Job Data</h3>
522623
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
523624
document.querySelector(`[data-type="${type}"]`).classList.add('active');
524625

626+
// Save the selected tab for the current job
627+
if (selectedJob) {
628+
localStorage.setItem(`selectedTab_${selectedJob.id}`, type);
629+
localStorage.setItem('currentSelectedTab', type);
630+
}
631+
525632
if (type === 'analysis') {
526633
// Render markdown
527634
// Function to generate heading IDs
@@ -648,11 +755,100 @@ <h3>Dump File Information</h3>
648755
});
649756
});
650757

758+
// Real-time polling system
759+
function startPolling() {
760+
if (isPolling) return; // Prevent multiple polling intervals
761+
762+
isPolling = true;
763+
console.log('Starting real-time polling...');
764+
765+
// Initial load
766+
loadJobs();
767+
768+
// Set up polling interval
769+
pollInterval = setInterval(async () => {
770+
try {
771+
await loadJobsWithStatusTracking();
772+
} catch (error) {
773+
console.error('Polling error:', error);
774+
}
775+
}, 3000); // Poll every 3 seconds
776+
}
777+
778+
function stopPolling() {
779+
if (pollInterval) {
780+
clearInterval(pollInterval);
781+
pollInterval = null;
782+
isPolling = false;
783+
console.log('Stopped real-time polling');
784+
}
785+
}
786+
787+
async function loadJobsWithStatusTracking() {
788+
try {
789+
const response = await fetch('queue-status.aspx');
790+
const result = await response.json();
791+
792+
// Update connection status
793+
updateConnectionStatus(true);
794+
795+
if (result.success) {
796+
const newJobs = result.jobs || [];
797+
798+
// Status changes are now handled by automatic polling updates
799+
800+
jobs = newJobs;
801+
renderJobs();
802+
803+
// Update selected job if it changed
804+
if (selectedJob) {
805+
const updatedSelectedJob = jobs.find(j => j.id === selectedJob.id);
806+
if (updatedSelectedJob &&
807+
(updatedSelectedJob.status !== selectedJob.status ||
808+
updatedSelectedJob.updated !== selectedJob.updated)) {
809+
810+
console.log('Selected job updated:', updatedSelectedJob);
811+
selectedJob = updatedSelectedJob;
812+
813+
// Refresh the viewer if job completed
814+
if (updatedSelectedJob.status === 'completed') {
815+
selectJob(updatedSelectedJob);
816+
}
817+
}
818+
}
819+
} else {
820+
console.error('Failed to load jobs:', result.error);
821+
updateConnectionStatus(false);
822+
}
823+
} catch (error) {
824+
console.error('Load jobs error:', error);
825+
updateConnectionStatus(false);
826+
}
827+
}
828+
829+
function updateConnectionStatus(connected) {
830+
const statusDot = document.querySelector('.status-dot');
831+
const statusText = document.querySelector('.status-text');
832+
833+
if (statusDot && statusText) {
834+
if (connected) {
835+
statusDot.classList.remove('disconnected');
836+
statusText.textContent = 'Live';
837+
statusText.style.color = '#7d8590';
838+
} else {
839+
statusDot.classList.add('disconnected');
840+
statusText.textContent = 'Offline';
841+
statusText.style.color = '#f85149';
842+
}
843+
}
844+
}
845+
846+
651847
// Initialize
652848
loadJobs();
653849
startPolling();
654850

655-
// Stop polling when page is hidden
851+
// Stop polling when page is hidden, resume when visible
656852
document.addEventListener('visibilitychange', () => {
657853
if (document.hidden) {
658854
stopPolling();
@@ -661,6 +857,11 @@ <h3>Dump File Information</h3>
661857
}
662858
});
663859

860+
// Stop polling when page is about to unload
861+
window.addEventListener('beforeunload', () => {
862+
stopPolling();
863+
});
864+
664865
// Add smooth scrolling for anchor links in markdown content
665866
document.addEventListener('click', (e) => {
666867
if (e.target.tagName === 'A' && e.target.hash) {

0 commit comments

Comments
 (0)