diff --git a/web/server/main.go b/web/server/main.go index e6798bd..7f7ee73 100644 --- a/web/server/main.go +++ b/web/server/main.go @@ -32,9 +32,11 @@ type Config struct { } type AnalyzeResponse struct { - Markdown string `json:"markdown"` - Console string `json:"console"` - Error string `json:"error,omitempty"` + Markdown string `json:"markdown"` + Console string `json:"console"` + ClusterName string `json:"clusterName,omitempty"` + AnalysisTimestamp string `json:"analysisTimestamp,omitempty"` + Error string `json:"error,omitempty"` } type VersionResponse struct { @@ -183,6 +185,8 @@ func handleAnalyzeBoth(cfg *Config) http.HandlerFunc { if err != nil { response.Error = fmt.Sprintf("Analysis failed: %v", err) } else { + response.ClusterName = report.ClusterName + response.AnalysisTimestamp = report.Timestamp.Format(time.RFC3339) response.Console = reporter.GenerateConsole(report) markdown, mdErr := reporter.GenerateMarkdown(report, cfg.TemplatePath) if mdErr != nil { diff --git a/web/static/index.html b/web/static/index.html index 4516f07..53c6d1a 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -156,42 +156,6 @@ display: block; } - .tabs { - display: flex; - border-bottom: 2px solid #ecf0f1; - margin-bottom: 20px; - } - - .tab { - padding: 12px 24px; - cursor: pointer; - border: none; - background: none; - color: #7f8c8d; - font-size: 16px; - transition: color 0.3s; - margin: 0; - border-radius: 0; - } - - .tab:hover { - color: #3498db; - } - - .tab.active { - color: #3498db; - border-bottom: 2px solid #3498db; - margin-bottom: -2px; - } - - .tab-content { - display: none; - } - - .tab-content.active { - display: block; - } - .markdown-rendered { background: #ffffff; color: #2c3e50; @@ -227,6 +191,23 @@ overflow-x: auto; } + .report-actions { + display: flex; + gap: 10px; + margin-bottom: 14px; + } + + .report-download-btn { + margin-top: 0; + padding: 10px 16px; + font-size: 14px; + background: #16a085; + } + + .report-download-btn:hover:not(:disabled) { + background: #138d75; + } + pre { background: #2c3e50; color: #ecf0f1; @@ -275,7 +256,7 @@

Sensor Metrics Analyzer

Privacy: Uploaded files are analyzed and not retained. Temporary files are deleted immediately after processing, and metric data is not stored.
- 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. + 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.
@@ -293,17 +274,11 @@

Sensor Metrics Analyzer

-
- - -
-
-

-            
-
-
-

+            
+ +
+
@@ -316,14 +291,15 @@

Sensor Metrics Analyzer

const errorDiv = document.getElementById('error'); const loadingDiv = document.getElementById('loading'); const resultsDiv = document.getElementById('results'); - const consoleOutput = document.getElementById('consoleOutput'); - const markdownOutput = document.getElementById('markdownOutput'); const markdownRendered = document.getElementById('markdownRendered'); - const tabs = document.querySelectorAll('.tab'); - const tabContents = document.querySelectorAll('.tab-content'); + const downloadMdBtn = document.getElementById('downloadMdBtn'); + const downloadHtmlBtn = document.getElementById('downloadHtmlBtn'); const versionValue = document.getElementById('versionValue'); const lastUpdateValue = document.getElementById('lastUpdateValue'); const releaseLink = document.getElementById('releaseLink'); + let currentMarkdownReport = ''; + let currentRenderedReportHtml = ''; + let currentReportBaseName = 'sensor-metrics-report'; const releasesUrl = 'https://github.com/stackrox/sensor-metrics-analyzer/releases'; releaseLink.href = releasesUrl; @@ -406,6 +382,10 @@

Sensor Metrics Analyzer

resultsDiv.classList.remove('show'); loadingDiv.style.display = 'block'; analyzeBtn.disabled = true; + setDownloadButtonsEnabled(false); + currentMarkdownReport = ''; + currentRenderedReportHtml = ''; + currentReportBaseName = 'sensor-metrics-report'; const formData = new FormData(); formData.append('file', file); @@ -421,28 +401,20 @@

Sensor Metrics Analyzer

loadingDiv.style.display = 'none'; analyzeBtn.disabled = false; - if (data.error && !data.console && !data.markdown) { + if (data.error && !data.markdown) { showError(data.error); return; } - // Show results - if (data.console) { - consoleOutput.textContent = data.console; - } else { - consoleOutput.textContent = 'No console output available'; - } - if (data.markdown) { - if (window.marked && typeof window.marked.parse === 'function') { - markdownRendered.innerHTML = window.marked.parse(data.markdown); - } else { - markdownRendered.textContent = data.markdown; - } - markdownOutput.textContent = data.markdown; + currentMarkdownReport = data.markdown; + currentRenderedReportHtml = renderMarkdownToHtml(data.markdown); + markdownRendered.innerHTML = currentRenderedReportHtml; + currentReportBaseName = buildReportBaseName(data.clusterName, data.analysisTimestamp); + setDownloadButtonsEnabled(true); } else { markdownRendered.textContent = 'No markdown output available'; - markdownOutput.textContent = 'No markdown output available'; + setDownloadButtonsEnabled(false); } if (data.error) { @@ -457,29 +429,140 @@

Sensor Metrics Analyzer

} }); - // Tab switching - tabs.forEach(tab => { - tab.addEventListener('click', () => { - const targetTab = tab.dataset.tab; - - // Update active tab - tabs.forEach(t => t.classList.remove('active')); - tab.classList.add('active'); + downloadMdBtn.addEventListener('click', () => { + if (!currentMarkdownReport) { + return; + } + downloadTextFile(currentMarkdownReport, `${currentReportBaseName}.md`, 'text/markdown;charset=utf-8'); + }); - // Update active content - tabContents.forEach(content => { - content.classList.remove('active'); - if (content.id === targetTab + 'Tab') { - content.classList.add('active'); - } - }); - }); + downloadHtmlBtn.addEventListener('click', () => { + if (!currentRenderedReportHtml) { + return; + } + const htmlDocument = buildHtmlReportDocument(currentRenderedReportHtml, currentReportBaseName); + downloadTextFile(htmlDocument, `${currentReportBaseName}.html`, 'text/html;charset=utf-8'); }); function showError(message) { errorDiv.textContent = message; errorDiv.style.display = 'block'; } + + function setDownloadButtonsEnabled(isEnabled) { + downloadMdBtn.disabled = !isEnabled; + downloadHtmlBtn.disabled = !isEnabled; + } + + function renderMarkdownToHtml(markdown) { + if (window.marked && typeof window.marked.parse === 'function') { + return window.marked.parse(markdown); + } + return `
${escapeHtml(markdown)}
`; + } + + function buildReportBaseName(clusterName, analysisTimestamp) { + const sanitizedCluster = sanitizeFilePart(clusterName || 'cluster'); + const formattedDate = formatDateForFilename(analysisTimestamp); + return `sensor-metrics-${sanitizedCluster}-${formattedDate}`; + } + + function formatDateForFilename(rawTimestamp) { + const date = rawTimestamp ? new Date(rawTimestamp) : new Date(); + if (Number.isNaN(date.getTime())) { + return 'unknown-date'; + } + const year = date.getUTCFullYear(); + const month = String(date.getUTCMonth() + 1).padStart(2, '0'); + const day = String(date.getUTCDate()).padStart(2, '0'); + const hours = String(date.getUTCHours()).padStart(2, '0'); + const minutes = String(date.getUTCMinutes()).padStart(2, '0'); + const seconds = String(date.getUTCSeconds()).padStart(2, '0'); + return `${year}${month}${day}-${hours}${minutes}${seconds}`; + } + + function sanitizeFilePart(value) { + return String(value) + .toLowerCase() + .replace(/[^a-z0-9-]+/g, '-') + .replace(/^-+|-+$/g, '') + .slice(0, 64) || 'cluster'; + } + + function downloadTextFile(content, filename, mimeType) { + const blob = new Blob([content], { type: mimeType }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + } + + function buildHtmlReportDocument(renderedHtml, reportBaseName) { + const escapedTitle = escapeHtml(reportBaseName); + return ` + + + + + ${escapedTitle} + + + +
+${renderedHtml} +
+ +`; + } + + function escapeHtml(text) { + return String(text) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + }