""" Dashboard Generator - Interactive HTML SEO dashboard with Chart.js ================================================================== Purpose: Generate a self-contained HTML dashboard from aggregated SEO report data, with responsive charts for health scores, traffic trends, keyword rankings, issue breakdowns, and competitor radar. Python: 3.10+ Usage: python dashboard_generator.py --report aggregated_report.json --output dashboard.html python dashboard_generator.py --report aggregated_report.json --output dashboard.html --title "My SEO Dashboard" """ import argparse import json import logging import sys from dataclasses import dataclass, field from datetime import datetime from pathlib import Path from typing import Any from jinja2 import Template logger = logging.getLogger(__name__) logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", ) # --------------------------------------------------------------------------- # Data classes # --------------------------------------------------------------------------- @dataclass class DashboardConfig: """Configuration for dashboard generation.""" title: str = "SEO Reporting Dashboard" domain: str = "" date_range: str = "" theme: str = "light" chart_options: dict[str, Any] = field(default_factory=dict) # --------------------------------------------------------------------------- # HTML template # --------------------------------------------------------------------------- DASHBOARD_TEMPLATE = """ {{ title }} - {{ domain }}

{{ title }}

{{ domain }} | {{ report_date }} | Audit ID: {{ audit_id }}
{{ overall_health }}
Overall Health Score
{{ trend_label }}

Category Scores

Health Score Timeline

Issue Distribution

{% if has_competitor_data %}

Competitive Comparison

{% endif %}

Top Issues ({{ issues_count }})

    {% for issue in top_issues %}
  • {{ issue.severity }} {{ issue.description }} ({{ issue.category }})
  • {% endfor %}
{% if top_wins %}

Top Wins ({{ wins_count }})

    {% for win in top_wins %}
  • WIN {{ win.description }} ({{ win.category }})
  • {% endfor %}
{% endif %}

Audit History

