diff --git a/README.md b/README.md
index f3db19a..461a133 100644
--- a/README.md
+++ b/README.md
@@ -61,6 +61,17 @@ docker-compose up --build
**Metrics:** Utilization, temperature, memory, power draw, fan speed, clock speeds, PCIe info, P-State, throttle status, encoder/decoder sessions
+### Export Features
+
+**Snapshot exports:**
+- **JSON Export** - Machine-readable metrics + 60s history
+- **HTML Report** - Professional, printable, self-contained report
+- **Chart Images** - High-resolution PNG screenshots
+
+**Use cases:** Bug reports, documentation, capacity planning
+
+**How to use:** Click "📊 Export" button → Select format → Downloads automatically
+
---
## Configuration
diff --git a/static/css/styles.css b/static/css/styles.css
index 0da5542..e8fb851 100644
--- a/static/css/styles.css
+++ b/static/css/styles.css
@@ -1185,6 +1185,119 @@ body {
box-shadow: 0 0 0 1px rgba(79, 172, 254, 0.3);
}
+/* Export Controls */
+.export-controls {
+ position: relative;
+ margin-bottom: 2rem;
+ display: flex;
+ justify-content: flex-end;
+}
+
+.export-btn {
+ padding: 0.75rem 1.5rem;
+ background: linear-gradient(135deg, rgba(79, 172, 254, 0.15) 0%, rgba(30, 58, 138, 0.15) 100%);
+ border: 1px solid rgba(79, 172, 254, 0.3);
+ border-radius: 12px;
+ color: var(--text-primary);
+ font-size: 0.9rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ letter-spacing: 0.3px;
+}
+
+.export-btn:hover {
+ background: linear-gradient(135deg, rgba(79, 172, 254, 0.25) 0%, rgba(30, 58, 138, 0.25) 100%);
+ border-color: rgba(79, 172, 254, 0.5);
+ box-shadow: 0 4px 16px rgba(79, 172, 254, 0.2);
+ transform: translateY(-2px);
+}
+
+.export-btn:active {
+ transform: translateY(0);
+}
+
+.export-chevron {
+ font-size: 0.75rem;
+ transition: transform 0.3s ease;
+}
+
+.export-btn.active .export-chevron {
+ transform: rotate(180deg);
+}
+
+.export-menu {
+ position: absolute;
+ top: calc(100% + 0.5rem);
+ right: 0;
+ min-width: 280px;
+ background: linear-gradient(135deg, rgba(15, 15, 35, 0.98) 0%, rgba(20, 20, 50, 0.98) 100%);
+ backdrop-filter: blur(30px);
+ border: 1px solid rgba(79, 172, 254, 0.3);
+ border-radius: 12px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
+ padding: 0.5rem;
+ z-index: 1000;
+ opacity: 1;
+ transform: translateY(0);
+ transition: all 0.3s ease;
+}
+
+.export-menu.hidden {
+ opacity: 0;
+ transform: translateY(-10px);
+ pointer-events: none;
+}
+
+.export-option {
+ width: 100%;
+ padding: 0.875rem 1rem;
+ background: transparent;
+ border: none;
+ border-radius: 8px;
+ color: var(--text-secondary);
+ font-size: 0.875rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ text-align: left;
+ display: flex;
+ align-items: flex-start;
+ gap: 0.75rem;
+ line-height: 1.4;
+}
+
+.export-option:hover {
+ background: rgba(79, 172, 254, 0.1);
+ color: var(--text-primary);
+ transform: translateX(4px);
+}
+
+.export-option:active {
+ background: rgba(79, 172, 254, 0.15);
+}
+
+/* Mobile responsive */
+@media (max-width: 768px) {
+ .export-controls {
+ justify-content: center;
+ }
+
+ .export-btn {
+ width: 100%;
+ justify-content: center;
+ }
+
+ .export-menu {
+ left: 0;
+ right: 0;
+ min-width: 100%;
+ }
+}
+
/* Tab Content with smooth transitions */
.tab-content {
display: none;
diff --git a/static/js/export.js b/static/js/export.js
new file mode 100644
index 0000000..929fdbd
--- /dev/null
+++ b/static/js/export.js
@@ -0,0 +1,563 @@
+/**
+ * GPU Hot - Export Functionality
+ * Client-side export of GPU metrics to JSON, HTML, and Chart Images
+ */
+
+// Toggle export menu visibility
+function toggleExportMenu() {
+ const menu = document.getElementById('export-menu');
+ const btn = document.getElementById('export-btn');
+
+ menu.classList.toggle('hidden');
+ btn.classList.toggle('active');
+
+ // Close menu when clicking outside
+ if (!menu.classList.contains('hidden')) {
+ setTimeout(() => {
+ document.addEventListener('click', closeExportMenuOnOutsideClick);
+ }, 100);
+ } else {
+ document.removeEventListener('click', closeExportMenuOnOutsideClick);
+ }
+}
+
+function closeExportMenuOnOutsideClick(event) {
+ const menu = document.getElementById('export-menu');
+ const btn = document.getElementById('export-btn');
+ const controls = document.querySelector('.export-controls');
+
+ if (!controls.contains(event.target)) {
+ menu.classList.add('hidden');
+ btn.classList.remove('active');
+ document.removeEventListener('click', closeExportMenuOnOutsideClick);
+ }
+}
+
+// Export as JSON
+function exportAsJSON() {
+ try {
+ const exportData = {
+ metadata: {
+ export_timestamp: new Date().toISOString(),
+ export_format: 'json',
+ application: 'GPU Hot',
+ version: document.getElementById('version-current')?.textContent || 'unknown'
+ },
+ gpus: collectGPUData(),
+ processes: collectProcessData(),
+ system: collectSystemData(),
+ historical_data: collectHistoricalData()
+ };
+
+ const jsonString = JSON.stringify(exportData, null, 2);
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
+ const filename = `gpu-hot-export-${timestamp}.json`;
+
+ downloadFile(jsonString, filename, 'application/json');
+ closeExportMenu();
+
+ console.log('JSON export completed:', filename);
+ } catch (error) {
+ console.error('Error exporting JSON:', error);
+ alert('Error exporting JSON data. Check console for details.');
+ }
+}
+
+// Export as HTML Report
+async function exportAsHTML() {
+ try {
+ const exportData = {
+ metadata: {
+ export_timestamp: new Date().toISOString(),
+ export_format: 'html',
+ application: 'GPU Hot',
+ version: document.getElementById('version-current')?.textContent || 'unknown'
+ },
+ gpus: collectGPUData(),
+ processes: collectProcessData(),
+ system: collectSystemData(),
+ charts: await generateChartImages()
+ };
+
+ const htmlContent = generateHTMLReport(exportData);
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
+ const filename = `gpu-hot-report-${timestamp}.html`;
+
+ downloadFile(htmlContent, filename, 'text/html');
+ closeExportMenu();
+
+ console.log('HTML export completed:', filename);
+ } catch (error) {
+ console.error('Error exporting HTML:', error);
+ alert('Error exporting HTML report. Check console for details.');
+ }
+}
+
+// Export Chart Images
+async function exportChartImages() {
+ try {
+ const chartImages = await generateChartImages();
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
+
+ if (Object.keys(chartImages).length === 0) {
+ alert('No charts available to export. Please wait for data to load.');
+ return;
+ }
+
+ // Download each chart as a separate PNG file
+ for (const [chartName, base64Image] of Object.entries(chartImages)) {
+ // Convert base64 to blob
+ const base64Data = base64Image.split(',')[1];
+ const byteCharacters = atob(base64Data);
+ const byteNumbers = new Array(byteCharacters.length);
+ for (let i = 0; i < byteCharacters.length; i++) {
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
+ }
+ const byteArray = new Uint8Array(byteNumbers);
+ const blob = new Blob([byteArray], { type: 'image/png' });
+
+ // Create download link
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `gpu-hot-${chartName}-${timestamp}.png`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+
+ // Small delay between downloads to prevent browser blocking
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+
+ closeExportMenu();
+ console.log(`Chart export completed: ${Object.keys(chartImages).length} images downloaded`);
+ } catch (error) {
+ console.error('Error exporting charts:', error);
+ alert('Error exporting chart images. Check console for details.');
+ }
+}
+
+// Helper: Close export menu
+function closeExportMenu() {
+ const menu = document.getElementById('export-menu');
+ const btn = document.getElementById('export-btn');
+ menu.classList.add('hidden');
+ btn.classList.remove('active');
+ document.removeEventListener('click', closeExportMenuOnOutsideClick);
+}
+
+// Collect GPU data from DOM
+function collectGPUData() {
+ const gpus = {};
+
+ // Get all GPU cards
+ const gpuCards = document.querySelectorAll('.overview-gpu-card, .gpu-card');
+
+ gpuCards.forEach((card, index) => {
+ const gpuId = card.dataset.gpuId || `gpu-${index}`;
+ const nodeName = card.dataset.nodeName || 'default';
+
+ // Extract GPU name
+ const nameElement = card.querySelector('.gpu-name, .gpu-model');
+ const gpuName = nameElement?.textContent.trim() || 'Unknown GPU';
+
+ // Extract metrics
+ const metrics = {};
+
+ // Utilization
+ const utilizationElement = card.querySelector('[data-metric="utilization"]');
+ if (utilizationElement) {
+ metrics.utilization = parseFloat(utilizationElement.textContent) || 0;
+ }
+
+ // Memory
+ const memoryElement = card.querySelector('[data-metric="memory"]');
+ if (memoryElement) {
+ metrics.memory_used = memoryElement.textContent.trim();
+ }
+
+ // Temperature
+ const temperatureElement = card.querySelector('[data-metric="temperature"]');
+ if (temperatureElement) {
+ metrics.temperature = parseFloat(temperatureElement.textContent) || 0;
+ }
+
+ // Power
+ const powerElement = card.querySelector('[data-metric="power"]');
+ if (powerElement) {
+ metrics.power_draw = powerElement.textContent.trim();
+ }
+
+ // Fan speed
+ const fanElement = card.querySelector('[data-metric="fan"]');
+ if (fanElement) {
+ metrics.fan_speed = parseFloat(fanElement.textContent) || 0;
+ }
+
+ // Clocks
+ const clockElement = card.querySelector('[data-metric="clock"]');
+ if (clockElement) {
+ metrics.gpu_clock = clockElement.textContent.trim();
+ }
+
+ gpus[gpuId] = {
+ node_name: nodeName,
+ name: gpuName,
+ metrics: metrics
+ };
+ });
+
+ return gpus;
+}
+
+// Collect process data from DOM
+function collectProcessData() {
+ const processes = [];
+
+ const processRows = document.querySelectorAll('.process-row, .process-item');
+
+ processRows.forEach((row) => {
+ const process = {
+ pid: row.querySelector('.process-pid')?.textContent.trim() || '',
+ name: row.querySelector('.process-name')?.textContent.trim() || '',
+ gpu_id: row.dataset.gpuId || '',
+ memory: row.querySelector('.process-memory')?.textContent.trim() || '',
+ gpu_utilization: row.querySelector('.process-utilization')?.textContent.trim() || ''
+ };
+
+ if (process.pid || process.name) {
+ processes.push(process);
+ }
+ });
+
+ return processes;
+}
+
+// Collect system data from DOM
+function collectSystemData() {
+ const system = {};
+
+ // CPU usage
+ const cpuElement = document.getElementById('cpu-usage');
+ if (cpuElement) {
+ system.cpu_usage = cpuElement.textContent.trim();
+ }
+
+ // Memory usage
+ const memoryElement = document.getElementById('memory-usage');
+ if (memoryElement) {
+ system.memory_usage = memoryElement.textContent.trim();
+ }
+
+ // Connection status
+ const connectionElement = document.getElementById('connection-status');
+ if (connectionElement) {
+ system.connection_status = connectionElement.textContent.trim();
+ }
+
+ // Process count
+ const processCountElement = document.getElementById('process-count');
+ if (processCountElement) {
+ system.active_processes = processCountElement.textContent.trim();
+ }
+
+ return system;
+}
+
+// Collect historical chart data (60 second window)
+function collectHistoricalData() {
+ const historical = {};
+
+ // Check if chartData is available globally
+ if (typeof chartData !== 'undefined' && chartData) {
+ // Copy the chart data structure
+ Object.keys(chartData).forEach(key => {
+ if (chartData[key] && Array.isArray(chartData[key])) {
+ historical[key] = [...chartData[key]];
+ }
+ });
+ }
+
+ return historical;
+}
+
+// Generate chart images from Chart.js instances
+async function generateChartImages() {
+ const chartImages = {};
+
+ // Check if charts object is available globally
+ if (typeof charts === 'undefined' || !charts) {
+ console.warn('Charts object not available for export');
+ return chartImages;
+ }
+
+ // Export each chart as base64 PNG
+ for (const [chartName, chartInstance] of Object.entries(charts)) {
+ if (chartInstance && typeof chartInstance.toBase64Image === 'function') {
+ try {
+ const base64Image = chartInstance.toBase64Image('image/png', 1.0);
+ chartImages[chartName] = base64Image;
+ } catch (error) {
+ console.warn(`Failed to export chart ${chartName}:`, error);
+ }
+ }
+ }
+
+ return chartImages;
+}
+
+// Generate HTML report
+function generateHTMLReport(data) {
+ const timestamp = new Date(data.metadata.export_timestamp).toLocaleString();
+
+ let chartHTML = '';
+ if (data.charts && Object.keys(data.charts).length > 0) {
+ chartHTML = '
Performance Charts
';
+ for (const [chartName, base64Image] of Object.entries(data.charts)) {
+ chartHTML += `
+
+
${formatChartName(chartName)}
+

+
+ `;
+ }
+ chartHTML += '
';
+ }
+
+ let gpuHTML = '';
+ if (data.gpus && Object.keys(data.gpus).length > 0) {
+ gpuHTML = 'GPU Status
| GPU | Node | Utilization | Memory | Temperature | Power | Fan |
';
+
+ for (const [gpuId, gpu] of Object.entries(data.gpus)) {
+ gpuHTML += `
+
+ | ${gpu.name} |
+ ${gpu.node_name} |
+ ${gpu.metrics.utilization !== undefined ? gpu.metrics.utilization + '%' : 'N/A'} |
+ ${gpu.metrics.memory_used || 'N/A'} |
+ ${gpu.metrics.temperature !== undefined ? gpu.metrics.temperature + '°C' : 'N/A'} |
+ ${gpu.metrics.power_draw || 'N/A'} |
+ ${gpu.metrics.fan_speed !== undefined ? gpu.metrics.fan_speed + '%' : 'N/A'} |
+
+ `;
+ }
+
+ gpuHTML += '
';
+ }
+
+ let processHTML = '';
+ if (data.processes && data.processes.length > 0) {
+ processHTML = 'Active Processes
| PID | Name | GPU | Memory | Utilization |
';
+
+ data.processes.forEach(process => {
+ processHTML += `
+
+ | ${process.pid} |
+ ${process.name} |
+ ${process.gpu_id} |
+ ${process.memory} |
+ ${process.gpu_utilization} |
+
+ `;
+ });
+
+ processHTML += '
';
+ }
+
+ const systemHTML = `
+
+
System Metrics
+
+
CPU Usage: ${data.system.cpu_usage || 'N/A'}
+
RAM Usage: ${data.system.memory_usage || 'N/A'}
+
Connection: ${data.system.connection_status || 'N/A'}
+
Active Processes: ${data.system.active_processes || 'N/A'}
+
+
+ `;
+
+ const html = `
+
+
+
+
+ GPU Hot Report - ${timestamp}
+
+
+
+
+
🔥 GPU Hot Report
+
+
+ ${systemHTML}
+ ${gpuHTML}
+ ${processHTML}
+ ${chartHTML}
+
+
+
+
+`;
+
+ return html;
+}
+
+// Format chart name for display
+function formatChartName(chartName) {
+ return chartName
+ .replace(/([A-Z])/g, ' $1')
+ .replace(/^./, str => str.toUpperCase())
+ .replace(/-/g, ' ')
+ .trim();
+}
+
+// Download file helper
+function downloadFile(content, filename, mimeType) {
+ const blob = new Blob([content], { type: mimeType });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+}
diff --git a/templates/index.html b/templates/index.html
index a4dcd84..fe636d2 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -47,6 +47,24 @@ 🔥 GPU Hot
+
+
+
+
+
+
@@ -112,12 +130,13 @@
🔥 GPU Hot
-
+
+