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