""" HTML Reporter — человекочитаемый отчёт для архитекторов и менеджеров. Формирует самодостаточный HTML-файл (без внешних зависимостей): - Сводная таблица по системе - Таблица модулей с цветовой индикацией риска - Детальные карточки по каждому модулю - Граф зависимостей в SVG (через библиотеку Mermaid.js из CDN) - Список циклических зависимостей """ import html import logging from datetime import datetime from pathlib import Path from typing import List from models import AnalysisReport, ModuleDecision, ModernizationStrategy, RiskLevel logger = logging.getLogger(__name__) # Цвета по уровням риска _RISK_COLORS = { RiskLevel.LOW: ("#22c55e", "#dcfce7"), # зелёный RiskLevel.MEDIUM: ("#f59e0b", "#fef3c7"), # жёлтый RiskLevel.HIGH: ("#ef4444", "#fee2e2"), # красный RiskLevel.CRITICAL: ("#7f1d1d", "#fca5a5"), # тёмно-красный } _STRATEGY_LABELS = { ModernizationStrategy.KEEP: "Оставить", ModernizationStrategy.REFACTOR: "Рефакторинг", ModernizationStrategy.REENGINEER: "Реинжиниринг", ModernizationStrategy.REPLACE: "Замена", } _RISK_LABELS = { RiskLevel.LOW: "Низкий", RiskLevel.MEDIUM: "Средний", RiskLevel.HIGH: "Высокий", RiskLevel.CRITICAL: "Критический", } class HtmlReporter: """Формирует HTML-отчёт по результатам анализа.""" def write(self, report: AnalysisReport, output_dir: str) -> str: """ Записывает отчёт в файл report.html внутри output_dir. :returns: путь к созданному файлу """ out = Path(output_dir) out.mkdir(parents=True, exist_ok=True) file_path = out / "report.html" content = self._build_html(report) with open(file_path, "w", encoding="utf-8") as fh: fh.write(content) logger.info("HTML отчёт записан: %s", file_path) return str(file_path) # ------------------------------------------------------------------ # Сборка HTML # ------------------------------------------------------------------ def _build_html(self, report: AnalysisReport) -> str: sections = [ self._head(), "", self._header(report), self._summary_section(report), self._decisions_table(report), self._cycles_section(report), self._modules_detail(report), self._footer(), "", ] return "\n".join(sections) def _head(self) -> str: return """ Legacy Analyzer — Отчёт """ def _header(self, report: AnalysisReport) -> str: ts = datetime.now().strftime("%d.%m.%Y %H:%M") risk_color = _RISK_COLORS[report.system_risk_level][0] return f"""

📊 Legacy Analyzer

Проект: {html.escape(report.project_root)}  |  Сгенерировано: {ts}  |  Системный риск: {_RISK_LABELS[report.system_risk_level]}

""" def _summary_section(self, report: AnalysisReport) -> str: strategy_label = _STRATEGY_LABELS[report.recommended_system_strategy] risk_color = _RISK_COLORS[report.system_risk_level][0] return f"""

Сводка по системе

{report.total_files}
Файлов
{report.total_nloc:,}
Строк кода (NLOC)
{report.total_functions}
Функций
{report.avg_system_ccn:.1f}
Средняя CCN
{len(report.modules)}
Модулей
{len(report.dependency_cycles)}
Циклов зависимостей
{_RISK_LABELS[report.system_risk_level]}
Системный риск
{strategy_label}
Рекомендуемая стратегия
""" def _decisions_table(self, report: AnalysisReport) -> str: rows = [] for d in report.decisions: fg, bg = _RISK_COLORS[d.risk_level] risk_badge = ( f'' f'{_RISK_LABELS[d.risk_level]}' ) reasons_html = "" if d.reasons: items = "".join(f"
  • {html.escape(r)}
  • " for r in d.reasons) reasons_html = f'' rows.append(f""" {html.escape(d.module_name)} {risk_badge} {html.escape(_STRATEGY_LABELS[d.strategy])} {d.priority} {reasons_html or '—'} """) return f"""

    Таблица решений по модулям

    {"".join(rows)}
    МодульРискСтратегияПриоритетПричины
    """ def _cycles_section(self, report: AnalysisReport) -> str: if not report.dependency_cycles: return """

    Циклические зависимости

    ✓ Циклических зависимостей не обнаружено

    """ items = [] for i, cycle in enumerate(report.dependency_cycles, 1): badges = "".join(f'{html.escape(m)}' for m in cycle) items.append(f"

    Цикл {i}: {badges}

    ") return f"""

    Циклические зависимости ({len(report.dependency_cycles)})

    {"".join(items)}
    """ def _modules_detail(self, report: AnalysisReport) -> str: # Строим индекс decision по имени модуля decision_map = {d.module_name: d for d in report.decisions} cards = [] for mod_name, mod in sorted(report.modules.items()): decision = decision_map.get(mod_name) risk = decision.risk_level if decision else RiskLevel.LOW fg, bg = _RISK_COLORS[risk] # Топ-5 самых сложных функций top_funcs = sorted(mod.functions, key=lambda f: f.ccn, reverse=True)[:5] func_rows = "".join( f"{html.escape(f.name)}" f"{f.ccn}{f.nloc}{f.params}" f"{html.escape(Path(f.file).name)}:{f.line}" for f in top_funcs ) func_table = "" if func_rows: func_table = f"""
    Топ функций по сложности {func_rows}
    ФункцияCCNNLOCПараметрыРасположение
    """ cycle_warn = ( '⚠ Участвует в цикле зависимостей' if mod.has_cycles else "" ) cards.append(f"""

    {html.escape(mod_name)}

    {_RISK_LABELS[risk]}
    {cycle_warn}
    Язык{mod.language}
    NLOC{mod.total_nloc:,}
    Функций{mod.total_functions}
    Avg CCN{mod.avg_ccn:.1f}
    Max CCN{mod.max_ccn}
    C_out / C_in{mod.coupling_out} / {mod.coupling_in}
    Нестабильность (I){mod.instability:.2f}
    {func_table}
    """) return f"""

    Детализация по модулям

    {"".join(cards)}""" def _footer(self) -> str: return """

    Legacy Analyzer — методический инструментарий модернизации унаследованных систем

    """