{% for entry in timeline %} {% endfor %}
Date Skill Category Score Issues
{{ entry.date }} {{ entry.skill }} {{ entry.category }} {{ entry.health_score }} {{ entry.issues_count }}
""" # --------------------------------------------------------------------------- # Generator # --------------------------------------------------------------------------- CATEGORY_KOREAN_LABELS: dict[str, str] = { "technical": "기술 SEO", "on_page": "온페이지", "performance": "성능", "content": "콘텐츠", "links": "링크", "local": "로컬 SEO", "keywords": "키워드", "competitor": "경쟁사", "schema": "스키마", "kpi": "KPI", "comprehensive": "종합 감사", "search_console": "서치 콘솔", "ecommerce": "이커머스", "international": "국제 SEO", "ai_search": "AI 검색", "entity_seo": "엔티티 SEO", "migration": "사이트 이전", } class DashboardGenerator: """Generate an interactive HTML dashboard from aggregated SEO report data.""" def __init__(self): self.template = Template(DASHBOARD_TEMPLATE) @staticmethod def _score_class(score: float) -> str: """Return CSS class based on health score.""" if score >= 90: return "score-excellent" elif score >= 75: return "score-good" elif score >= 60: return "score-average" elif score >= 40: return "score-poor" else: return "score-critical" @staticmethod def _gauge_color(score: float) -> str: """Return color hex for gauge chart.""" if score >= 90: return "#198754" elif score >= 75: return "#20c997" elif score >= 60: return "#ffc107" elif score >= 40: return "#fd7e14" else: return "#dc3545" @staticmethod def _category_color(score: float) -> str: """Return color for category bar based on score.""" if score >= 80: return "#198754" elif score >= 60: return "#0d6efd" elif score >= 40: return "#ffc107" else: return "#dc3545" @staticmethod def _trend_label(trend: str) -> str: """Return human-readable trend label in Korean.""" labels = { "improving": "개선 중 ↑", "stable": "안정 →", "declining": "하락 중 ↓", } return labels.get(trend, trend.title()) def generate_health_gauge(self, score: float) -> dict[str, Any]: """Generate gauge chart data for health score.""" return { "score": score, "remainder": 100 - score, "color": self._gauge_color(score), "class": self._score_class(score), } def generate_traffic_chart(self, traffic_data: list[dict]) -> dict[str, Any]: """Generate line chart data for traffic trends.""" dates = [d.get("date", "") for d in traffic_data] values = [d.get("traffic", 0) for d in traffic_data] return {"labels": dates, "values": values} def generate_keyword_chart(self, keyword_data: list[dict]) -> dict[str, Any]: """Generate bar chart data for keyword ranking distribution.""" labels = [d.get("range", "") for d in keyword_data] values = [d.get("count", 0) for d in keyword_data] return {"labels": labels, "values": values} def generate_issues_chart( self, issues_data: list[dict[str, Any]] ) -> dict[str, Any]: """Generate pie chart data for issue category distribution.""" category_counts: dict[str, int] = {} for issue in issues_data: cat = issue.get("category", "other") category_counts[cat] = category_counts.get(cat, 0) + 1 sorted_cats = sorted( category_counts.items(), key=lambda x: x[1], reverse=True ) return { "labels": [CATEGORY_KOREAN_LABELS.get(c[0], c[0]) for c in sorted_cats], "values": [c[1] for c in sorted_cats], } def generate_competitor_radar( self, competitor_data: dict[str, Any] ) -> dict[str, Any]: """Generate radar chart data for competitor comparison.""" labels = list(competitor_data.get("dimensions", [])) datasets = [] colors = [ "rgba(13, 110, 253, 0.5)", "rgba(220, 53, 69, 0.5)", "rgba(25, 135, 84, 0.5)", ] border_colors = ["#0d6efd", "#dc3545", "#198754"] for i, (domain, scores) in enumerate( competitor_data.get("scores", {}).items() ): datasets.append({ "label": domain, "data": [scores.get(dim, 0) for dim in labels], "backgroundColor": colors[i % len(colors)], "borderColor": border_colors[i % len(border_colors)], "borderWidth": 2, }) return {"labels": labels, "datasets": datasets} def render_html( self, report: dict[str, Any], config: DashboardConfig, ) -> str: """Render the full HTML dashboard from aggregated report data.""" overall_health = report.get("overall_health", 0) health_trend = report.get("health_trend", "stable") # Category scores (with Korean labels) cat_scores = report.get("category_scores", {}) category_labels = [ CATEGORY_KOREAN_LABELS.get(k, k) for k in cat_scores.keys() ] category_values = list(cat_scores.values()) category_colors = [self._category_color(v) for v in category_values] # Timeline timeline = report.get("timeline", []) timeline_dates = [e.get("date", "") for e in timeline] timeline_scores = [e.get("health_score", 0) for e in timeline] # Issues top_issues = report.get("top_issues", []) issues_chart = self.generate_issues_chart(top_issues) # Wins top_wins = report.get("top_wins", []) # Competitor radar has_competitor_data = False radar_labels: list[str] = [] radar_datasets: list[dict] = [] raw_outputs = report.get("raw_outputs", []) for output in raw_outputs: if output.get("category") == "competitor": has_competitor_data = True comp_data = output.get("data", {}) if "comparison_matrix" in comp_data: radar_result = self.generate_competitor_radar( comp_data["comparison_matrix"] ) radar_labels = radar_result["labels"] radar_datasets = radar_result["datasets"] break context = { "title": config.title, "domain": config.domain or report.get("domain", ""), "report_date": report.get("report_date", ""), "audit_id": report.get("audit_id", ""), "timestamp": report.get("timestamp", datetime.now().isoformat()), "overall_health": overall_health, "score_class": self._score_class(overall_health), "health_trend": health_trend, "trend_label": self._trend_label(health_trend), "gauge_color": self._gauge_color(overall_health), "category_labels": category_labels, "category_values": category_values, "category_colors": category_colors, "timeline_dates": timeline_dates, "timeline_scores": timeline_scores, "issue_category_labels": issues_chart["labels"], "issue_category_values": issues_chart["values"], "top_issues": top_issues[:15], "issues_count": len(top_issues), "top_wins": top_wins[:10], "wins_count": len(top_wins), "timeline": timeline[:20], "has_competitor_data": has_competitor_data, "radar_labels": radar_labels, "radar_datasets": radar_datasets, } return self.template.render(**context) def save(self, html_content: str, output_path: str) -> None: """Save rendered HTML to a file.""" Path(output_path).write_text(html_content, encoding="utf-8") logger.info(f"Dashboard saved to {output_path}") def run( self, report_json: str, output_path: str, title: str = "SEO Reporting Dashboard", ) -> str: """Orchestrate dashboard generation from a report JSON file.""" # Load report data report_path = Path(report_json) if not report_path.exists(): raise FileNotFoundError(f"Report file not found: {report_json}") report = json.loads(report_path.read_text(encoding="utf-8")) logger.info(f"Loaded report: {report.get('domain', 'unknown')}") # Configure config = DashboardConfig( title=title, domain=report.get("domain", ""), date_range=report.get("report_date", ""), ) # Render html = self.render_html(report, config) logger.info(f"Rendered HTML dashboard ({len(html):,} bytes)") # Save self.save(html, output_path) return output_path # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser( description="SEO Dashboard Generator - Interactive HTML dashboard with Chart.js", formatter_class=argparse.RawDescriptionHelpFormatter, epilog="""\ Examples: python dashboard_generator.py --report aggregated_report.json --output dashboard.html python dashboard_generator.py --report aggregated_report.json --output dashboard.html --title "My Dashboard" """, ) parser.add_argument( "--report", required=True, help="Path to aggregated report JSON file (from report_aggregator.py)", ) parser.add_argument( "--output", required=True, help="Output HTML file path", ) parser.add_argument( "--title", type=str, default="SEO Reporting Dashboard", help="Dashboard title (default: 'SEO Reporting Dashboard')", ) return parser.parse_args(argv) def main() -> None: args = parse_args() generator = DashboardGenerator() output = generator.run( report_json=args.report, output_path=args.output, title=args.title, ) logger.info(f"Dashboard generated: {output}") if __name__ == "__main__": main()