156156 display : block;
157157 }
158158
159- .tabs {
160- display : flex;
161- border-bottom : 2px solid # ecf0f1 ;
162- margin-bottom : 20px ;
163- }
164-
165- .tab {
166- padding : 12px 24px ;
167- cursor : pointer;
168- border : none;
169- background : none;
170- color : # 7f8c8d ;
171- font-size : 16px ;
172- transition : color 0.3s ;
173- margin : 0 ;
174- border-radius : 0 ;
175- }
176-
177- .tab : hover {
178- color : # 3498db ;
179- }
180-
181- .tab .active {
182- color : # 3498db ;
183- border-bottom : 2px solid # 3498db ;
184- margin-bottom : -2px ;
185- }
186-
187- .tab-content {
188- display : none;
189- }
190-
191- .tab-content .active {
192- display : block;
193- }
194-
195159 .markdown-rendered {
196160 background : # ffffff ;
197161 color : # 2c3e50 ;
227191 overflow-x : auto;
228192 }
229193
194+ .report-actions {
195+ display : flex;
196+ gap : 10px ;
197+ margin-bottom : 14px ;
198+ }
199+
200+ .report-download-btn {
201+ margin-top : 0 ;
202+ padding : 10px 16px ;
203+ font-size : 14px ;
204+ background : # 16a085 ;
205+ }
206+
207+ .report-download-btn : hover : not (: disabled ) {
208+ background : # 138d75 ;
209+ }
210+
230211 pre {
231212 background : # 2c3e50 ;
232213 color : # ecf0f1 ;
@@ -275,7 +256,7 @@ <h1>Sensor Metrics Analyzer</h1>
275256 Privacy: Uploaded files are analyzed and not retained. Temporary files are deleted immediately after processing, and metric data is not stored.
276257 </ div >
277258 < div class ="disclaimer-note ">
278- Disclaimer: This project is AI-generated and only a small fraction of the code and metric rules were verified by a human. Analysis results may be inaccurate and, in extreme cases, totally wrong.
259+ Disclaimer: The code of this project is AI-generated and only a fraction of the code and metric rules were verified by a human. Analysis results may be inaccurate and - in extreme cases - totally wrong.
279260 </ div >
280261
281262 < div class ="upload-section " id ="uploadSection ">
@@ -293,17 +274,11 @@ <h1>Sensor Metrics Analyzer</h1>
293274 </ div >
294275
295276 < div class ="results " id ="results ">
296- < div class ="tabs ">
297- < button class ="tab " data-tab ="console "> Console Output</ button >
298- < button class ="tab active " data-tab ="markdown "> Markdown Report</ button >
299- </ div >
300- < div class ="tab-content " id ="consoleTab ">
301- < pre id ="consoleOutput "> </ pre >
302- </ div >
303- < div class ="tab-content active " id ="markdownTab ">
304- < div id ="markdownRendered " class ="markdown-rendered "> </ div >
305- < pre id ="markdownOutput " style ="margin-top: 16px; "> </ pre >
277+ < div class ="report-actions ">
278+ < button id ="downloadMdBtn " class ="report-download-btn " disabled > Download Markdown</ button >
279+ < button id ="downloadHtmlBtn " class ="report-download-btn " disabled > Download HTML</ button >
306280 </ div >
281+ < div id ="markdownRendered " class ="markdown-rendered "> </ div >
307282 </ div >
308283 </ div >
309284
@@ -316,14 +291,15 @@ <h1>Sensor Metrics Analyzer</h1>
316291 const errorDiv = document . getElementById ( 'error' ) ;
317292 const loadingDiv = document . getElementById ( 'loading' ) ;
318293 const resultsDiv = document . getElementById ( 'results' ) ;
319- const consoleOutput = document . getElementById ( 'consoleOutput' ) ;
320- const markdownOutput = document . getElementById ( 'markdownOutput' ) ;
321294 const markdownRendered = document . getElementById ( 'markdownRendered' ) ;
322- const tabs = document . querySelectorAll ( '.tab ') ;
323- const tabContents = document . querySelectorAll ( '.tab-content ') ;
295+ const downloadMdBtn = document . getElementById ( 'downloadMdBtn ') ;
296+ const downloadHtmlBtn = document . getElementById ( 'downloadHtmlBtn ') ;
324297 const versionValue = document . getElementById ( 'versionValue' ) ;
325298 const lastUpdateValue = document . getElementById ( 'lastUpdateValue' ) ;
326299 const releaseLink = document . getElementById ( 'releaseLink' ) ;
300+ let currentMarkdownReport = '' ;
301+ let currentRenderedReportHtml = '' ;
302+ let currentReportBaseName = 'sensor-metrics-report' ;
327303
328304 const releasesUrl = 'https://github.com/stackrox/sensor-metrics-analyzer/releases' ;
329305 releaseLink . href = releasesUrl ;
@@ -406,6 +382,10 @@ <h1>Sensor Metrics Analyzer</h1>
406382 resultsDiv . classList . remove ( 'show' ) ;
407383 loadingDiv . style . display = 'block' ;
408384 analyzeBtn . disabled = true ;
385+ setDownloadButtonsEnabled ( false ) ;
386+ currentMarkdownReport = '' ;
387+ currentRenderedReportHtml = '' ;
388+ currentReportBaseName = 'sensor-metrics-report' ;
409389
410390 const formData = new FormData ( ) ;
411391 formData . append ( 'file' , file ) ;
@@ -421,28 +401,20 @@ <h1>Sensor Metrics Analyzer</h1>
421401 loadingDiv . style . display = 'none' ;
422402 analyzeBtn . disabled = false ;
423403
424- if ( data . error && ! data . console && ! data . markdown ) {
404+ if ( data . error && ! data . markdown ) {
425405 showError ( data . error ) ;
426406 return ;
427407 }
428408
429- // Show results
430- if ( data . console ) {
431- consoleOutput . textContent = data . console ;
432- } else {
433- consoleOutput . textContent = 'No console output available' ;
434- }
435-
436409 if ( data . markdown ) {
437- if ( window . marked && typeof window . marked . parse === 'function' ) {
438- markdownRendered . innerHTML = window . marked . parse ( data . markdown ) ;
439- } else {
440- markdownRendered . textContent = data . markdown ;
441- }
442- markdownOutput . textContent = data . markdown ;
410+ currentMarkdownReport = data . markdown ;
411+ currentRenderedReportHtml = renderMarkdownToHtml ( data . markdown ) ;
412+ markdownRendered . innerHTML = currentRenderedReportHtml ;
413+ currentReportBaseName = buildReportBaseName ( data . clusterName , data . analysisTimestamp ) ;
414+ setDownloadButtonsEnabled ( true ) ;
443415 } else {
444416 markdownRendered . textContent = 'No markdown output available' ;
445- markdownOutput . textContent = 'No markdown output available' ;
417+ setDownloadButtonsEnabled ( false ) ;
446418 }
447419
448420 if ( data . error ) {
@@ -457,29 +429,140 @@ <h1>Sensor Metrics Analyzer</h1>
457429 }
458430 } ) ;
459431
460- // Tab switching
461- tabs . forEach ( tab => {
462- tab . addEventListener ( 'click' , ( ) => {
463- const targetTab = tab . dataset . tab ;
464-
465- // Update active tab
466- tabs . forEach ( t => t . classList . remove ( 'active' ) ) ;
467- tab . classList . add ( 'active' ) ;
432+ downloadMdBtn . addEventListener ( 'click' , ( ) => {
433+ if ( ! currentMarkdownReport ) {
434+ return ;
435+ }
436+ downloadTextFile ( currentMarkdownReport , `${ currentReportBaseName } .md` , 'text/markdown;charset=utf-8' ) ;
437+ } ) ;
468438
469- // Update active content
470- tabContents . forEach ( content => {
471- content . classList . remove ( 'active' ) ;
472- if ( content . id === targetTab + 'Tab' ) {
473- content . classList . add ( 'active' ) ;
474- }
475- } ) ;
476- } ) ;
439+ downloadHtmlBtn . addEventListener ( 'click' , ( ) => {
440+ if ( ! currentRenderedReportHtml ) {
441+ return ;
442+ }
443+ const htmlDocument = buildHtmlReportDocument ( currentRenderedReportHtml , currentReportBaseName ) ;
444+ downloadTextFile ( htmlDocument , `${ currentReportBaseName } .html` , 'text/html;charset=utf-8' ) ;
477445 } ) ;
478446
479447 function showError ( message ) {
480448 errorDiv . textContent = message ;
481449 errorDiv . style . display = 'block' ;
482450 }
451+
452+ function setDownloadButtonsEnabled ( isEnabled ) {
453+ downloadMdBtn . disabled = ! isEnabled ;
454+ downloadHtmlBtn . disabled = ! isEnabled ;
455+ }
456+
457+ function renderMarkdownToHtml ( markdown ) {
458+ if ( window . marked && typeof window . marked . parse === 'function' ) {
459+ return window . marked . parse ( markdown ) ;
460+ }
461+ return `<pre>${ escapeHtml ( markdown ) } </pre>` ;
462+ }
463+
464+ function buildReportBaseName ( clusterName , analysisTimestamp ) {
465+ const sanitizedCluster = sanitizeFilePart ( clusterName || 'cluster' ) ;
466+ const formattedDate = formatDateForFilename ( analysisTimestamp ) ;
467+ return `sensor-metrics-${ sanitizedCluster } -${ formattedDate } ` ;
468+ }
469+
470+ function formatDateForFilename ( rawTimestamp ) {
471+ const date = rawTimestamp ? new Date ( rawTimestamp ) : new Date ( ) ;
472+ if ( Number . isNaN ( date . getTime ( ) ) ) {
473+ return 'unknown-date' ;
474+ }
475+ const year = date . getUTCFullYear ( ) ;
476+ const month = String ( date . getUTCMonth ( ) + 1 ) . padStart ( 2 , '0' ) ;
477+ const day = String ( date . getUTCDate ( ) ) . padStart ( 2 , '0' ) ;
478+ const hours = String ( date . getUTCHours ( ) ) . padStart ( 2 , '0' ) ;
479+ const minutes = String ( date . getUTCMinutes ( ) ) . padStart ( 2 , '0' ) ;
480+ const seconds = String ( date . getUTCSeconds ( ) ) . padStart ( 2 , '0' ) ;
481+ return `${ year } ${ month } ${ day } -${ hours } ${ minutes } ${ seconds } ` ;
482+ }
483+
484+ function sanitizeFilePart ( value ) {
485+ return String ( value )
486+ . toLowerCase ( )
487+ . replace ( / [ ^ a - z 0 - 9 - ] + / g, '-' )
488+ . replace ( / ^ - + | - + $ / g, '' )
489+ . slice ( 0 , 64 ) || 'cluster' ;
490+ }
491+
492+ function downloadTextFile ( content , filename , mimeType ) {
493+ const blob = new Blob ( [ content ] , { type : mimeType } ) ;
494+ const url = URL . createObjectURL ( blob ) ;
495+ const link = document . createElement ( 'a' ) ;
496+ link . href = url ;
497+ link . download = filename ;
498+ document . body . appendChild ( link ) ;
499+ link . click ( ) ;
500+ document . body . removeChild ( link ) ;
501+ URL . revokeObjectURL ( url ) ;
502+ }
503+
504+ function buildHtmlReportDocument ( renderedHtml , reportBaseName ) {
505+ const escapedTitle = escapeHtml ( reportBaseName ) ;
506+ return `<!DOCTYPE html>
507+ <html lang="en">
508+ <head>
509+ <meta charset="UTF-8">
510+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
511+ <title>${ escapedTitle } </title>
512+ <style>
513+ body {
514+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
515+ background: #f7f9fb;
516+ color: #2c3e50;
517+ margin: 0;
518+ padding: 24px;
519+ }
520+ .report {
521+ max-width: 1000px;
522+ margin: 0 auto;
523+ background: #fff;
524+ border: 1px solid #e4ebf0;
525+ border-radius: 8px;
526+ padding: 24px;
527+ }
528+ .report h1, .report h2, .report h3, .report h4 {
529+ margin: 16px 0 8px;
530+ }
531+ .report p {
532+ margin: 8px 0;
533+ }
534+ .report code {
535+ background: #f2f4f6;
536+ padding: 2px 4px;
537+ border-radius: 3px;
538+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
539+ font-size: 13px;
540+ }
541+ .report pre {
542+ background: #2c3e50;
543+ color: #ecf0f1;
544+ padding: 16px;
545+ border-radius: 4px;
546+ overflow-x: auto;
547+ }
548+ </style>
549+ </head>
550+ <body>
551+ <article class="report">
552+ ${ renderedHtml }
553+ </article>
554+ </body>
555+ </html>` ;
556+ }
557+
558+ function escapeHtml ( text ) {
559+ return String ( text )
560+ . replace ( / & / g, '&' )
561+ . replace ( / < / g, '<' )
562+ . replace ( / > / g, '>' )
563+ . replace ( / " / g, '"' )
564+ . replace ( / ' / g, ''' ) ;
565+ }
483566 </ script >
484567</ body >
485568</ html >
0 commit comments