From 507d01be674e5b0b255a66f00641deb5176d8ae7 Mon Sep 17 00:00:00 2001 From: tamara Date: Mon, 6 Apr 2026 12:10:59 +0200 Subject: [PATCH 1/2] Add scripts files --- .../analyze_codee_reports.py | 398 ++++++++++++++++++ .../templates/codee_report.css | 128 ++++++ .../templates/codee_report.html | 73 ++++ .../templates/codee_report.js | 227 ++++++++++ 4 files changed, 826 insertions(+) create mode 100644 scripts/post_processing_codee_reports/analyze_codee_reports.py create mode 100644 scripts/post_processing_codee_reports/templates/codee_report.css create mode 100644 scripts/post_processing_codee_reports/templates/codee_report.html create mode 100644 scripts/post_processing_codee_reports/templates/codee_report.js diff --git a/scripts/post_processing_codee_reports/analyze_codee_reports.py b/scripts/post_processing_codee_reports/analyze_codee_reports.py new file mode 100644 index 0000000..ff30fb8 --- /dev/null +++ b/scripts/post_processing_codee_reports/analyze_codee_reports.py @@ -0,0 +1,398 @@ +#!/usr/bin/env python3 +"""Analyze Codee JSON or HTML reports over time and generate time-series visualizations.""" + +import argparse +import json +import logging +import re +import sys +import pandas as pd +from collections import defaultdict +from datetime import datetime +from pathlib import Path + + +def setup_logging() -> logging.Logger: + """Configure logging for the script.""" + logger = logging.getLogger("codee_analyzer") + logger.setLevel(logging.INFO) + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")) + logger.addHandler(handler) + return logger + + +def load_json_file(file_path: Path, logger: logging.Logger) -> dict | None: + """Load and parse a JSON file, returning None on failure.""" + try: + with open(file_path, "r", encoding="utf-8") as f: + return json.load(f) + except json.JSONDecodeError as e: + logger.warning(f"Malformed JSON in {file_path.name}: {e}") + return None + except OSError as e: + logger.warning(f"Failed to read {file_path.name}: {e}") + return None + + +def load_html_report(report_dir: Path, logger: logging.Logger) -> dict | None: + """Load a Codee HTML report by parsing report.js file.""" + report_js_path = report_dir / "report.js" + if not report_js_path.exists(): + logger.warning(f"report.js not found in {report_dir.name}") + return None + + try: + content = report_js_path.read_text(encoding="utf-8") + match = re.search(r"const\s+report\s*=\s*(\{.*\});", content, re.DOTALL) + if not match: + match = re.search(r"var\s+report\s*=\s*(\{.*\});", content, re.DOTALL) + + if not match: + logger.warning(f"Could not find report object in {report_js_path}") + return None + + json_str = match.group(1) + return json.loads(json_str) + except Exception as e: + logger.warning(f"Failed to parse report.js in {report_dir.name}: {e}") + return None + + +def extract_timestamp(report: dict, logger: logging.Logger) -> int | None: + """Extract timestamp from report.""" + try: + exec_info = report.get("CodeeExecutionInfo", {}) + ts_utc = exec_info.get("TimestampUTC") + if ts_utc: + dt = datetime.fromisoformat(ts_utc.replace("Z", "+00:00")) + return int(dt.timestamp()) + return exec_info.get("TimestampEpochSeconds") + except Exception as e: + logger.warning(f"Failed to extract timestamp: {e}") + return None + + +def extract_checker_counts(report: dict, logger: logging.Logger) -> dict[str, int]: + """Extract checker counts from Quality and Optimization rankings.""" + counts: dict[str, int] = {} + + try: + screening = report.get("Screening", {}) + + quality_table = screening.get("Ranking of Quality Checkers", {}).get( + "DataTable", [] + ) + for row in quality_table: + checker = row.get("Checker", "") + if checker and checker != "Total": + try: + counts[checker] = int(row.get("#", "0")) + except ValueError: + logger.warning(f"Invalid count for checker {checker}") + + opt_table = screening.get("Ranking of Optimization Checkers", {}).get( + "DataTable", [] + ) + for row in opt_table: + checker = row.get("Checker", "") + if checker and checker != "Total": + try: + counts[checker] = int(row.get("#", "0")) + except ValueError: + logger.warning(f"Invalid count for checker {checker}") + + except Exception as e: + logger.warning(f"Failed to extract checker counts: {e}") + + return counts + + +def extract_checker_priorities(report: dict, logger: logging.Logger) -> dict[str, str]: + """Extract priority for each checker.""" + priorities: dict[str, str] = {} + + try: + screening = report.get("Screening", {}) + + quality_table = screening.get("Ranking of Quality Checkers", {}).get( + "DataTable", [] + ) + for row in quality_table: + checker = row.get("Checker", "") + if checker and checker != "Total": + priority = row.get("Priority", "") + if priority: + priorities[checker] = priority + + opt_table = screening.get("Ranking of Optimization Checkers", {}).get( + "DataTable", [] + ) + for row in opt_table: + checker = row.get("Checker", "") + if checker and checker != "Total": + priority = row.get("Priority", "") + if priority: + priorities[checker] = priority + + except Exception as e: + logger.warning(f"Failed to extract checker priorities: {e}") + + return priorities + + +def extract_l_level(priority_str: str) -> str: + """Extract L-level from priority string like 'P18 (L1)'.""" + if not priority_str: + return "Unknown" + if "L1" in priority_str: + return "L1" + if "L2" in priority_str: + return "L2" + if "L3" in priority_str: + return "L3" + if "L4" in priority_str: + return "L4" + return "Unknown" + + +def detect_input_type(input_dir: Path) -> str: + """Detect if input is JSON or HTML format by searching recursively.""" + has_json = list(input_dir.rglob("*.json")) + has_html_dirs = list(input_dir.rglob("report.js")) + + if has_html_dirs: + return "html" + elif has_json: + return "json" + return "unknown" + + +def load_reports( + input_dir: Path, logger: logging.Logger +) -> list[tuple[int, dict, Path | None]]: + """Load all reports from directory recursively, sorted by timestamp. + + Returns list of tuples: (timestamp, report_dict, path) + path is the path to the report file (for linking) + """ + reports: list[tuple[int, dict, Path | None]] = [] + input_type = detect_input_type(input_dir) + + if input_type == "html": + logger.info("Detected HTML format (recursive search for report.js)") + html_dirs = sorted(input_dir.rglob("report.js")) + for report_js in html_dirs: + report_dir = report_js.parent + report = load_html_report(report_dir, logger) + if report is None: + continue + + timestamp = extract_timestamp(report, logger) + if timestamp is None: + logger.warning(f"Skipping {report_dir.name}: no valid timestamp") + continue + + reports.append((timestamp, report, report_dir)) + + elif input_type == "json": + logger.info("Detected JSON format (recursive search for *.json)") + json_files = sorted(input_dir.rglob("*.json")) + if not json_files: + logger.warning(f"No JSON files found in {input_dir}") + return reports + + for file_path in json_files: + report = load_json_file(file_path, logger) + if report is None: + continue + + timestamp = extract_timestamp(report, logger) + if timestamp is None: + logger.warning(f"Skipping {file_path.name}: no valid timestamp") + continue + + reports.append((timestamp, report, file_path)) + + else: + logger.error(f"No valid reports found in {input_dir}") + return reports + + reports.sort(key=lambda x: x[0]) + return reports + + +def build_checker_dataframe( + reports: list[tuple[int, dict, Path | None]], logger: logging.Logger +) -> pd.DataFrame: + """Build a DataFrame with timestamps as index and checker counts as columns.""" + all_checkers: set[str] = set() + + for _, report, _ in reports: + counts = extract_checker_counts(report, logger) + all_checkers.update(counts.keys()) + + sorted_checkers = sorted(all_checkers) + data: dict[str, list[int]] = {checker: [] for checker in sorted_checkers} + timestamps: list[int] = [] + + for timestamp, report, _ in reports: + timestamps.append(timestamp) + counts = extract_checker_counts(report, logger) + for checker in sorted_checkers: + data[checker].append(counts.get(checker, 0)) + + df = pd.DataFrame(data, index=pd.to_datetime(timestamps, unit="s")) + df.index.name = "timestamp" + return df + + +def get_checker_priorities_for_df( + df: pd.DataFrame, + reports: list[tuple[int, dict, Path | None]], + logger: logging.Logger, +) -> dict[str, str]: + """Get priority mapping for all checkers in the DataFrame.""" + priorities: dict[str, str] = {} + + for _, report, _ in reports: + report_priorities = extract_checker_priorities(report, logger) + priorities.update(report_priorities) + + return priorities + + +def generate_html_report( + df: pd.DataFrame, + reports: list[tuple[int, dict, Path | None]], + input_dir: Path, + output_dir: Path, + logger: logging.Logger, +) -> None: + """Generate interactive HTML report with Chart.js.""" + logger.info("Generating interactive HTML report") + + priorities = get_checker_priorities_for_df(df, reports, logger) + labels = [d.strftime("%Y-%m-%d") for d in df.index] + total_data = df.sum(axis=1).tolist() + + checker_data: dict[str, list[int]] = {} + for checker in df.columns: + checker_data[checker] = df[checker].tolist() + + priority_order = ["L1", "L2", "L3", "L4", "Unknown"] + priority_colors = { + "L1": "#DC143C", + "L2": "#FF8C00", + "L3": "#228B22", + "L4": "#90EE90", + "Unknown": "#D3D3D3", + } + + l_level_data: dict[str, list[int]] = {l: [] for l in priority_order} + for idx in df.index: + row = df.loc[idx] + p_groups: dict[str, int] = {l: 0 for l in priority_order} + for checker, count in row.items(): + l_level = extract_l_level(priorities.get(checker, "")) + p_groups[l_level] += count + for l_level in priority_order: + l_level_data[l_level].append(p_groups[l_level]) + + # Build links to original reports (JSON or HTML) + report_links: list[dict | None] = [] + for _, _, report_path in reports: + if report_path: + abs_path = report_path.resolve() + if report_path.suffix == ".json": + report_links.append({"link": str(abs_path), "type": "json"}) + else: + index_html = abs_path / "index.html" + if index_html.exists(): + report_links.append({"link": str(index_html), "type": "html"}) + else: + report_links.append({"link": str(abs_path), "type": "html"}) + else: + report_links.append(None) + + chart_data = { + "labels": labels, + "total": total_data, + "checkers": checker_data, + "priorities": l_level_data, + "priorityColors": priority_colors, + "reportLinks": report_links, + "hasLinks": any(link is not None for link in report_links), + } + + template_path = Path(__file__).parent / "templates" / "codee_report.html" + css_path = Path(__file__).parent / "templates" / "codee_report.css" + js_path = Path(__file__).parent / "templates" / "codee_report.js" + html_template = template_path.read_text(encoding="utf-8") + css_content = css_path.read_text(encoding="utf-8") + js_template = js_path.read_text(encoding="utf-8") + + replacements = { + "${REPORT_COUNT}": str(len(reports)), + "${CHECKER_COUNT}": str(len(checker_data)), + "${TOTAL_FINDINGS}": str(int(total_data[-1])), + "${INPUT_DIR}": str(input_dir), + "${CHART_DATA}": json.dumps(chart_data, indent=2), + } + + for placeholder, value in replacements.items(): + html_template = html_template.replace(placeholder, value) + js_template = js_template.replace(placeholder, value) + + html_path = output_dir / "codee_analysis.html" + css_output_path = output_dir / "codee_report.css" + js_output_path = output_dir / "codee_report.js" + html_path.write_text(html_template, encoding="utf-8") + css_output_path.write_text(css_content, encoding="utf-8") + js_output_path.write_text(js_template, encoding="utf-8") + logger.info(f"HTML report saved to {html_path}") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Analyze Codee JSON or HTML reports and generate HTML visualizations." + ) + parser.add_argument( + "input_dir", + type=Path, + help="Directory containing Codee report files (JSON or HTML with report.js)", + ) + parser.add_argument( + "output_dir", + type=Path, + help="Directory where HTML report will be saved", + ) + args = parser.parse_args() + + logger = setup_logging() + + if not args.input_dir.is_dir(): + logger.error(f"Input directory does not exist: {args.input_dir}") + return 1 + + args.output_dir.mkdir(parents=True, exist_ok=True) + + logger.info(f"Loading reports from {args.input_dir}") + reports = load_reports(args.input_dir, logger) + + if not reports: + logger.warning("No valid reports loaded. Exiting.") + return 0 + + logger.info(f"Loaded {len(reports)} reports") + + logger.info("Building checker DataFrame") + df = build_checker_dataframe(reports, logger) + + generate_html_report(df, reports, args.input_dir, args.output_dir, logger) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/post_processing_codee_reports/templates/codee_report.css b/scripts/post_processing_codee_reports/templates/codee_report.css new file mode 100644 index 0000000..755e111 --- /dev/null +++ b/scripts/post_processing_codee_reports/templates/codee_report.css @@ -0,0 +1,128 @@ +* { + box-sizing: border-box; +} +body { + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, + sans-serif; + margin: 0; + padding: 20px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; +} +.container { + max-width: 1200px; + margin: 0 auto; +} +header { + text-align: center; + color: white; + margin-bottom: 30px; +} +header h1 { + font-size: 2.5rem; + margin: 0 0 10px 0; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); +} +header p { + font-size: 1.1rem; + opacity: 0.9; +} + +.summary { + display: flex; + justify-content: center; + gap: 20px; + margin-bottom: 30px; + flex-wrap: wrap; +} +.summary-card { + background: white; + padding: 20px 30px; + border-radius: 12px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + text-align: center; +} +.summary-card .value { + font-size: 2rem; + font-weight: bold; + color: #667eea; +} +.summary-card .label { + color: #666; + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 1px; +} + +.chart-container { + background: white; + border-radius: 12px; + padding: 25px; + margin-bottom: 25px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} +.chart-container h2 { + margin: 0 0 20px 0; + color: #333; + font-size: 1.3rem; + border-bottom: 2px solid #667eea; + padding-bottom: 10px; +} +.chart-wrapper { + position: relative; + height: 350px; +} + +.links-section { + background: white; + border-radius: 12px; + padding: 25px; + margin-bottom: 25px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} +.links-section h2 { + margin: 0 0 20px 0; + color: #333; + font-size: 1.3rem; + border-bottom: 2px solid #667eea; + padding-bottom: 10px; +} +.links-table { + width: 100%; + border-collapse: collapse; +} +.links-table th, +.links-table td { + padding: 12px; + text-align: left; + border-bottom: 1px solid #eee; +} +.links-table th { + background: #f8f9fa; + font-weight: 600; + color: #333; +} +.links-table tr:hover { + background: #f8f9fa; +} +.links-table a { + color: #667eea; + text-decoration: none; + font-weight: 500; +} +.links-table a:hover { + text-decoration: underline; +} +.no-links { + color: #666; + font-style: italic; +} + +footer { + text-align: center; + color: white; + opacity: 0.8; + margin-top: 30px; + font-size: 0.9rem; +} diff --git a/scripts/post_processing_codee_reports/templates/codee_report.html b/scripts/post_processing_codee_reports/templates/codee_report.html new file mode 100644 index 0000000..2219406 --- /dev/null +++ b/scripts/post_processing_codee_reports/templates/codee_report.html @@ -0,0 +1,73 @@ + + + + + + Codee Report Analysis + + + + +
+
+

