import { writeFile, mkdir, readFile } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { generatePerSpecExecutiveReport, generatePerSpecTechnicalReport, } from './doc-generator/per-spec-report/index.js';
import { buildSpecMetricsSection, buildEffortDistributionSvg, buildSpecDependenciesSection, buildSpecTagsSection, } from './doc-generator/per-spec-report/spec-section-builders.js';
import { extractPerSpecData } from './doc-generator/per-spec-report/spec-data-extractor.js';
import { markdownToHtml } from './markdown-renderer.js';
const STATUS_COLORS = {
    draft: { bg: '#e5e7eb', fg: '#374151' },
    review: { bg: '#dbeafe', fg: '#1d4ed8' },
    approved: { bg: '#d1fae5', fg: '#065f46' },
    implementing: { bg: '#fef3c7', fg: '#92400e' },
    done: { bg: '#d1fae5', fg: '#065f46' },
};
const RISK_ICONS = { low: '🟢', medium: '🟡', high: '🔴' };
function escapeHtml(text) {
    return text
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;');
}
function statusBadge(status) {
    const colors = STATUS_COLORS[status] ?? { bg: '#e5e7eb', fg: '#374151' };
    return `<span class="badge" data-i18n-status="${escapeHtml(status)}" style="background:${colors.bg};color:${colors.fg}">${escapeHtml(status)}</span>`;
}
function specNumericId(spec) {
    const m = /SPEC-(\d+)/.exec(spec.id);
    return m?.[1] ? parseInt(m[1], 10) : 0;
}
function sortSpecs(specs) {
    return [...specs].sort((a, b) => specNumericId(a) - specNumericId(b));
}
function specFolder(spec) {
    if (!spec.specPath) {
        return '';
    }
    const parts = spec.specPath.split('/');
    return parts.length >= 2 ? (parts[parts.length - 2] ?? '') : '';
}
function renderSpecRow(spec) {
    const risk = RISK_ICONS[spec.risk] ?? '⚪';
    const tags = spec.tags.length > 0
        ? spec.tags.map((t) => `<span class="tag">${escapeHtml(t)}</span>`).join('')
        : '';
    const folder = specFolder(spec);
    const titleHtml = folder
        ? `<a href="specs/${escapeHtml(folder)}/executive-report.html" class="spec-link">${escapeHtml(spec.id)}: ${escapeHtml(spec.title)}</a>`
        : `<span class="spec-title">${escapeHtml(spec.id)}: ${escapeHtml(spec.title)}</span>`;
    const reportsHtml = folder
        ? `<a href="specs/${escapeHtml(folder)}/executive-report.html" class="report-link" title="Executive Report">📊</a> <a href="specs/${escapeHtml(folder)}/technical-report.html" class="report-link" title="Technical Report">🔧</a>`
        : '';
    return `<tr data-status="${escapeHtml(spec.status)}" data-search="${escapeHtml(`${spec.id} ${spec.title} ${spec.tags.join(' ')} ${spec.type} ${spec.status}`.toLowerCase())}">
  <td>${titleHtml}</td>
  <td>${statusBadge(spec.status)}</td>
  <td><span class="type-badge">${escapeHtml(spec.type)}</span></td>
  <td>${risk} <span data-i18n-risk="${escapeHtml(spec.risk)}">${escapeHtml(spec.risk)}</span></td>
  <td class="num">${String(spec.estimation.devHours)}h</td>
  <td class="num">$${String(spec.estimation.totalCostUsd)}</td>
  <td class="tags-cell">${tags}</td>
  <td class="reports-cell">${reportsHtml}</td>
</tr>`;
}
function renderMetricCard(labelEn, labelEs, value, color) {
    return `<div class="metric-card">
  <div class="metric-value" style="color:${color}">${value}</div>
  <div class="metric-label" data-i18n-en="${escapeHtml(labelEn)}" data-i18n-es="${escapeHtml(labelEs)}">${escapeHtml(labelEn)}</div>
</div>`;
}
function renderProgressBar(done, total) {
    const pct = total > 0 ? Math.round((done / total) * 100) : 0;
    const color = pct >= 80 ? '#22c55e' : pct >= 40 ? '#f59e0b' : '#ef4444';
    return `<div class="progress-bar">
  <div class="progress-fill" style="width:${String(pct)}%;background:${color}"></div>
  <span class="progress-text">${String(pct)}% <span data-i18n-en="complete" data-i18n-es="completado">complete</span></span>
</div>`;
}
function getInlineCss() {
    return `*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f8fafc; color: #1f2937; line-height: 1.5; max-width: 1280px; margin: 0 auto; padding: 24px; }
  .header-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
  h1 { font-size: 1.5rem; color: #111827; }
  .subtitle { color: #6b7280; font-size: 0.85rem; margin-bottom: 20px; }
  .lang-toggle { display: flex; gap: 4px; }
  .lang-btn { padding: 4px 10px; border: 1px solid #d1d5db; border-radius: 4px; background: #fff; cursor: pointer; font-size: 0.75rem; font-weight: 600; color: #6b7280; transition: all 0.15s; }
  .lang-btn.active { background: #4F46E5; color: #fff; border-color: #4F46E5; }
  .metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 12px; margin-bottom: 20px; }
  .metric-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 14px; text-align: center; }
  .metric-value { font-size: 1.6rem; font-weight: 700; line-height: 1.2; }
  .metric-label { font-size: 0.72rem; color: #6b7280; margin-top: 4px; text-transform: uppercase; letter-spacing: 0.05em; }
  .progress-bar { background: #e5e7eb; border-radius: 99px; height: 20px; position: relative; overflow: hidden; margin-bottom: 20px; }
  .progress-fill { height: 100%; border-radius: 99px; transition: width 0.3s; }
  .progress-text { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); font-size: 0.7rem; font-weight: 600; color: #374151; }
  .toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 12px; }
  .search-input { flex: 1; min-width: 200px; padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.85rem; outline: none; }
  .search-input:focus { border-color: #4F46E5; box-shadow: 0 0 0 2px rgba(79,70,229,0.15); }
  .filter-btn { padding: 5px 12px; border: 1px solid #d1d5db; border-radius: 6px; background: #fff; cursor: pointer; font-size: 0.78rem; color: #374151; transition: all 0.15s; }
  .filter-btn:hover { border-color: #4F46E5; color: #4F46E5; }
  .filter-btn.active { background: #4F46E5; color: #fff; border-color: #4F46E5; }
  .table-wrap { overflow-x: auto; }
  table { width: 100%; border-collapse: collapse; font-size: 0.82rem; table-layout: fixed; }
  thead th { text-align: left; padding: 8px 10px; background: #f9fafb; border-bottom: 2px solid #e5e7eb; color: #6b7280; font-weight: 600; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; white-space: nowrap; }
  th:nth-child(1) { width: 38%; } th:nth-child(2) { width: 8%; } th:nth-child(3) { width: 7%; }
  th:nth-child(4) { width: 8%; } th:nth-child(5) { width: 6%; } th:nth-child(6) { width: 7%; }
  th:nth-child(7) { width: 18%; } th:nth-child(8) { width: 8%; }
  tbody td { padding: 8px 10px; border-bottom: 1px solid #f3f4f6; vertical-align: middle; overflow: hidden; text-overflow: ellipsis; }
  tbody tr:hover { background: #f9fafb; }
  tbody tr.hidden { display: none; }
  .badge { display: inline-block; padding: 2px 10px; border-radius: 99px; font-size: 0.72rem; font-weight: 600; white-space: nowrap; text-transform: capitalize; }
  .type-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; background: #f3f4f6; color: #6b7280; font-size: 0.72rem; }
  .tag { display: inline-block; padding: 1px 6px; border-radius: 3px; background: #ede9fe; color: #5b21b6; font-size: 0.65rem; margin: 1px 2px; }
  .tags-cell { max-width: 200px; overflow: hidden; }
  .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
  .spec-link { color: #4F46E5; text-decoration: none; font-weight: 500; }
  .spec-link:hover { text-decoration: underline; }
  .spec-title { font-weight: 500; }
  .report-link { text-decoration: none; font-size: 1rem; padding: 2px 3px; border-radius: 4px; transition: background 0.15s; }
  .report-link:hover { background: #f3f4f6; }
  .reports-cell { white-space: nowrap; text-align: center; }
  .footer { margin-top: 24px; padding-top: 12px; border-top: 1px solid #e5e7eb; color: #9ca3af; font-size: 0.72rem; text-align: center; }
  .footer a { color: #4F46E5; text-decoration: none; }
  .empty { text-align: center; padding: 48px 20px; color: #9ca3af; }
  @media print { body { padding: 0; max-width: 100%; } .toolbar, .lang-toggle { display: none; } }
  @media (max-width: 640px) { .metrics { grid-template-columns: repeat(2, 1fr); } .tags-cell { display: none; } th:nth-child(7), td:nth-child(7) { display: none; } }`;
}
function getInlineScript() {
    return `(function() {
  var rows = document.querySelectorAll('#specs-body tr');
  var search = document.getElementById('search');
  var filters = document.querySelectorAll('.filter-btn');
  var langBtns = document.querySelectorAll('.lang-btn');
  var activeFilter = 'all';

  // i18n dictionaries
  var i18n = {
    en: {
      status: { draft: 'Draft', review: 'Review', approved: 'Approved', implementing: 'Building', done: 'Done' },
      risk: { low: 'low', medium: 'medium', high: 'high' },
      filter: { all: 'All', implementing: 'Building', approved: 'Approved', review: 'Review', draft: 'Draft', done: 'Done' }
    },
    es: {
      status: { draft: 'Borrador', review: 'Revisión', approved: 'Aprobado', implementing: 'En progreso', done: 'Hecho' },
      risk: { low: 'bajo', medium: 'medio', high: 'alto' },
      filter: { all: 'Todos', implementing: 'En progreso', approved: 'Aprobados', review: 'Revisión', draft: 'Borrador', done: 'Hechos' }
    }
  };

  function applyFilters() {
    var q = (search ? search.value : '').toLowerCase();
    rows.forEach(function(row) {
      var matchFilter = activeFilter === 'all' || row.dataset.status === activeFilter;
      var matchSearch = !q || (row.dataset.search || '').includes(q);
      row.classList.toggle('hidden', !(matchFilter && matchSearch));
    });
  }

  function setLang(lang) {
    var dict = i18n[lang] || i18n.en;
    // Status badges
    document.querySelectorAll('[data-i18n-status]').forEach(function(el) {
      var key = el.dataset.i18nStatus;
      if (dict.status[key]) el.textContent = dict.status[key];
    });
    // Risk labels
    document.querySelectorAll('[data-i18n-risk]').forEach(function(el) {
      var key = el.dataset.i18nRisk;
      if (dict.risk[key]) el.textContent = dict.risk[key];
    });
    // Filter buttons
    document.querySelectorAll('.filter-btn[data-filter]').forEach(function(el) {
      var key = el.dataset.filter;
      if (dict.filter[key]) el.textContent = dict.filter[key];
    });
    // Generic i18n labels
    document.querySelectorAll('[data-i18n-' + lang + ']').forEach(function(el) {
      el.textContent = el.getAttribute('data-i18n-' + lang);
    });
    // Table headers
    var thMap = lang === 'es'
      ? { Spec: 'Spec', Status: 'Estado', Type: 'Tipo', Risk: 'Riesgo', Hours: 'Horas', Cost: 'Costo', Tags: 'Etiquetas', Reports: 'Informes' }
      : { Spec: 'Spec', Estado: 'Status', Tipo: 'Type', Riesgo: 'Risk', Horas: 'Hours', Costo: 'Cost', Etiquetas: 'Tags', Informes: 'Reports' };
    document.querySelectorAll('thead th').forEach(function(th) {
      if (thMap[th.textContent]) th.textContent = thMap[th.textContent];
    });
    // Search placeholder
    if (search) search.placeholder = lang === 'es' ? 'Buscar specs\\u2026' : 'Search specs\\u2026';
    // Lang button state
    langBtns.forEach(function(b) { b.classList.toggle('active', b.dataset.lang === lang); });
    // Store preference
    try { localStorage.setItem('planu-lang', lang); } catch(e) {}
  }

  if (search) search.addEventListener('input', applyFilters);
  filters.forEach(function(btn) {
    btn.addEventListener('click', function() {
      filters.forEach(function(b) { b.classList.remove('active'); });
      btn.classList.add('active');
      activeFilter = btn.dataset.filter || 'all';
      applyFilters();
    });
  });
  langBtns.forEach(function(btn) {
    btn.addEventListener('click', function() { setLang(btn.dataset.lang); });
  });

  // Auto-detect language
  var saved = null;
  try { saved = localStorage.getItem('planu-lang'); } catch(e) {}
  var autoLang = saved || (navigator.language.startsWith('es') ? 'es' : 'en');
  setLang(autoLang);
})();`;
}
function renderBody(specs, generatedAt) {
    const statusCounts = {};
    for (const s of specs) {
        statusCounts[s.status] = (statusCounts[s.status] ?? 0) + 1;
    }
    const totalSpecs = specs.length;
    const doneCount = statusCounts.done ?? 0;
    const implementingCount = statusCounts.implementing ?? 0;
    const totalDevHours = specs.reduce((s, sp) => s + sp.estimation.devHours, 0);
    const totalCost = specs.reduce((s, sp) => s + sp.estimation.totalCostUsd, 0);
    const sortedSpecs = sortSpecs(specs);
    const rows = sortedSpecs.map((s) => renderSpecRow(s)).join('\n');
    const metricsHtml = `<div class="metrics">
  ${renderMetricCard('Specs', 'Specs', String(totalSpecs), '#4F46E5')}
  ${renderMetricCard('Done', 'Hechos', String(doneCount), '#22c55e')}
  ${renderMetricCard('Building', 'En progreso', String(implementingCount), '#f59e0b')}
  ${renderMetricCard('Dev Hours', 'Horas Dev', String(Math.round(totalDevHours * 10) / 10), '#6366f1')}
  ${renderMetricCard('Est. Cost', 'Costo Est.', '$' + String(Math.round(totalCost)), '#4F46E5')}
</div>`;
    const tableHtml = totalSpecs === 0
        ? '<div class="empty"><p data-i18n-en="No specs yet. Create one with create_spec." data-i18n-es="No hay specs aún. Crea uno con create_spec.">No specs yet. Create one with <code>create_spec</code>.</p></div>'
        : `<div class="toolbar">
  <input type="text" class="search-input" placeholder="Search specs…" id="search">
  <button class="filter-btn active" data-filter="all">All</button>
  <button class="filter-btn" data-filter="implementing">Building</button>
  <button class="filter-btn" data-filter="approved">Approved</button>
  <button class="filter-btn" data-filter="review">Review</button>
  <button class="filter-btn" data-filter="draft">Draft</button>
  <button class="filter-btn" data-filter="done">Done</button>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>Spec</th><th>Status</th><th>Type</th><th>Risk</th><th>Hours</th><th>Cost</th><th>Tags</th><th>Reports</th></tr></thead>
<tbody id="specs-body">${rows}</tbody>
</table>
</div>`;
    return `<div class="header-row">
  <h1>📋 Planu — Spec Dashboard</h1>
  <div class="lang-toggle">
    <button class="lang-btn" data-lang="en">EN</button>
    <button class="lang-btn" data-lang="es">ES</button>
  </div>
</div>
<p class="subtitle" data-i18n-en="Auto-generated by Planu · ${escapeHtml(generatedAt)}" data-i18n-es="Generado automáticamente por Planu · ${escapeHtml(generatedAt)}">Auto-generated by Planu · ${escapeHtml(generatedAt)}</p>
${metricsHtml}
${renderProgressBar(doneCount, totalSpecs)}
${tableHtml}
<div class="footer">Generated by <a href="https://planu.dev">Planu</a> — Spec Driven Development</div>`;
}
function generateHtml(specs) {
    const generatedAt = new Date().toLocaleString('en-US', {
        dateStyle: 'medium',
        timeStyle: 'short',
    });
    return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Planu — Spec Dashboard</title>
<style>${getInlineCss()}</style>
</head>
<body>
${renderBody(specs, generatedAt)}
<script>${getInlineScript()}</script>
</body>
</html>`;
}
const REPORT_CSS = `
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 900px; margin: 0 auto; padding: 24px; color: #1f2937; background: #fff; line-height: 1.6; font-size: 14px; }
  .header { border-bottom: 3px solid #4F46E5; padding-bottom: 16px; margin-bottom: 24px; }
  .header h1 { font-size: 1.1em; color: #6b7280; font-weight: 400; margin: 0 0 4px; }
  .header h2 { font-size: 1.6em; color: #111827; margin: 0; }
  .header .meta { margin-top: 8px; font-size: 0.85em; color: #6b7280; }
  .badge { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 0.78em; font-weight: 600; }
  .nav { margin-bottom: 24px; }
  .nav a { color: #4F46E5; text-decoration: none; font-size: 0.85em; }
  .nav a:hover { text-decoration: underline; }
  .section { margin-bottom: 28px; }
  .section h2 { font-size: 1.15em; color: #4F46E5; border-left: 4px solid #4F46E5; padding-left: 10px; margin-bottom: 14px; }
  .metric-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin-bottom: 16px; }
  .metric-card { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 14px; text-align: center; }
  .metric-value { font-size: 1.8em; font-weight: 700; color: #4F46E5; line-height: 1; }
  .metric-label { font-size: 0.78em; color: #6b7280; margin-top: 4px; }
  .tags-section .tag { display: inline-block; padding: 2px 8px; border-radius: 4px; background: #ede9fe; color: #5b21b6; font-size: 0.8em; margin: 2px 4px; }
  .md-content h1 { font-size: 1.4em; color: #111827; border-bottom: 2px solid #e5e7eb; padding-bottom: 8px; margin: 24px 0 12px; }
  .md-content h2 { font-size: 1.2em; color: #4F46E5; border-left: 4px solid #4F46E5; padding-left: 10px; margin: 20px 0 10px; }
  .md-content h3 { font-size: 1.05em; color: #374151; margin: 16px 0 8px; }
  .md-content p { margin: 8px 0; }
  .md-content ul, .md-content ol { padding-left: 24px; margin: 8px 0; }
  .md-content li { margin: 4px 0; }
  .md-content blockquote { border-left: 4px solid #d1d5db; padding: 8px 16px; margin: 12px 0; color: #6b7280; background: #f9fafb; border-radius: 0 6px 6px 0; }
  .md-content code { background: #f3f4f6; padding: 1px 5px; border-radius: 3px; font-size: 0.9em; }
  .md-content pre { background: #1f2937; color: #e5e7eb; padding: 16px; border-radius: 8px; overflow-x: auto; margin: 12px 0; }
  .md-content pre code { background: none; color: inherit; padding: 0; }
  .md-table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 0.85em; }
  .md-table th { text-align: left; padding: 8px 10px; background: #f9fafb; border-bottom: 2px solid #e5e7eb; font-weight: 600; color: #374151; }
  .md-table td { padding: 8px 10px; border-bottom: 1px solid #f3f4f6; }
  .md-table tr:hover { background: #f9fafb; }
  .md-content hr { border: none; border-top: 1px solid #e5e7eb; margin: 20px 0; }
  .md-content input[type=checkbox] { margin-right: 6px; }
  .footer { margin-top: 32px; padding-top: 16px; border-top: 1px solid #e5e7eb; font-size: 0.78em; color: #9ca3af; text-align: center; }
  .footer a { color: #4F46E5; text-decoration: none; }
  @media print { body { padding: 0; } .nav { display: none; } }`;
function buildMetricsDashboard(spec) {
    const data = extractPerSpecData(spec, [], []);
    return [
        buildSpecMetricsSection(data),
        buildEffortDistributionSvg(data.devHours, data.reviewHours),
        buildSpecDependenciesSection(data),
        buildSpecTagsSection(data),
    ]
        .filter(Boolean)
        .join('\n');
}
function buildReportHtml(spec, title, mdContent) {
    const colors = STATUS_COLORS[spec.status] ?? { bg: '#e5e7eb', fg: '#374151' };
    const date = new Date().toLocaleDateString('en-US', {
        year: 'numeric',
        month: 'long',
        day: 'numeric',
    });
    const bodyHtml = markdownToHtml(mdContent);
    const dashboard = buildMetricsDashboard(spec);
    return `<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${escapeHtml(title)}</title>
<style>${REPORT_CSS}</style></head>
<body>
<div class="nav"><a href="../../index.html">&larr; Back to Dashboard</a></div>
<div class="header">
  <h1>Planu — Spec Report</h1>
  <h2>${escapeHtml(spec.id)}: ${escapeHtml(spec.title)}</h2>
  <div class="meta">
    <span class="badge" style="background:${colors.bg};color:${colors.fg}">${escapeHtml(spec.status)}</span>
    &middot; Generated ${escapeHtml(date)}
  </div>
</div>
${dashboard}
<div class="md-content">${bodyHtml}</div>
<div class="footer">Generated by <strong>Planu</strong> — Spec Driven Development</div>
</body></html>`;
}
async function readMdSafe(path) {
    try {
        return await readFile(path, 'utf-8');
    }
    catch {
        return '';
    }
}
/**
 * Regenerate executive-report.html and technical-report.html for each spec.
 * Reads actual spec.md and technical.md content to produce rich HTML reports.
 * Also generates structural reports as fallback data source.
 * Best-effort per spec — one failure doesn't block others.
 */
async function regeneratePerSpecReports(specs) {
    await Promise.all(specs.map(async (spec) => {
        if (!spec.specPath) {
            return;
        }
        const specDir = dirname(spec.specPath);
        try {
            const [specMd, techMd] = await Promise.all([
                readMdSafe(spec.specPath),
                readMdSafe(spec.technicalPath),
            ]);
            const execHtml = specMd
                ? buildReportHtml(spec, `Executive Report — ${spec.id}: ${spec.title}`, specMd)
                : generatePerSpecExecutiveReport(spec, [], []).content;
            const techHtml = techMd
                ? buildReportHtml(spec, `Technical Report — ${spec.id}: ${spec.title}`, techMd)
                : generatePerSpecTechnicalReport(spec, [], [], []).content;
            await Promise.all([
                writeFile(join(specDir, 'executive-report.html'), execHtml, 'utf-8'),
                writeFile(join(specDir, 'technical-report.html'), techHtml, 'utf-8'),
            ]);
        }
        catch {
            /* best-effort per spec */
        }
    }));
}
/**
 * Generate planu/index.html with specs overview + regenerate per-spec reports.
 * Designed to be called from list_specs, update_status, create_spec.
 * Best-effort: never throws, never blocks the caller.
 */
export async function regenerateSpecSummaryHtml(projectPath, specs) {
    const outputPath = join(projectPath, 'planu/index.html');
    try {
        await mkdir(dirname(outputPath), { recursive: true });
        await writeFile(outputPath, generateHtml(specs), 'utf-8');
        await regeneratePerSpecReports(specs);
    }
    catch {
        /* best-effort — never fail the caller */
    }
}
//# sourceMappingURL=spec-summary-html.js.map