diff --git a/scripts/export_report.py b/scripts/export_report.py
new file mode 100644
index 0000000..e553f57
--- /dev/null
+++ b/scripts/export_report.py
@@ -0,0 +1,302 @@
+#!/usr/bin/env python3
+"""
+Storage Saturation Report Export Script
+Converts JSON report data to PDF or JPG format using weasyprint and Pillow
+"""
+
+import sys
+import json
+import argparse
+import os
+from datetime import datetime
+
+try:
+ from weasyprint import HTML, CSS
+except ImportError:
+ print("Error: weasyprint module not installed. Please install with: pip3 install weasyprint", file=sys.stderr)
+ sys.exit(1)
+
+try:
+ from PIL import Image
+ from pdf2image import convert_from_path
+except ImportError:
+ print("Error: Pillow or pdf2image module not installed. Please install with: pip3 install Pillow pdf2image", file=sys.stderr)
+ sys.exit(1)
+
+
+def generate_html_template(data):
+ """Generate HTML report from JSON data"""
+
+ title = data.get('title', 'Storage Saturation Report')
+ date = data.get('date', datetime.now().strftime('%m/%d/%y, %I:%M %p'))
+ services = data.get('services', [])
+
+ # Determine color class for available space
+ def get_available_class(available_bytes):
+ if available_bytes == 0:
+ return 'text-muted'
+ available_gb = available_bytes / (1024 * 1024 * 1024)
+ if available_gb < 5:
+ return 'text-danger'
+ elif available_gb < 50:
+ return 'text-warning'
+ return 'text-success'
+
+ # Determine progress bar class
+ def get_progress_class(percent):
+ if percent >= 95:
+ return 'progress-bar-danger'
+ elif percent >= 85:
+ return 'progress-bar-warning'
+ elif percent >= 70:
+ return 'progress-bar-info'
+ return 'progress-bar-success'
+
+ # Build table rows
+ table_rows = ''
+ if isinstance(services, dict) and 'error' in services:
+ table_rows = f'''
+
+ |
+ Error: {services['error']}
+ |
+
+ '''
+ elif isinstance(services, list) and len(services) > 0:
+ for service in services:
+ host_name = service.get('host_name', 'N/A')
+ caption = service.get('service_description', service.get('name', 'N/A'))
+ disk_used = service.get('disk_used_display', 'N/A')
+ disk_available = service.get('disk_available_display', 'N/A')
+ disk_percent = service.get('disk_usage_percent', 0)
+ disk_percent_display = service.get('disk_percent_display', 'N/A')
+
+ # Get color classes
+ available_bytes = service.get('disk_available_bytes', 0)
+ available_class = get_available_class(available_bytes)
+ progress_class = get_progress_class(disk_percent)
+
+ # Build progress bar HTML
+ if disk_percent > 0 and disk_percent_display != 'N/A':
+ progress_bar = f'''
+
+
+ {disk_percent_display}
+
+
+ '''
+ else:
+ progress_bar = 'N/A'
+
+ # Determine text color for available
+ available_color = '#d9534f' if available_class == 'text-danger' else '#f0ad4e' if available_class == 'text-warning' else '#5cb85c'
+
+ table_rows += f'''
+
+ | {host_name} |
+ {caption} |
+ {disk_used} |
+ {disk_available} |
+ {progress_bar} |
+
+ '''
+ else:
+ table_rows = '''
+
+ |
+ No data available
+ |
+
+ '''
+
+ html_template = f'''
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+ | DISPLAY NAME |
+ CAPTION |
+ DISK SPACE USED |
+ DISK SPACE AVAILABLE |
+ PERCENT USED |
+
+
+
+ {table_rows}
+
+
+
+
+'''
+
+ return html_template
+
+
+def convert_to_pdf(html_content, output_file):
+ """Convert HTML to PDF using weasyprint"""
+ try:
+ HTML(string=html_content).write_pdf(output_file)
+ return True
+ except Exception as e:
+ print(f"Error converting to PDF: {e}", file=sys.stderr)
+ return False
+
+
+def convert_pdf_to_jpg(pdf_file, jpg_file):
+ """Convert PDF to JPG using pdf2image"""
+ try:
+ # Convert PDF to images (first page only)
+ images = convert_from_path(pdf_file, first_page=1, last_page=1, dpi=150)
+ if images:
+ # Save as JPG
+ images[0].save(jpg_file, 'JPEG', quality=95)
+ return True
+ return False
+ except Exception as e:
+ print(f"Error converting PDF to JPG: {e}", file=sys.stderr)
+ return False
+
+
+def main():
+ parser = argparse.ArgumentParser(description='Export Storage Saturation Report to PDF or JPG')
+ parser.add_argument('--input', required=True, help='Input JSON file path')
+ parser.add_argument('--output', required=True, help='Output file path')
+ parser.add_argument('--format', required=True, choices=['pdf', 'jpg'], help='Output format (pdf or jpg)')
+
+ args = parser.parse_args()
+
+ # Read JSON input
+ try:
+ with open(args.input, 'r', encoding='utf-8') as f:
+ data = json.load(f)
+ except FileNotFoundError:
+ print(f"Error: Input file not found: {args.input}", file=sys.stderr)
+ sys.exit(1)
+ except json.JSONDecodeError as e:
+ print(f"Error: Invalid JSON in input file: {e}", file=sys.stderr)
+ sys.exit(1)
+ except Exception as e:
+ print(f"Error reading input file: {e}", file=sys.stderr)
+ sys.exit(1)
+
+ # Generate HTML
+ html_content = generate_html_template(data)
+
+ # Convert to PDF
+ if args.format == 'pdf':
+ if not convert_to_pdf(html_content, args.output):
+ sys.exit(1)
+ elif args.format == 'jpg':
+ # First convert to PDF, then to JPG
+ temp_pdf = args.output.replace('.jpg', '.pdf').replace('.jpeg', '.pdf')
+ if not convert_to_pdf(html_content, temp_pdf):
+ sys.exit(1)
+
+ if not convert_pdf_to_jpg(temp_pdf, args.output):
+ # Clean up temp PDF
+ if os.path.exists(temp_pdf):
+ os.remove(temp_pdf)
+ sys.exit(1)
+
+ # Clean up temp PDF
+ if os.path.exists(temp_pdf):
+ os.remove(temp_pdf)
+
+ print(f"Successfully created {args.format.upper()}: {args.output}")
+ sys.exit(0)
+
+
+if __name__ == '__main__':
+ main()
+
diff --git a/scripts/main.js b/scripts/main.js
new file mode 100644
index 0000000..a19157d
--- /dev/null
+++ b/scripts/main.js
@@ -0,0 +1,176 @@
+/**
+ * Storage Saturation Report - Main JavaScript
+ * Loads report via mode=getreport. PDF/Image export via client-side jsPDF + html2canvas (no XI Chromium pipeline).
+ */
+
+$(document).ready(function() {
+ load_report();
+
+ $('#export-pdf-btn').on('click', function() {
+ exportToPdf();
+ });
+
+ $('#export-image-btn').on('click', function() {
+ exportToImage();
+ });
+
+ function load_report() {
+ var baseUrl = window.saturationreportBaseUrl || (window.location.pathname.replace(/\/[^/]*$/, '/index.php'));
+ var url = baseUrl + '?mode=getreport';
+
+ $.get(url, function(html) {
+ $('#report').html(html);
+ }).fail(function() {
+ $('#report').html(
+ '' +
+ 'Error loading storage saturation report.' +
+ '
'
+ );
+ });
+ }
+
+ function getComponentBase() {
+ return (window.saturationreportBaseUrl || '').replace(/\/[^/]*$/, '');
+ }
+
+ function loadScript(src, onload, onerror) {
+ var script = document.createElement('script');
+ script.src = src;
+ script.onload = onload;
+ script.onerror = onerror || function() {};
+ document.head.appendChild(script);
+ }
+
+ function exportToPdf() {
+ var btn = $('#export-pdf-btn');
+ var originalText = btn.html();
+ btn.prop('disabled', true).html(' Generating...');
+
+ function doPdf() {
+ if (typeof html2canvas === 'undefined') {
+ var componentBase = getComponentBase();
+ var localH2c = componentBase ? (componentBase + '/scripts/vendor/html2canvas.min.js') : '';
+ var cdnH2c = 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js';
+ loadScript(localH2c || cdnH2c, function() { doPdf(); }, function() {
+ if (localH2c) { loadScript(cdnH2c, doPdf, fallbackPdf); return; }
+ fallbackPdf();
+ });
+ return;
+ }
+ if (typeof window.jspdf === 'undefined' || !window.jspdf.jsPDF) {
+ var componentBase = getComponentBase();
+ var localJspdf = componentBase ? (componentBase + '/scripts/vendor/jspdf.umd.min.js') : '';
+ var cdnJspdf = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js';
+ loadScript(localJspdf || cdnJspdf, doPdf, function() {
+ if (localJspdf) loadScript(cdnJspdf, doPdf, fallbackPdf);
+ else fallbackPdf();
+ });
+ return;
+ }
+ var container = document.getElementById('saturationreport-container');
+ if (!container) container = document.getElementById('report');
+ if (!container) {
+ alert('Report container not found');
+ btn.prop('disabled', false).html(originalText);
+ return;
+ }
+ html2canvas(container, {
+ backgroundColor: '#ffffff',
+ scale: 2,
+ logging: false,
+ useCORS: true
+ }).then(function(canvas) {
+ try {
+ var JsPDF = window.jspdf.jsPDF;
+ var doc = new JsPDF('p', 'mm', 'a4');
+ var pageW = doc.internal.pageSize.getWidth();
+ var pageH = doc.internal.pageSize.getHeight();
+ var margin = 10;
+ var maxW = pageW - 2 * margin;
+ var maxH = pageH - 2 * margin;
+ var imgW = maxW;
+ var imgH = (canvas.height * maxW) / canvas.width;
+ if (imgH > maxH) {
+ imgH = maxH;
+ imgW = (canvas.width * maxH) / canvas.height;
+ }
+ doc.addImage(canvas.toDataURL('image/png'), 'PNG', margin, margin, imgW, imgH);
+ doc.save('storage-saturation-report-' + new Date().getTime() + '.pdf');
+ } catch (err) {
+ console.error('PDF export error:', err);
+ alert('Failed to generate PDF. Try Print to PDF from the browser.');
+ }
+ btn.prop('disabled', false).html(originalText);
+ }).catch(function(err) {
+ console.error('Capture error:', err);
+ fallbackPdf();
+ });
+ }
+
+ function fallbackPdf() {
+ btn.prop('disabled', false).html(originalText);
+ window.print();
+ }
+
+ doPdf();
+ }
+
+ function exportToImage() {
+ if (typeof html2canvas === 'undefined') {
+ var componentBase = (window.saturationreportBaseUrl || '').replace(/\/[^/]*$/, '');
+ var localSrc = componentBase ? (componentBase + '/scripts/vendor/html2canvas.min.js') : '';
+ var cdnSrc = 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js';
+ var script = document.createElement('script');
+ script.src = localSrc || cdnSrc;
+ script.onload = function() { captureImage(); };
+ script.onerror = function() {
+ if (localSrc && script.src === localSrc) {
+ var fallback = document.createElement('script');
+ fallback.src = cdnSrc;
+ fallback.onload = function() { captureImage(); };
+ fallback.onerror = function() {
+ alert('Failed to load image export library. Please try using browser screenshot or print-to-PDF instead.');
+ };
+ document.head.appendChild(fallback);
+ } else {
+ alert('Failed to load image export library. Please try using browser screenshot or print-to-PDF instead.');
+ }
+ };
+ document.head.appendChild(script);
+ } else {
+ captureImage();
+ }
+ }
+
+ function captureImage() {
+ var container = document.getElementById('saturationreport-container');
+ if (!container) {
+ container = document.getElementById('report');
+ }
+ if (!container) {
+ alert('Report container not found');
+ return;
+ }
+
+ var btn = $('#export-image-btn');
+ var originalText = btn.html();
+ btn.prop('disabled', true).html(' Generating...');
+
+ html2canvas(container, {
+ backgroundColor: '#ffffff',
+ scale: 2,
+ logging: false,
+ useCORS: true
+ }).then(function(canvas) {
+ var link = document.createElement('a');
+ link.download = 'saturation-report-' + new Date().getTime() + '.png';
+ link.href = canvas.toDataURL('image/png');
+ link.click();
+ btn.prop('disabled', false).html(originalText);
+ }).catch(function(err) {
+ console.error('Error capturing image:', err);
+ alert('Failed to generate image. Please try using browser screenshot or print-to-PDF instead.');
+ btn.prop('disabled', false).html(originalText);
+ });
+ }
+});