Codee Report Analysis

+

Analyzed ${REPORT_COUNT} reports from ${INPUT_DIR}

+
+ +
+
+
${REPORT_COUNT}
+
Reports
+
+
+
${CHECKER_COUNT}
+
Unique Checkers Found
+
+
+
${TOTAL_FINDINGS}
+
Findings (Latest Report)
+
+
+ +
+

Total Findings Over Time

+
+ +
+
+ +
+

Findings by Checker

+
+ +
+
+ +
+

Findings by Priority Level

+
+ +
+
+ + + +
Generated with Codee Report Analyzer
+
+ + + + diff --git a/scripts/post_processing_codee_reports/templates/codee_report.js b/scripts/post_processing_codee_reports/templates/codee_report.js new file mode 100644 index 0000000..08cd0b1 --- /dev/null +++ b/scripts/post_processing_codee_reports/templates/codee_report.js @@ -0,0 +1,227 @@ +const chartData = ${CHART_DATA}; + +function getFileUrl(path) { + if (!path) return null; + const normalizedPath = path.replace(/\\/g, '/'); + return 'file://' + normalizedPath; +} + +function initLinksTable() { + const linksSection = document.getElementById('linksSection'); + const linksTableBody = document.getElementById('linksTableBody'); + + if (chartData.hasLinks && chartData.reportLinks) { + chartData.reportLinks.forEach((linkData, index) => { + const date = chartData.labels[index]; + const row = document.createElement('tr'); + + if (linkData && linkData.link) { + const fileUrl = getFileUrl(linkData.link); + const typeLabel = linkData.type === 'json' ? 'JSON' : 'HTML'; + const fileName = linkData.link.split('/').pop() || linkData.link; + row.innerHTML = ` + ${date} + Report ${index + 1} (${typeLabel}) + ${fileName} + `; + } else { + row.innerHTML = ` + ${date} + Report ${index + 1} + No link available + `; + } + linksTableBody.appendChild(row); + }); + } else { + linksSection.style.display = 'none'; + } +} + +function getLinkAtIndex(idx) { + if (chartData.reportLinks && chartData.reportLinks[idx]) { + return chartData.reportLinks[idx].link; + } + return null; +} + +function getLinkTypeAtIndex(idx) { + if (chartData.reportLinks && chartData.reportLinks[idx]) { + return chartData.reportLinks[idx].type || 'unknown'; + } + return null; +} + +function createTotalChart() { + const ctx = document.getElementById('totalChart').getContext('2d'); + new Chart(ctx, { + type: 'line', + data: { + labels: chartData.labels, + datasets: [{ + label: 'Total Findings', + data: chartData.total, + borderColor: '#667eea', + backgroundColor: 'rgba(102, 126, 234, 0.1)', + fill: true, + tension: 0.3, + pointRadius: 6, + pointHoverRadius: 8, + borderWidth: 3 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { display: false }, + tooltip: { + backgroundColor: 'rgba(0,0,0,0.8)', + padding: 12, + cornerRadius: 8, + callbacks: { + afterLabel: function(context) { + const idx = context.dataIndex; + const link = getLinkAtIndex(idx); + if (link) { + const type = getLinkTypeAtIndex(idx); + return `Click to open ${type} report`; + } + return ''; + } + } + } + }, + onClick: function(event, elements) { + if (elements.length > 0) { + const idx = elements[0].index; + const link = getLinkAtIndex(idx); + if (link) { + const fileUrl = getFileUrl(link); + window.open(fileUrl, '_blank'); + } + } + }, + scales: { + y: { + beginAtZero: true, + grid: { color: 'rgba(0,0,0,0.05)' } + }, + x: { + grid: { display: false } + } + } + } + }); +} + +function createCheckerChart() { + const checkerPalette = [ + '#e6194B', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', + '#42d4f4', '#f032e6', '#bfef45', '#fabed4', '#469990', '#dcbeff', + '#9A6324', '#fffac8', '#800000', '#aaffc3', '#808000', '#ffd8b1', + '#000075', '#a9a9a9', '#ffffff', '#000000', '#a93226', '#2471a3' + ]; + + const checkerDatasets = Object.entries(chartData.checkers).map(([checker, data], i) => { + const color = checkerPalette[i % checkerPalette.length]; + return { + label: checker, + data: data, + borderColor: color, + backgroundColor: color, + tension: 0.3, + pointRadius: 4, + pointHoverRadius: 6, + fill: false + }; + }); + + const ctx = document.getElementById('checkerChart').getContext('2d'); + new Chart(ctx, { + type: 'line', + data: { labels: chartData.labels, datasets: checkerDatasets }, + options: { + responsive: true, + maintainAspectRatio: false, + interaction: { + mode: 'index', + intersect: false + }, + plugins: { + legend: { + position: 'right', + labels: { boxWidth: 12, padding: 15 } + }, + tooltip: { + backgroundColor: 'rgba(0,0,0,0.8)', + padding: 12, + cornerRadius: 8 + } + }, + scales: { + y: { + beginAtZero: true, + grid: { color: 'rgba(0,0,0,0.05)' } + }, + x: { + grid: { display: false } + } + } + } + }); +} + +function createPriorityChart() { + const priorityDatasets = Object.entries(chartData.priorities) + .filter(([_, data]) => data.some(v => v > 0)) + .map(([priority, data]) => ({ + label: priority, + data: data, + backgroundColor: chartData.priorityColors[priority] + 'CC', + borderColor: chartData.priorityColors[priority], + borderWidth: 1 + })); + + const ctx = document.getElementById('priorityChart').getContext('2d'); + new Chart(ctx, { + type: 'line', + data: { labels: chartData.labels, datasets: priorityDatasets }, + options: { + responsive: true, + maintainAspectRatio: false, + interaction: { + mode: 'index', + intersect: false + }, + plugins: { + legend: { + position: 'top', + labels: { boxWidth: 12, padding: 15 } + }, + tooltip: { + backgroundColor: 'rgba(0,0,0,0.8)', + padding: 12, + cornerRadius: 8 + } + }, + scales: { + y: { + stacked: false, + beginAtZero: true, + grid: { color: 'rgba(0,0,0,0.05)' } + }, + x: { + grid: { display: false } + } + } + } + }); +} + +document.addEventListener('DOMContentLoaded', function() { + initLinksTable(); + createTotalChart(); + createCheckerChart(); + createPriorityChart(); +}); From e3f687d46d7b84c1a09db308694cf930a5e7db69 Mon Sep 17 00:00:00 2001 From: tamara Date: Mon, 6 Apr 2026 12:11:10 +0200 Subject: [PATCH 2/2] Add README.md --- .../post_processing_codee_reports/README.md | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 scripts/post_processing_codee_reports/README.md diff --git a/scripts/post_processing_codee_reports/README.md b/scripts/post_processing_codee_reports/README.md new file mode 100644 index 0000000..ca541e4 --- /dev/null +++ b/scripts/post_processing_codee_reports/README.md @@ -0,0 +1,101 @@ +# Codee Report Analyzer + +Analyze Codee reports over time and generate interactive HTML visualizations. + +## Overview + +This script analyzes multiple Codee reports, tracking how checker findings evolve over time. +It supports both JSON and HTML report formats and generates an interactive HTML dashboard with three +charts: + +- **Total Findings Over Time**: Line chart showing total findings per report +- **Findings by Checker**: Line chart for each unique checker type found +- **Findings by Priority Level**: Line chart grouped by L1/L2/L3/L4 priority + +## Requirements + +- Python 3.10+ +- pandas + +## Installation + +```bash +pip install pandas +``` + +## Usage + +### Basic Usage + +```bash +python analyze_codee_reports.py +``` + +### Input Formats + +The script automatically detects the input format: + +** Codee JSON Reports** (from `codee checks --json`): +- Searches recursively for `*.json` files +- Example directory structure: + ``` + reports/ + ├── screening-1.json + ├── screening-2.json + └── subdir/ + └── screening-3.json + ``` + +**Codee HTML Reports** (from `codee --html`): +- Searches recursively for `report.js` files +- When HTML reports are found, the generated dashboard includes links to open each original report +- Example directory structure: + ``` + reports/ + ├── run-2024-01-01/ + │ ├── report.js + │ └── ... + └── run-2024-02-01/ + ├── report.js + └── ... + ``` + +## Dashboard Features + +The generated HTML dashboard includes: + +- **Interactive charts**: Hover for tooltips, click on points to open original reports +- **Summary cards**: Show report count, unique checkers found, and the total findings from the latest report +- **Links section**: Table with links to open each original HTML report +- **Responsive design**: Works on desktop and mobile +- **No external dependencies**: Uses Chart.js from CDN (internet required for first load) + +## Understanding the Charts + +### Total Findings +Shows the sum of all checker findings for each report over time. Click on any point to open the original report (if available). + +### Findings by Checker +Displays each unique checker type (e.g., PWR007, PWR068) as a separate line, allowing you to see which specific checkers contribute to findings. + +### Findings by Priority Level +Groups findings by their severity level: +- **L1** (Red): High priority +- **L2** (Orange): Medium priority +- **L3** (Green): Low priority +- **L4** (Light Green): Very-low priority + +## Troubleshooting + +### "No JSON/HTML files found" +- Ensure your input directory contains valid report files +- For JSON: look for `*.json` files +- For HTML: look for directories containing `report.js` + +### "Malformed JSON/JS" +- One or more files are corrupted +- The script will skip these files and continue + +### "No valid timestamp" +- Some files may be missing the timestamp field +- The script will skip these files and continue