"use strict";

const state = {
  username: null,
  view: "dashboard",
  bands: { page: 1, pageSize: 50, q: "", location_q: "", country: "", status: "", genre: "", enriched: "", has_lat: "", has_location: "", sort: "ma_id", dir: "asc", total: 0 },
  runs: { page: 1, pageSize: 50, run_type: "", status: "", total: 0 },
  logs: { page: 1, pageSize: 100, level: "", autoRefresh: true },
  audit: { page: 1, pageSize: 50 },
  logsTimer: null,
  queueTimer: null,
  geoTimer: null,
  cmdLogTimer: null,
  monitorTimer: null,
  monitorLogs: [],
  monitorLastId: 0,
  monitorPaused: false,
  monitorInterval: 5000,
};

// ------------------------------------------------------------------
// API helper
// ------------------------------------------------------------------
async function api(path, opts = {}) {
  const res = await fetch(path, {
    credentials: "include",
    headers: { "Content-Type": "application/json", ...(opts.headers || {}) },
    ...opts,
  });
  if (res.status === 401) {
    showLogin();
    throw new Error("unauthorized");
  }
  const body = await res.json().catch(() => ({}));
  if (!res.ok || body.ok === false) {
    throw new Error(body.error || `HTTP ${res.status}`);
  }
  return body;
}

// ------------------------------------------------------------------
// Auth
// ------------------------------------------------------------------
function showLogin() {
  document.getElementById("login-screen").classList.remove("hidden");
  document.getElementById("app").classList.add("hidden");
  if (state.logsTimer) { clearInterval(state.logsTimer); state.logsTimer = null; }
  if (state.queueTimer) { clearInterval(state.queueTimer); state.queueTimer = null; }
  if (state.geoTimer) { clearInterval(state.geoTimer); state.geoTimer = null; }
  if (state.cmdLogTimer) { clearInterval(state.cmdLogTimer); state.cmdLogTimer = null; }
  if (state.monitorTimer) { clearInterval(state.monitorTimer); state.monitorTimer = null; }
}

function showApp() {
  document.getElementById("login-screen").classList.add("hidden");
  document.getElementById("app").classList.remove("hidden");
  document.getElementById("whoami").textContent = state.username || "";
}

async function checkSession() {
  try {
    const r = await api("/admin/auth/me");
    state.username = r.username;
    showApp();
    router();
  } catch {
    showLogin();
  }
}

document.getElementById("login-form").addEventListener("submit", async (e) => {
  e.preventDefault();
  const username = document.getElementById("login-username").value.trim();
  const password = document.getElementById("login-password").value;
  const errEl = document.getElementById("login-error");
  errEl.textContent = "";
  try {
    const res = await fetch("/admin/auth/login", {
      method: "POST",
      credentials: "include",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ username, password }),
    });
    const body = await res.json().catch(() => ({}));
    if (!res.ok || body.ok === false) {
      errEl.textContent = body.error || "Échec de connexion";
      return;
    }
    state.username = body.username;
    document.getElementById("login-password").value = "";
    showApp();
    router();
  } catch {
    errEl.textContent = "Erreur réseau";
  }
});

document.getElementById("logout-btn").addEventListener("click", async () => {
  try { await fetch("/admin/auth/logout", { method: "POST", credentials: "include" }); } catch {}
  state.username = null;
  showLogin();
});

// ------------------------------------------------------------------
// Router
// ------------------------------------------------------------------
const VIEWS = ["dashboard", "bands", "queue", "runs", "monitor", "logs", "geocoding", "llm", "conflicts", "jobs", "checkpoints", "audit"];

function router() {
  const hash = (location.hash || "#/dashboard").replace("#/", "");
  const view = VIEWS.includes(hash) ? hash : "dashboard";
  state.view = view;
  document.querySelectorAll(".nav a").forEach((a) => {
    a.classList.toggle("active", a.dataset.view === view);
  });
  if (state.logsTimer) { clearInterval(state.logsTimer); state.logsTimer = null; }
  if (state.queueTimer) { clearInterval(state.queueTimer); state.queueTimer = null; }
  if (state.geoTimer) { clearInterval(state.geoTimer); state.geoTimer = null; }
  if (state.cmdLogTimer) { clearInterval(state.cmdLogTimer); state.cmdLogTimer = null; }
  if (state.monitorTimer) { clearInterval(state.monitorTimer); state.monitorTimer = null; }
  const renderers = {
    dashboard: renderDashboard,
    bands: renderBands,
    conflicts: renderConflicts,
    queue: renderQueue,
    runs: renderRuns,
    monitor: renderMonitor,
    logs: renderLogs,
    geocoding: renderGeocoding,
    llm: renderLLM,
    jobs: renderJobs,
    checkpoints: renderCheckpoints,
    audit: renderAudit,
  };
  renderers[view]();
}
window.addEventListener("hashchange", router);

function esc(s) {
  return String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
}
function fmtDate(s) {
  if (!s) return "—";
  const d = new Date(s);
  return d.toLocaleString("fr-FR", { day: "2-digit", month: "2-digit", year: "2-digit", hour: "2-digit", minute: "2-digit" });
}
const content = () => document.getElementById("content");

// ------------------------------------------------------------------
// Dashboard
// ------------------------------------------------------------------
// Découpe un genre complexe ("Doom/Death Metal; Gothic/Progressive") en mots-clés
function splitGenreElements(genreRows) {
  const counts = new Map();
  for (const { genre, total } of genreRows) {
    if (!genre) continue;
    // Retire les parenthèses et leur contenu (ex: "(early)", "(later)")
    const cleaned = genre.replace(/\([^)]*\)/g, "");
    // Découpe sur /  ;  ,
    const tokens = cleaned.split(/[\/;,]+/);
    for (const tok of tokens) {
      const kw = tok.trim();
      if (kw.length < 3) continue;
      counts.set(kw, (counts.get(kw) || 0) + total);
    }
  }
  return [...counts.entries()]
    .map(([kw, n]) => ({ kw, n }))
    .sort((a, b) => b.n - a.n);
}

async function renderDashboard() {
  content().innerHTML = `<div class="loading">Chargement…</div>`;
  try {
    const r = await api("/admin/api/stats");
    const t = r.totals;
    const maxStatus = Math.max(1, ...r.by_status.map((x) => x.total));
    const maxCountry = Math.max(1, ...r.by_country.map((x) => x.total));
    const elements = splitGenreElements(r.by_genre);
    const maxEl = Math.max(1, ...elements.map((x) => x.n));

    content().innerHTML = `
      <div class="grid grid-stats">
        ${statCard("Total bands", t.total)}
        ${statCard("Enrichis", t.enriched, "ok")}
        ${statCard("Non enrichis", t.not_enriched, "warn")}
        ${statCard("Géocodés", t.geocoded, "ok")}
        ${statCard("Sans localisation", t.not_geocoded, "warn")}
        ${statCard("Jamais crawlés (nouveau système)", t.total - t.crawled_by_current_system, "warn")}
        ${statCard("Jamais enrichis (band_page absent)", t.never_enriched, "err")}
        ${statCard("Stale (>30j)", t.stale, "warn")}
      </div>
      <div class="grid grid-2">
        <div class="card" style="max-height:420px;overflow-y:auto">
          <h2>Par statut</h2>
          ${r.by_status.map((x) => barRow(x.status, x.total, maxStatus)).join("") || emptyRow()}
        </div>
        <div class="card" style="max-height:420px;overflow-y:auto">
          <h2>Par pays (${r.by_country.length})</h2>
          ${r.by_country.map((x) => barRow(x.country, x.total, maxCountry)).join("") || emptyRow()}
        </div>
        <div class="card" style="max-height:500px;overflow-y:auto">
          <h2>Éléments de genre (${elements.length} mots-clés)</h2>
          <p style="font-size:11px;color:var(--muted);margin:0 0 10px">Chaque genre complexe (ex: "Doom/Death Metal; Gothic") est découpé en mots-clés.</p>
          ${elements.map((x) => barRow(x.kw, x.n, maxEl)).join("") || emptyRow()}
        </div>
      </div>
    `;
  } catch (e) {
    content().innerHTML = `<div class="empty">Erreur : ${esc(e.message)}</div>`;
  }
}

function statCard(label, value, cls = "") {
  return `<div class="card stat-card"><div class="label">${esc(label)}</div><div class="value ${cls}">${Number(value).toLocaleString("fr-FR")}</div></div>`;
}
function barRow(label, count, max) {
  const pct = Math.max(2, Math.round((count / max) * 100));
  return `<div class="bar-row"><div class="bar-label" title="${esc(label)}">${esc(label)}</div><div class="bar-track"><div class="bar-fill" style="width:${pct}%"></div></div><div class="bar-count">${count}</div></div>`;
}
function emptyRow() { return `<div class="empty">Aucune donnée</div>`; }

// ------------------------------------------------------------------
// Queue
// ------------------------------------------------------------------
async function renderQueue() {
  if (!document.getElementById("q-body")) {
    content().innerHTML = `
      <div class="grid grid-stats" id="q-stats"></div>
      <div class="card">
        <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px">
          <h2 style="margin:0">Derniers runs d'enrichissement</h2>
          <div style="display:flex;gap:8px;align-items:center">
            <span id="q-refresh-status" style="font-size:11px;color:var(--muted)">Auto-refresh 15s</span>
            <button class="btn btn-mini" id="q-cleanup-btn">🧹 Annuler runs bloqués (>30min)</button>
          </div>
        </div>
        <div class="table-wrap"><table>
          <thead><tr><th>ID</th><th>Statut</th><th>Début</th><th>Fin</th><th>Vus</th><th>Enrichis</th><th>Progression</th><th>Erreur</th></tr></thead>
          <tbody id="q-body"></tbody>
        </table></div>
      </div>
    `;
    document.getElementById("q-cleanup-btn").addEventListener("click", async () => {
      const btn = document.getElementById("q-cleanup-btn");
      btn.disabled = true;
      try {
        const r = await api("/admin/api/crawl-runs/cleanup", { method: "POST", body: JSON.stringify({ older_than_minutes: 30 }) });
        alert(`${r.cleaned} run(s) annulé(s).`);
        await loadQueue();
      } catch (e) {
        alert(`Erreur: ${e.message}`);
      } finally { btn.disabled = false; }
    });
  }
  await loadQueue();
  if (state.queueTimer) clearInterval(state.queueTimer);
  state.queueTimer = setInterval(loadQueue, 15000);
}

async function loadQueue() {
  try {
    const r = await api("/admin/api/queue");
    const b = r.breakdown;
    const statsEl = document.getElementById("q-stats");
    if (statsEl) statsEl.innerHTML = `
      ${statCard("Nouveaux (priorité 1)", b.new_bands, "err")}
      ${statCard("Modifiés depuis enrich (p.2)", b.modified_since_enrich, "warn")}
      ${statCard("Legacy à ré-enrichir (p.3)", b.legacy_pending, "warn")}
      ${statCard("Stale >30j (p.4)", b.stale, "ok")}
    `;
    const tbody = document.getElementById("q-body");
    if (tbody) tbody.innerHTML = r.recent_enrich_runs.map((x) => {
      const isRunning = x.status === "running";
      const elapsed = isRunning ? Math.round((Date.now() - new Date(x.started_at)) / 60000) : null;
      const prog = isRunning && x.bands_seen > 0
        ? `<span style="color:var(--muted)">${x.bands_seen} vus, ${x.bands_enriched} enrichis${elapsed ? `, ${elapsed}min` : ""}</span>`
        : isRunning ? `<span style="color:var(--warn)">en attente… (${elapsed}min)</span>` : "—";
      return `<tr>
        <td>${x.id}</td>
        <td>${statusBadge(x.status)}</td>
        <td>${fmtDate(x.started_at)}</td>
        <td>${fmtDate(x.finished_at)}</td>
        <td>${x.bands_seen}</td>
        <td>${x.bands_enriched}</td>
        <td>${prog}</td>
        <td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;color:var(--err)">${esc(x.error || "")}</td>
      </tr>`;
    }).join("") || `<tr><td colspan="8" class="empty">Aucun run</td></tr>`;
  } catch (e) {
    const tbody = document.getElementById("q-body");
    if (tbody) tbody.innerHTML = `<tr><td colspan="8" class="empty">Erreur : ${esc(e.message)}</td></tr>`;
  }
}

function statusBadge(status) {
  const cls = status === "done" ? "ok" : status === "error" ? "err" : status === "running" ? "warn" : "muted";
  return `<span class="badge ${cls}">${esc(status)}</span>`;
}

// ------------------------------------------------------------------
// Bands
// ------------------------------------------------------------------
async function renderBands() {
  const s = state.bands;
  content().innerHTML = `
    <div class="toolbar">
      <input type="text" id="b-q" placeholder="Nom / genre…" value="${esc(s.q)}" style="min-width:180px">
      <input type="text" id="b-location_q" placeholder="Lieu…" value="${esc(s.location_q)}" style="width:160px">
      <input type="text" id="b-country" placeholder="Pays (FR, DE…)" value="${esc(s.country)}" style="width:110px">
      <input type="text" id="b-status" placeholder="Statut" value="${esc(s.status)}" style="width:130px">
      <input type="text" id="b-genre" placeholder="Genre" value="${esc(s.genre)}" style="width:150px">
      <select id="b-enriched">
        <option value="">Enrichi: tous</option>
        <option value="true" ${s.enriched === "true" ? "selected" : ""}>Enrichi ✓</option>
        <option value="false" ${s.enriched === "false" ? "selected" : ""}>Non enrichi ✗</option>
      </select>
      <select id="b-has_lat">
        <option value="">Géocodé: tous</option>
        <option value="true" ${s.has_lat === "true" ? "selected" : ""}>Géocodé ✓</option>
        <option value="false" ${s.has_lat === "false" ? "selected" : ""}>Sans coords ✗</option>
      </select>
      <input type="text" id="b-themes_q" placeholder="Thèmes…" value="${esc(s.themes_q||"")}" style="width:150px">
      <select id="b-has_location">
        <option value="">Lieu texte: tous</option>
        <option value="true" ${s.has_location === "true" ? "selected" : ""}>Lieu renseigné ✓</option>
        <option value="false" ${s.has_location === "false" ? "selected" : ""}>Lieu vide ✗</option>
      </select>
      <button class="btn" id="b-apply">Filtrer</button>
    </div>
    <div id="b-table" class="table-wrap"><div class="loading">Chargement…</div></div>
    <div class="pager" id="b-pager"></div>
  `;
  document.getElementById("b-apply").addEventListener("click", () => {
    s.q = document.getElementById("b-q").value.trim();
    s.location_q = document.getElementById("b-location_q").value.trim();
    s.country = document.getElementById("b-country").value.trim();
    s.status = document.getElementById("b-status").value.trim();
    s.genre = document.getElementById("b-genre").value.trim();
    s.enriched = document.getElementById("b-enriched").value;
    s.themes_q = document.getElementById("b-themes_q").value.trim();
    s.has_lat = document.getElementById("b-has_lat").value;
    s.has_location = document.getElementById("b-has_location").value;
    s.page = 1;
    loadBands();
  });
  await loadBands();
}

const BAND_COLUMNS = [
  { key: "ma_id", label: "MA ID" },
  { key: "name", label: "Nom" },
  { key: "country", label: "Pays" },
  { key: "status", label: "Statut" },
  { key: "genre", label: "Genre" },
  { key: "themes", label: "Thèmes" },
  { key: "location_text", label: "Lieu" },
  { key: "formed_year", label: "Année" },
  { key: "enriched", label: "Enrichi" },
  { key: "crawled_at", label: "Crawlé le" },
  { key: "updated_at", label: "Modifié le" },
];

async function loadBands() {
  const s = state.bands;
  const params = new URLSearchParams({
    page: s.page, pageSize: s.pageSize, sort: s.sort, dir: s.dir,
  });
  if (s.q) params.set("q", s.q);
  if (s.location_q) params.set("location_q", s.location_q);
  if (s.country) params.set("country", s.country);
  if (s.status) params.set("status", s.status);
  if (s.genre) params.set("genre", s.genre);
  if (s.enriched) params.set("enriched", s.enriched);
  if (s.themes_q) params.set("themes_q", s.themes_q);
  if (s.has_lat) params.set("has_lat", s.has_lat);
  if (s.has_location) params.set("has_location", s.has_location);

  try {
    const r = await api(`/admin/api/bands?${params}`);
    s.total = r.total;
    const tableEl = document.getElementById("b-table");
    tableEl.innerHTML = `
      <table>
        <thead><tr>
          ${BAND_COLUMNS.map((c) => `<th data-sort="${c.key}">${c.label}${s.sort === c.key ? (s.dir === "asc" ? " ▲" : " ▼") : ""}</th>`).join("")}
        </tr></thead>
        <tbody>
          ${r.items.map((b) => `
            <tr class="clickable" data-ma-id="${b.ma_id}">
              <td>${b.ma_id}</td>
              <td>${esc(b.name)}</td>
              <td>${esc(b.country || "")}</td>
              <td>${esc(b.status || "")}</td>
              <td>${esc(b.genre || "")}</td>
              <td style="max-width:140px;overflow:hidden;text-overflow:ellipsis" title="${esc(b.themes || "")}">${esc(b.themes || "")}</td>
              <td style="max-width:140px;overflow:hidden;text-overflow:ellipsis" title="${esc(b.location_text || "")}">${esc(b.location_text || "")}</td>
              <td>${b.formed_year || ""}</td>
              <td>${b.enriched ? '<span class="badge ok">oui</span>' : '<span class="badge warn">non</span>'}${Object.keys(b.locked_fields||{}).length ? ' <span title="Champs verrouillés manuellement" style="color:var(--blood)">🔒</span>' : ""}</td>
              <td>${fmtDate(b.crawled_at)}</td>
              <td>${fmtDate(b.updated_at)}</td>
            </tr>
          `).join("") || `<tr><td colspan="${BAND_COLUMNS.length}" class="empty">Aucun résultat</td></tr>`}
        </tbody>
      </table>
    `;
    tableEl.querySelectorAll("th[data-sort]").forEach((th) => {
      th.addEventListener("click", () => {
        const key = th.dataset.sort;
        if (s.sort === key) s.dir = s.dir === "asc" ? "desc" : "asc";
        else { s.sort = key; s.dir = "asc"; }
        loadBands();
      });
    });
    tableEl.querySelectorAll("tr[data-ma-id]").forEach((tr) => {
      tr.addEventListener("click", () => openBandModal(Number(tr.dataset.maId)));
    });

    const totalPages = Math.max(1, Math.ceil(s.total / s.pageSize));
    document.getElementById("b-pager").innerHTML = `
      <button class="btn btn-mini" id="b-prev" ${s.page <= 1 ? "disabled" : ""}>← Préc.</button>
      <span>Page ${s.page} / ${totalPages} (${s.total.toLocaleString("fr-FR")} bands)</span>
      <button class="btn btn-mini" id="b-next" ${s.page >= totalPages ? "disabled" : ""}>Suiv. →</button>
    `;
    document.getElementById("b-prev")?.addEventListener("click", () => { s.page--; loadBands(); });
    document.getElementById("b-next")?.addEventListener("click", () => { s.page++; loadBands(); });
  } catch (e) {
    document.getElementById("b-table").innerHTML = `<div class="empty">Erreur : ${esc(e.message)}</div>`;
  }
}

async function openBandModal(maId) {
  let band, locations = [], llmCalls = [];
  try {
    const r = await api(`/admin/api/bands/${maId}`);
    band = r.item;
    locations = r.locations || [];
    llmCalls = r.llm || [];
  } catch (e) {
    alert(`Erreur : ${e.message}`);
    return;
  }
  // ---- Provenance & debug (lecture seule) ----
  const lockedFields = (() => {
    const lf = band.locked_fields;
    if (!lf) return [];
    if (Array.isArray(lf)) return lf;
    if (typeof lf === "object") return Object.keys(lf).filter(k => lf[k]);
    try { const p = JSON.parse(lf); return Array.isArray(p) ? p : Object.keys(p); } catch { return []; }
  })();
  const srcRow = (label, val) => `<div style="display:flex;justify-content:space-between;gap:12px;padding:2px 0;font-size:12px"><span style="color:var(--muted)">${esc(label)}</span><span style="text-align:right">${val}</span></div>`;

  const locRowsHtml = locations.length ? locations.map(l => {
    const coords = (l.lat != null && l.lon != null) ? `${Number(l.lat).toFixed(3)}, ${Number(l.lon).toFixed(3)}` : "—";
    const conf = l.geocode_confidence != null ? Number(l.geocode_confidence).toFixed(2) : "—";
    const tries = `${l.geocode_tries_geo || 0}g/${l.geocode_tries_llm || 0}l`;
    const err = l.geocode_error ? `<div style="color:var(--err);font-size:11px">${esc(l.geocode_error)}</div>` : "";
    return `<tr>
      <td>${l.step_order}${l.step_label ? ` <span style="color:var(--muted)">(${esc(l.step_label)})</span>` : ""}</td>
      <td>${esc(l.location_raw)}${err}</td>
      <td>${esc(l.geocode_status)}</td>
      <td>${esc(l.geocode_provider || "—")}</td>
      <td>${conf}</td>
      <td>${coords}</td>
      <td>${tries}</td>
      <td style="font-size:11px;color:var(--muted)">${esc(l.geocode_query || "—")}</td>
    </tr>`;
  }).join("") : `<tr><td colspan="8" style="color:var(--muted)">Aucune localisation dans band_locations</td></tr>`;

  const llmHtml = llmCalls.length ? llmCalls.map(c => {
    const res = c.is_null ? `<span style="color:var(--warn)">city=null</span>` : `${esc(c.parsed_city || "?")} (${esc(c.parsed_country || "?")})`;
    const cost = c.cost_usd != null ? `$${Number(c.cost_usd).toFixed(6)}` : "—";
    return `<details style="margin:4px 0;background:rgba(255,255,255,0.03);border-radius:4px;padding:6px 8px">
      <summary style="cursor:pointer;font-size:12px">${esc(c.model)} · ${esc(c.location_raw || "")} → ${res} · ${c.tokens_in || 0}+${c.tokens_out || 0}tok · ${cost} · ${fmtDate(c.created_at)}</summary>
      <div style="margin-top:6px;font-size:11px;color:var(--muted);white-space:pre-wrap">PROMPT:\n${esc(c.prompt || "")}\n\nRESPONSE:\n${esc(c.response || "")}</div>
    </details>`;
  }).join("") : `<div style="color:var(--muted);font-size:12px">Aucun appel LLM enregistré pour ce groupe.</div>`;

  const provenanceHtml = `
    <details style="margin-top:14px;border-top:1px solid rgba(255,255,255,0.1);padding-top:10px" open>
      <summary style="cursor:pointer;font-weight:600;color:var(--muted)">🔎 Provenance & géocodage (lecture seule)</summary>

      <div style="margin-top:8px">
        <div style="font-size:11px;text-transform:uppercase;color:var(--muted);margin-bottom:4px">Metal Archives (crawl)</div>
        ${srcRow("enrichi", band.enriched ? "oui" : "non")}
        ${srcRow("crawled_at", fmtDate(band.crawled_at))}
        ${srcRow("MA créé / modifié", `${fmtDate(band.ma_created_at)} / ${fmtDate(band.ma_modified_at)}`)}
        ${srcRow("champs verrouillés (manuel)", lockedFields.length ? esc(lockedFields.join(", ")) : "—")}
      </div>

      <div style="margin-top:10px">
        <div style="font-size:11px;text-transform:uppercase;color:var(--muted);margin-bottom:4px">Géocodage (point du groupe)</div>
        ${srcRow("provider", esc(band.geocode_provider || "—"))}
        ${srcRow("query", esc(band.geocode_query || "—"))}
        ${srcRow("geocoded_at", fmtDate(band.geocoded_at))}
        ${band.geocode_error ? srcRow("erreur", `<span style="color:var(--err)">${esc(band.geocode_error)}</span>`) : ""}
      </div>

      <div style="margin-top:10px">
        <div style="font-size:11px;text-transform:uppercase;color:var(--muted);margin-bottom:4px">Localisations (band_locations)</div>
        <div style="overflow-x:auto">
          <table style="width:100%;border-collapse:collapse;font-size:12px">
            <thead><tr style="text-align:left;color:var(--muted)">
              <th>Step</th><th>Brut</th><th>Statut</th><th>Provider</th><th>Conf</th><th>Coords</th><th>Essais</th><th>Query</th>
            </tr></thead>
            <tbody>${locRowsHtml}</tbody>
          </table>
        </div>
      </div>

      <div style="margin-top:10px">
        <div style="font-size:11px;text-transform:uppercase;color:var(--muted);margin-bottom:4px">Appels LLM (Groq)</div>
        ${llmHtml}
      </div>
    </details>`;

  const backdrop = document.createElement("div");
  backdrop.className = "modal-backdrop";
  backdrop.innerHTML = `
    <div class="modal">
      <span class="modal-close">&times;</span>
      <h3>${esc(band.name)} <span style="color:var(--muted);font-weight:400">#${band.ma_id}</span></h3>
      <label>Nom<input type="text" id="m-name" value="${esc(band.name || "")}"></label>
      <label>Pays<input type="text" id="m-country" value="${esc(band.country || "")}"></label>
      <label>Statut<input type="text" id="m-status" value="${esc(band.status || "")}"></label>
      <label>Genre<input type="text" id="m-genre" value="${esc(band.genre || "")}"></label>
      <label>Année de formation<input type="number" id="m-formed_year" value="${band.formed_year ?? ""}"></label>
      <label>Localisation<input type="text" id="m-location_text" value="${esc(band.location_text || "")}"></label>
      <label>Thèmes<input type="text" id="m-themes" value="${esc(band.themes || "")}"></label>
      <label>Latitude<input type="number" step="any" id="m-lat" value="${band.lat ?? ""}"></label>
      <label>Longitude<input type="number" step="any" id="m-lon" value="${band.lon ?? ""}"></label>
      ${provenanceHtml}
      <div class="modal-error" id="m-error"></div>
      <div class="modal-actions">
        <button class="btn btn-mini" id="m-cancel">Annuler</button>
        <button class="btn" id="m-save">Enregistrer</button>
      </div>
    </div>
  `;
  document.body.appendChild(backdrop);
  const close = () => backdrop.remove();
  backdrop.querySelector(".modal-close").addEventListener("click", close);
  backdrop.addEventListener("click", (e) => { if (e.target === backdrop) close(); });
  backdrop.querySelector("#m-cancel").addEventListener("click", close);
  backdrop.querySelector("#m-save").addEventListener("click", async () => {
    const errEl = backdrop.querySelector("#m-error");
    errEl.textContent = "";
    const payload = {
      name: backdrop.querySelector("#m-name").value.trim() || null,
      country: backdrop.querySelector("#m-country").value.trim() || null,
      status: backdrop.querySelector("#m-status").value.trim() || null,
      genre: backdrop.querySelector("#m-genre").value.trim() || null,
      location_text: backdrop.querySelector("#m-location_text").value.trim() || null,
      themes: backdrop.querySelector("#m-themes").value.trim() || null,
      formed_year: backdrop.querySelector("#m-formed_year").value ? Number(backdrop.querySelector("#m-formed_year").value) : null,
      lat: backdrop.querySelector("#m-lat").value ? Number(backdrop.querySelector("#m-lat").value) : null,
      lon: backdrop.querySelector("#m-lon").value ? Number(backdrop.querySelector("#m-lon").value) : null,
    };
    try {
      await api(`/admin/api/bands/${maId}`, { method: "PATCH", body: JSON.stringify(payload) });
      close();
      loadBands();
    } catch (e) {
      errEl.textContent = e.message;
    }
  });
}

// ------------------------------------------------------------------
// Crawl runs
// ------------------------------------------------------------------
async function renderRuns() {
  const s = state.runs;
  content().innerHTML = `
    <div class="toolbar">
      <select id="r-type">
        <option value="">Type: tous</option>
        <option value="full_europe">full_europe</option>
        <option value="incremental_created">incremental_created</option>
        <option value="incremental_modified">incremental_modified</option>
        <option value="enrich">enrich</option>
      </select>
      <select id="r-status">
        <option value="">Statut: tous</option>
        <option value="running">running</option>
        <option value="done">done</option>
        <option value="error">error</option>
      </select>
      <button class="btn" id="r-apply">Filtrer</button>
    </div>
    <div id="r-table" class="table-wrap"><div class="loading">Chargement…</div></div>
    <div class="pager" id="r-pager"></div>
  `;
  document.getElementById("r-apply").addEventListener("click", () => {
    s.run_type = document.getElementById("r-type").value;
    s.status = document.getElementById("r-status").value;
    s.page = 1;
    loadRuns();
  });
  await loadRuns();
}

async function loadRuns() {
  const s = state.runs;
  const params = new URLSearchParams({ page: s.page, pageSize: s.pageSize });
  if (s.run_type) params.set("run_type", s.run_type);
  if (s.status) params.set("status", s.status);
  try {
    const r = await api(`/admin/api/crawl-runs?${params}`);
    s.total = r.total;
    document.getElementById("r-table").innerHTML = `
      <table>
        <thead><tr><th>ID</th><th>Type</th><th>Pays</th><th>Statut</th><th>Début</th><th>Fin</th><th>Vus</th><th>Nouveaux</th><th>MAJ</th><th>Enrichis</th><th>Erreur</th></tr></thead>
        <tbody>
          ${r.items.map((x) => `
            <tr>
              <td>${x.id}</td>
              <td>${esc(x.run_type)}</td>
              <td>${esc((x.countries || []).join(", "))}</td>
              <td>${statusBadge(x.status)}</td>
              <td>${fmtDate(x.started_at)}</td>
              <td>${fmtDate(x.finished_at)}</td>
              <td>${x.bands_seen}</td>
              <td>${x.bands_new}</td>
              <td>${x.bands_updated}</td>
              <td>${x.bands_enriched}</td>
              <td>${esc(x.error || "")}</td>
            </tr>
          `).join("") || `<tr><td colspan="11" class="empty">Aucun run</td></tr>`}
        </tbody>
      </table>
    `;
    const totalPages = Math.max(1, Math.ceil(s.total / s.pageSize));
    document.getElementById("r-pager").innerHTML = `
      <button class="btn btn-mini" id="r-prev" ${s.page <= 1 ? "disabled" : ""}>← Préc.</button>
      <span>Page ${s.page} / ${totalPages} (${s.total} runs)</span>
      <button class="btn btn-mini" id="r-next" ${s.page >= totalPages ? "disabled" : ""}>Suiv. →</button>
    `;
    document.getElementById("r-prev")?.addEventListener("click", () => { s.page--; loadRuns(); });
    document.getElementById("r-next")?.addEventListener("click", () => { s.page++; loadRuns(); });
  } catch (e) {
    document.getElementById("r-table").innerHTML = `<div class="empty">Erreur : ${esc(e.message)}</div>`;
  }
}

// ------------------------------------------------------------------
// Live Monitor
// ------------------------------------------------------------------
async function renderMonitor() {
  state.monitorLogs = [];
  state.monitorLastId = 0;
  state.monitorPaused = false;

  content().innerHTML = `
    <div class="mon-header">
      <div style="display:flex;align-items:center;gap:10px">
        <span class="pulse-dot" id="mon-dot" style="background:var(--warn)"></span>
        <span style="font-size:12px;color:var(--muted)" id="mon-ts">Connexion…</span>
      </div>
      <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
        <label style="font-size:12px;color:var(--muted);display:flex;align-items:center;gap:6px">
          Refresh
          <select id="mon-interval" style="padding:4px 8px;font-size:12px;border-radius:8px;border:1px solid rgba(255,255,255,0.10);background:rgba(4,6,10,0.72);color:var(--text);outline:none">
            <option value="3000">3 s</option>
            <option value="5000" selected>5 s</option>
            <option value="10000">10 s</option>
            <option value="30000">30 s</option>
          </select>
        </label>
        <button class="btn btn-mini" id="mon-pause-btn">⏸ Pause</button>
        <button class="btn btn-mini" id="mon-clear-btn">🗑 Vider logs</button>
      </div>
    </div>

    <div class="grid grid-3" id="mon-status" style="margin-bottom:16px">
      <div class="card"><div class="loading">…</div></div>
      <div class="card"><div class="loading">…</div></div>
      <div class="card"><div class="loading">…</div></div>
    </div>

    <div class="card">
      <div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap">
        <h2 style="margin:0">Journal en direct</h2>
        <select id="mon-level" style="padding:5px 8px;font-size:12px;border-radius:8px;border:1px solid rgba(255,255,255,0.10);background:rgba(4,6,10,0.72);color:var(--text);outline:none">
          <option value="">Tous</option>
          <option value="info">info</option>
          <option value="warning">warning</option>
          <option value="error">error</option>
        </select>
        <input type="text" id="mon-search" placeholder="Filtrer…" style="padding:5px 10px;font-size:12px;border-radius:8px;border:1px solid rgba(255,255,255,0.10);background:rgba(4,6,10,0.72);color:var(--text);width:200px;outline:none">
        <span id="mon-count" style="font-size:11px;color:var(--muted);margin-left:auto"></span>
      </div>
      <div id="mon-stream" class="log-stream"></div>
    </div>
  `;

  document.getElementById("mon-pause-btn").addEventListener("click", () => {
    state.monitorPaused = !state.monitorPaused;
    const btn = document.getElementById("mon-pause-btn");
    btn.textContent = state.monitorPaused ? "▶ Reprendre" : "⏸ Pause";
    btn.style.borderColor = state.monitorPaused ? "rgba(198,26,26,0.5)" : "";
  });

  document.getElementById("mon-clear-btn").addEventListener("click", () => {
    state.monitorLogs = [];
    state.monitorLastId = 0;
    renderMonitorStream();
  });

  document.getElementById("mon-level").addEventListener("input", renderMonitorStream);
  document.getElementById("mon-search").addEventListener("input", renderMonitorStream);

  document.getElementById("mon-interval").addEventListener("change", (e) => {
    state.monitorInterval = Number(e.target.value) || 5000;
    if (state.monitorTimer) clearInterval(state.monitorTimer);
    state.monitorTimer = setInterval(tickMonitor, state.monitorInterval);
  });

  await tickMonitor();
  state.monitorTimer = setInterval(tickMonitor, state.monitorInterval);
}

async function tickMonitor() {
  if (state.monitorPaused) return;
  const dot = document.getElementById("mon-dot");
  const tsEl = document.getElementById("mon-ts");
  try {
    const [live, logs] = await Promise.all([
      api("/admin/api/live"),
      api(`/admin/api/logs?pageSize=100${state.monitorLastId ? `&min_id=${state.monitorLastId}` : ""}`),
    ]);

    renderMonitorStatus(live);

    if (logs.items.length) {
      const maxId = Math.max(...logs.items.map(x => x.id));
      if (maxId > state.monitorLastId) state.monitorLastId = maxId;
      state.monitorLogs = [...logs.items, ...state.monitorLogs].slice(0, 500);
      renderMonitorStream(logs.items.length);
    } else if (!state.monitorLogs.length) {
      renderMonitorStream(0);
    }

    if (dot) { dot.style.background = "var(--ok)"; dot.classList.add("live"); }
    if (tsEl) tsEl.textContent = `Actualisé à ${new Date().toLocaleTimeString("fr-FR")}`;
  } catch (e) {
    if (dot) { dot.style.background = "var(--err)"; dot.classList.remove("live"); }
    if (tsEl) tsEl.textContent = `Erreur : ${e.message}`;
  }
}

function renderMonitorStatus(live) {
  const el = document.getElementById("mon-status");
  if (!el) return;

  // --- Crawl runs ---
  const runsHtml = live.active_runs.length
    ? live.active_runs.map(r => {
        const elapsed = Math.round((Date.now() - new Date(r.started_at)) / 60000);
        return `<div class="mon-run-row">
          <div>
            <span class="badge warn">▶ ${esc(r.run_type)}</span>
            <div style="font-size:11px;color:var(--muted);margin-top:5px">
              ${elapsed}min · ${r.bands_seen} vus · ${r.bands_new} nouveaux · ${r.bands_enriched} enrichis
            </div>
            ${r.error ? `<div style="font-size:11px;color:var(--err);margin-top:3px">${esc(r.error)}</div>` : ""}
          </div>
          <button class="btn btn-mini" style="border-color:rgba(198,26,26,0.4)" onclick="adminCancelRun(${r.id})">✕ Annuler</button>
        </div>`;
      }).join("")
    : `<div class="empty" style="padding:16px 0">Aucun run actif</div>`;

  // --- Géocodeur ---
  const gTotal = live.geo_queue.reduce((s, x) => s + x.n, 0);
  const gDone  = live.geo_queue.find(x => x.status === "done")?.n || 0;
  const gQ     = live.geo_queue.find(x => x.status === "queued")?.n || 0;
  const gErr   = live.geo_queue.find(x => x.status === "error")?.n || 0;
  const gProc  = live.geo_queue.find(x => x.status === "processing")?.n || 0;
  const gPct   = gTotal ? Math.round((gDone / gTotal) * 100) : 0;
  const geoHtml = `
    <div style="background:rgba(255,255,255,0.06);border-radius:6px;height:6px;overflow:hidden;margin-bottom:8px">
      <div style="width:${gPct}%;height:100%;background:linear-gradient(90deg,var(--blood),rgba(198,26,26,0.5));transition:width 0.5s ease"></div>
    </div>
    <div style="font-size:13px;font-weight:700;margin-bottom:6px">${gPct}% — ${gDone.toLocaleString("fr-FR")} / ${gTotal.toLocaleString("fr-FR")}</div>
    <div style="font-size:11px;color:var(--muted);display:flex;flex-wrap:wrap;gap:8px">
      <span>${gQ} en attente</span>
      ${gProc ? `<span style="color:var(--warn)">⚡ ${gProc} en cours</span>` : ""}
      ${gErr  ? `<span style="color:var(--err)">✗ ${gErr} erreurs</span>` : ""}
    </div>
    ${live.geo_processing.map(p => `
      <div style="margin-top:8px;padding:6px 8px;background:rgba(255,255,255,0.04);border-radius:7px;font-size:11px">
        <span style="color:var(--warn)">⚡</span> ${esc(p.name)} <span style="color:var(--muted)">(${esc(p.country || "?")})</span>
        ${p.tries > 1 ? `<span style="color:var(--warn);margin-left:6px">essai ${p.tries}</span>` : ""}
      </div>`).join("")}
  `;

  // --- Jobs en attente ---
  const jobsHtml = live.pending_jobs.length
    ? live.pending_jobs.map(j => {
        const age = Math.round((Date.now() - new Date(j.created_at)) / 60000);
        return `<div class="mon-run-row">
          <div>
            <span class="badge muted">${esc(j.job_type)}</span>
            <div style="font-size:11px;color:var(--muted);margin-top:4px">par ${esc(j.requested_by || "?")} · ${age}min</div>
          </div>
          <button class="btn btn-mini" onclick="adminCancelJob(${j.id})">✕</button>
        </div>`;
      }).join("")
    : `<div class="empty" style="padding:16px 0">Aucun job en attente</div>`;

  el.innerHTML = `
    <div class="card">
      <h2 style="margin-bottom:10px">Crawl en cours
        <span style="font-weight:400;color:var(--muted)">(${live.active_runs.length})</span>
      </h2>
      ${runsHtml}
    </div>
    <div class="card">
      <h2 style="margin-bottom:10px">Géocodeur</h2>
      ${geoHtml}
    </div>
    <div class="card">
      <h2 style="margin-bottom:10px">Jobs en attente
        <span style="font-weight:400;color:var(--muted)">(${live.pending_jobs.length})</span>
      </h2>
      ${jobsHtml}
    </div>
  `;
}

function renderMonitorStream(newCount = 0) {
  const stream = document.getElementById("mon-stream");
  if (!stream) return;
  const level  = document.getElementById("mon-level")?.value || "";
  const search = (document.getElementById("mon-search")?.value || "").toLowerCase();

  const filtered = state.monitorLogs.filter(x => {
    if (level && x.level !== level) return false;
    if (search && !x.message.toLowerCase().includes(search)) return false;
    return true;
  });

  const countEl = document.getElementById("mon-count");
  if (countEl) {
    countEl.textContent = `${filtered.length} entrée${filtered.length !== 1 ? "s" : ""}${state.monitorLogs.length !== filtered.length ? ` / ${state.monitorLogs.length} total` : ""}`;
  }

  const wasAtTop = stream.scrollTop < 40;
  stream.innerHTML = filtered.map((x, i) => {
    const lvlCls = x.level === "error" ? "err" : x.level === "warning" ? "warn" : "muted";
    return `<div class="log-line${i < newCount ? " log-new" : ""}">
      <span class="ts" style="color:var(--muted);flex-shrink:0">${fmtDate(x.created_at)}</span>
      <span style="flex-shrink:0;width:56px;font-weight:700;color:var(--${lvlCls})">${esc(x.level)}</span>
      <span style="word-break:break-all">${esc(x.message)}${x.ma_id ? ` <span style="color:var(--muted)">#${x.ma_id}</span>` : ""}</span>
    </div>`;
  }).join("") || `<div class="empty">Aucun log${level || search ? " (filtres actifs)" : ""}</div>`;

  if (wasAtTop || newCount > 0) stream.scrollTop = 0;
}

async function adminCancelRun(id) {
  if (!confirm(`Annuler le run #${id} ?`)) return;
  try {
    await api(`/admin/api/crawl-runs/${id}/cancel`, { method: "POST", body: "{}" });
    await tickMonitor();
  } catch (e) { alert(`Erreur : ${e.message}`); }
}

async function adminCancelJob(id) {
  if (!confirm(`Annuler le job trigger #${id} ?`)) return;
  try {
    await api(`/admin/api/job-triggers/${id}/cancel`, { method: "POST", body: "{}" });
    await tickMonitor();
  } catch (e) { alert(`Erreur : ${e.message}`); }
}

// ------------------------------------------------------------------
// Logs (historique)
// ------------------------------------------------------------------
async function renderLogs() {
  const s = state.logs;
  content().innerHTML = `
    <div class="toolbar">
      <select id="l-level">
        <option value="">Niveau: tous</option>
        <option value="info">info</option>
        <option value="warning">warning</option>
        <option value="error">error</option>
      </select>
      <button class="btn" id="l-apply">Filtrer</button>
      <label style="font-size:12px;color:var(--muted);display:flex;align-items:center;gap:6px;">
        <input type="checkbox" id="l-auto" ${s.autoRefresh ? "checked" : ""}> Auto-refresh (10s)
      </label>
    </div>
    <div id="l-list" class="card"><div class="loading">Chargement…</div></div>
  `;
  document.getElementById("l-apply").addEventListener("click", () => {
    s.level = document.getElementById("l-level").value;
    loadLogs();
  });
  document.getElementById("l-auto").addEventListener("change", (e) => {
    s.autoRefresh = e.target.checked;
    setupLogsTimer();
  });
  await loadLogs();
  setupLogsTimer();
}

function setupLogsTimer() {
  if (state.logsTimer) { clearInterval(state.logsTimer); state.logsTimer = null; }
  if (state.logs.autoRefresh) {
    state.logsTimer = setInterval(loadLogs, 10000);
  }
}

async function loadLogs() {
  const s = state.logs;
  const params = new URLSearchParams({ page: 1, pageSize: s.pageSize });
  if (s.level) params.set("level", s.level);
  try {
    const r = await api(`/admin/api/logs?${params}`);
    document.getElementById("l-list").innerHTML = r.items.map((x) => `
      <div class="log-line">
        <span class="ts">${fmtDate(x.created_at)}</span>
        <span class="lvl ${esc(x.level)}">${esc(x.level)}</span>
        <span>${esc(x.message)}${x.ma_id ? ` <span style="color:var(--muted)">(ma_id=${x.ma_id})</span>` : ""}</span>
      </div>
    `).join("") || `<div class="empty">Aucun log</div>`;
  } catch (e) {
    document.getElementById("l-list").innerHTML = `<div class="empty">Erreur : ${esc(e.message)}</div>`;
  }
}

// ------------------------------------------------------------------
// Conflits (champs verrouillés ≠ valeur crawler)
// ------------------------------------------------------------------
async function renderConflicts() {
  content().innerHTML = `<div class="loading">Chargement…</div>`;
  try {
    const r = await api("/admin/api/conflicts?page=1&pageSize=100");
    if (!r.items.length) {
      content().innerHTML = `<div class="card"><div class="empty">Aucun conflit — toutes les éditions manuelles sont cohérentes avec les données Metal Archives.</div></div>`;
      return;
    }
    content().innerHTML = `
      <p style="font-size:12px;color:var(--muted);margin:0 0 14px">
        Ces bands ont des champs édités manuellement (🔒) pour lesquels le crawler a trouvé une valeur différente.
        Choisissez champ par champ ce que vous voulez conserver.
      </p>
      <div id="conflict-list">${r.items.map(conflictCard).join("")}</div>
    `;
    document.getElementById("conflict-list").addEventListener("click", async (e) => {
      const btn = e.target.closest("[data-action]");
      if (!btn) return;
      const { action, maId, field } = btn.dataset;
      btn.disabled = true;
      try {
        await api(`/admin/api/bands/${maId}/resolve-conflict`, {
          method: "POST",
          body: JSON.stringify({ field, action }),
        });
        renderConflicts();
      } catch (err) { alert(err.message); btn.disabled = false; }
    });
  } catch (e) {
    content().innerHTML = `<div class="empty">Erreur : ${esc(e.message)}</div>`;
  }
}

function conflictCard(b) {
  const pending = b.crawler_pending || {};
  const locked = b.locked_fields || {};
  const rows = Object.entries(pending).map(([field, crawlerVal]) => {
    const myVal = b[field] ?? "—";
    return `<tr>
      <td><strong>${esc(field)}</strong></td>
      <td style="color:var(--bone)">${esc(String(myVal))}</td>
      <td style="color:var(--warn)">${esc(String(crawlerVal))}</td>
      <td>
        <button class="btn btn-mini" data-action="keep_mine" data-ma-id="${b.ma_id}" data-field="${field}">Garder ma valeur</button>
        <button class="btn btn-mini" style="border-color:rgba(198,26,26,0.4)" data-action="accept_crawler" data-ma-id="${b.ma_id}" data-field="${field}">Accepter MA</button>
      </td>
    </tr>`;
  }).join("");
  return `<div class="card" style="margin-bottom:14px">
    <h2 style="margin-bottom:8px">${esc(b.name)} <span style="color:var(--muted);font-weight:400">#${b.ma_id} · ${esc(b.country||"")}</span></h2>
    <div class="table-wrap"><table>
      <thead><tr><th>Champ</th><th>Ma valeur (verrouillée)</th><th>Valeur Metal Archives</th><th>Action</th></tr></thead>
      <tbody>${rows}</tbody>
    </table></div>
  </div>`;
}

// ------------------------------------------------------------------
// Géocodage
// ------------------------------------------------------------------
async function renderGeocoding() {
  if (!document.getElementById("geo-stats")) {
    content().innerHTML = `
      <div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;margin-bottom:12px">
        <div style="font-size:13px;font-weight:700;color:var(--bone)">Pipeline géocodage</div>
        <span id="geo-refresh-status" style="font-size:11px;color:var(--muted)">Auto-refresh 30s</span>
      </div>

      <div class="card" style="margin-bottom:16px">
        <div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;margin-bottom:10px">
          <h2 style="margin:0">Nouveau pipeline — band_locations</h2>
          <div style="display:flex;gap:8px;flex-wrap:wrap">
            <button class="btn btn-mini" id="loc-enqueue" style="border-color:rgba(63,174,90,0.5);color:var(--ok)">Lancer l'enqueue</button>
            <button class="btn btn-mini" id="loc-reset-errors">Reset erreurs</button>
            <button class="btn btn-mini" id="loc-reset-llm">Reset LLM needed</button>
            <button class="btn btn-mini" id="loc-requeue-all">Re-queue tout</button>
          </div>
        </div>
        <div class="grid grid-stats" id="loc-stats" style="margin-bottom:12px"></div>
        <div style="background:rgba(255,255,255,0.06);border-radius:8px;height:12px;overflow:hidden;margin-bottom:6px">
          <div id="loc-bar" style="width:0%;height:100%;background:linear-gradient(90deg,var(--blood),rgba(198,26,26,0.6));transition:width 0.4s ease"></div>
        </div>
        <div id="loc-pct" style="font-size:12px;color:var(--muted)">—</div>
        <div id="loc-providers" style="font-size:12px;color:var(--muted);margin-top:8px"></div>
        <div id="loc-feedback" style="margin-top:8px;font-size:12px"></div>
      </div>

      <div class="card" style="margin-bottom:16px">
        <div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;margin-bottom:10px">
          <h2 style="margin:0">Ancien pipeline — geocode_queue</h2>
          <div style="display:flex;gap:8px;flex-wrap:wrap">
            <button class="btn btn-mini" id="geo-reset-errors">Reset erreurs</button>
            <button class="btn btn-mini" id="geo-requeue-done">Re-queue tout</button>
          </div>
        </div>
        <div class="grid grid-stats" id="geo-stats" style="margin-bottom:12px"></div>
        <div style="background:rgba(255,255,255,0.06);border-radius:8px;height:12px;overflow:hidden;margin-bottom:6px">
          <div id="geo-bar" style="width:0%;height:100%;background:linear-gradient(90deg,#3fae5a,rgba(63,174,90,0.5));transition:width 0.4s ease"></div>
        </div>
        <div id="geo-pct" style="font-size:12px;color:var(--muted)">—</div>
        <div id="geo-feedback" style="margin-top:8px;font-size:12px"></div>
      </div>

      <div class="card">
        <h2>20 derniers géocodages (bands)</h2>
        <div class="table-wrap" id="geo-recent-wrap"><div class="loading">Chargement…</div></div>
      </div>
    `;

    // Nouveau pipeline — actions
    const makeLocAction = (btnId, fbId, url, body, confirmMsg) => {
      document.getElementById(btnId).addEventListener("click", async () => {
        if (confirmMsg && !confirm(confirmMsg)) return;
        const btn = document.getElementById(btnId);
        btn.disabled = true;
        const fb = document.getElementById(fbId);
        fb.textContent = "En cours…"; fb.style.color = "var(--muted)";
        try {
          const r = await api(url, { method: "POST", body: JSON.stringify(body || {}) });
          fb.textContent = `✓ ${r.count} entrée(s) modifiée(s).`;
          fb.style.color = "var(--ok)";
          await loadGeocoding();
        } catch (e) {
          fb.textContent = `Erreur: ${e.message}`; fb.style.color = "var(--err)";
        } finally { btn.disabled = false; }
      });
    };

    document.getElementById("loc-enqueue").addEventListener("click", async () => {
      const btn = document.getElementById("loc-enqueue");
      btn.disabled = true;
      const fb = document.getElementById("loc-feedback");
      fb.textContent = "Job envoyé, le daemon va démarrer l'enqueue (peut prendre quelques secondes)…";
      fb.style.color = "var(--muted)";
      try {
        await api("/admin/api/job-triggers", { method: "POST", body: JSON.stringify({ job_type: "geocoder_enqueue" }) });
        fb.textContent = "✓ Job geocoder_enqueue créé — le daemon parse les bands en arrière-plan.";
        fb.style.color = "var(--ok)";
        setTimeout(() => loadGeocoding(), 3000);
      } catch (e) {
        fb.textContent = `Erreur: ${e.message}`; fb.style.color = "var(--err)";
      } finally { btn.disabled = false; }
    });

    makeLocAction("loc-reset-errors", "loc-feedback", "/admin/api/locations/reset-errors", {}, null);
    makeLocAction("loc-reset-llm",    "loc-feedback", "/admin/api/locations/reset-llm",    {}, "Remettre en queue les entrées llm_needed et manual ?");
    makeLocAction("loc-requeue-all",  "loc-feedback", "/admin/api/locations/requeue-all",  { include_done: true }, "Re-queue TOUTES les band_locations (erreurs + LLM + manual + done) ?");

    // Ancien pipeline — actions
    document.getElementById("geo-reset-errors").addEventListener("click", async () => {
      const btn = document.getElementById("geo-reset-errors");
      btn.disabled = true;
      const fb = document.getElementById("geo-feedback");
      fb.textContent = "Réinitialisation…"; fb.style.color = "var(--muted)";
      try {
        const r = await api("/admin/api/geocoding/reset-errors", { method: "POST", body: JSON.stringify({}) });
        fb.textContent = `✓ ${r.count} entrée(s) remise(s) en queue.`;
        fb.style.color = "var(--ok)";
        await loadGeocoding();
      } catch (e) {
        fb.textContent = `Erreur: ${e.message}`; fb.style.color = "var(--err)";
      } finally { btn.disabled = false; }
    });

    document.getElementById("geo-requeue-done").addEventListener("click", async () => {
      if (!confirm("Réinitialiser TOUTES les entrées geocode_queue (erreurs + fait) ?")) return;
      const btn = document.getElementById("geo-requeue-done");
      btn.disabled = true;
      const fb = document.getElementById("geo-feedback");
      fb.textContent = "Réinitialisation…"; fb.style.color = "var(--muted)";
      try {
        const r = await api("/admin/api/geocoding/requeue-all", { method: "POST", body: JSON.stringify({ include_done: true }) });
        fb.textContent = `✓ ${r.count} entrée(s) re-queueée(s).`;
        fb.style.color = "var(--ok)";
        await loadGeocoding();
      } catch (e) {
        fb.textContent = `Erreur: ${e.message}`; fb.style.color = "var(--err)";
      } finally { btn.disabled = false; }
    });
  }

  await loadGeocoding();
  if (state.geoTimer) clearInterval(state.geoTimer);
  state.geoTimer = setInterval(async () => {
    await loadGeocoding();
    const el = document.getElementById("geo-refresh-status");
    if (el) el.textContent = `Actualisé ${new Date().toLocaleTimeString("fr-FR")}`;
  }, 30000);
}

const LOC_STATUS_LABELS = {
  queued: "En queue", processing: "En cours", done: "Done",
  error: "Erreur", llm_needed: "LLM needed", manual: "Manuel",
  country_only: "Pays (centroïde)", skipped: "Ignoré",
};

async function loadGeocoding() {
  try {
    const r = await api("/admin/api/geocoding");

    // Nouveau pipeline — band_locations
    const locs     = r.locations || [];
    const locTotal = locs.reduce((s, x) => s + x.n, 0);
    const locDone  = (locs.find((x) => x.status === "done")?.n || 0) +
                     (locs.find((x) => x.status === "country_only")?.n || 0);
    const locLlm   = locs.find((x) => x.status === "llm_needed")?.n || 0;
    const locManual = locs.find((x) => x.status === "manual")?.n || 0;
    const locErr   = locs.find((x) => x.status === "error")?.n || 0;
    const locPct   = locTotal ? Math.round((locDone / locTotal) * 100) : 0;
    const rawCost  = Number(r.llm_cache?.total_cost_usd);
    const llmCost  = (Number.isFinite(rawCost) ? rawCost : 0).toFixed(4);

    const llmNull = r.llm_cache?.n_null || 0;
    const locStatsEl = document.getElementById("loc-stats");
    if (locStatsEl) locStatsEl.innerHTML = `
      ${statCard("Total locations", locTotal)}
      ${statCard("Géocodées", locDone, "ok")}
      ${statCard("LLM needed", locLlm, locLlm ? "warn" : "")}
      ${statCard("Manuel", locManual, locManual ? "warn" : "")}
      ${statCard("Erreurs", locErr, locErr ? "err" : "")}
      ${statCard("LLM cache", r.llm_cache?.n || 0)}
      ${statCard("LLM null", llmNull, llmNull ? "warn" : "")}
      ${statCard("Coût LLM (USD)", "$" + llmCost)}
    `;
    const locBar = document.getElementById("loc-bar");
    if (locBar) locBar.style.width = locPct + "%";
    const locPctEl = document.getElementById("loc-pct");
    if (locPctEl) locPctEl.textContent = `${locPct}% — ${locDone.toLocaleString("fr-FR")} / ${locTotal.toLocaleString("fr-FR")} — statuts: ${locs.map((x) => `${LOC_STATUS_LABELS[x.status] || x.status} ${x.n}`).join(", ")}`;

    // Breakdown par provider (source du géocodage) + par modèle LLM
    const provEl = document.getElementById("loc-providers");
    if (provEl) {
      const provTxt = (r.providers || []).map((p) => `${esc(p.provider)}: ${p.n.toLocaleString("fr-FR")}${p.avg_conf != null ? ` (conf~${p.avg_conf})` : ""}`).join(" · ");
      const modelTxt = (r.llm_models || []).map((m) => `${esc(m.model)}: ${m.n} (null ${m.n_null})`).join(" · ");
      provEl.innerHTML = `<div><strong>Sources géocodage :</strong> ${provTxt || "—"}</div>` +
        (modelTxt ? `<div style="margin-top:4px"><strong>Modèles LLM :</strong> ${modelTxt}</div>` : "");
    }

    // Ancien pipeline — geocode_queue
    const total      = r.queue.reduce((s, x) => s + x.n, 0);
    const done       = r.queue.find((x) => x.status === "done")?.n || 0;
    const queued     = r.queue.find((x) => x.status === "queued")?.n || 0;
    const errored    = r.queue.find((x) => x.status === "error")?.n || 0;
    const processing = r.queue.find((x) => x.status === "processing")?.n || 0;
    const pct        = total ? Math.round((done / total) * 100) : 0;

    const statsEl = document.getElementById("geo-stats");
    if (statsEl) statsEl.innerHTML = `
      ${statCard("Total queue", total)}
      ${statCard("Géocodés", done, "ok")}
      ${statCard("En attente", queued, queued ? "warn" : "")}
      ${statCard("En cours", processing, processing ? "ok" : "")}
      ${statCard("Erreurs", errored, errored ? "err" : "")}
      ${statCard("Cache Geoapify", r.cache_size)}
    `;
    const bar = document.getElementById("geo-bar");
    if (bar) bar.style.width = pct + "%";
    const pctEl = document.getElementById("geo-pct");
    if (pctEl) pctEl.textContent = `${pct}% — ${done.toLocaleString("fr-FR")} / ${total.toLocaleString("fr-FR")}${processing ? ` (${processing} en cours)` : ""}`;

    const recentWrap = document.getElementById("geo-recent-wrap");
    if (recentWrap) recentWrap.innerHTML = `<table>
      <thead><tr><th>MA ID</th><th>Nom</th><th>Pays</th><th>Provider</th><th>Query</th><th>Géocodé le</th><th>Erreur</th></tr></thead>
      <tbody>
        ${r.recent.map((x) => `<tr>
          <td>${x.ma_id}</td>
          <td>${esc(x.name)}</td>
          <td>${esc(x.country || "")}</td>
          <td>${esc(x.geocode_provider || "")}</td>
          <td style="max-width:200px;overflow:hidden;text-overflow:ellipsis">${esc(x.geocode_query || "")}</td>
          <td>${fmtDate(x.geocoded_at)}</td>
          <td style="color:var(--err)">${esc(x.geocode_error || "")}</td>
        </tr>`).join("") || `<tr><td colspan="7" class="empty">Aucun</td></tr>`}
      </tbody>
    </table>`;
  } catch (e) {
    const pctEl = document.getElementById("geo-pct");
    if (pctEl) pctEl.textContent = `Erreur : ${esc(e.message)}`;
  }
}

// ------------------------------------------------------------------
// LLM — debug des appels Groq (désambiguïsation géocodage)
// ------------------------------------------------------------------
async function renderLLM() {
  content().innerHTML = `
    <div class="toolbar">
      <input type="text" id="llm-q" placeholder="Filtrer par lieu / ville…" style="min-width:220px">
      <select id="llm-model">
        <option value="">Modèle: tous</option>
        <option value="llama-3.3-70b-versatile">llama-3.3-70b-versatile</option>
        <option value="llama-3.1-8b-instant">llama-3.1-8b-instant</option>
      </select>
      <label style="display:flex;align-items:center;gap:6px;font-size:13px">
        <input type="checkbox" id="llm-null"> Uniquement city=null
      </label>
      <button class="btn" id="llm-apply">Filtrer</button>
      <span id="llm-status" style="font-size:12px;color:var(--muted)"></span>
    </div>
    <p style="font-size:12px;color:var(--muted);margin:0 0 12px">
      Chaque appel Groq est mis en cache (clé = modèle + pays + lieu) et relié au groupe déclencheur.
      Déplie une ligne pour voir le prompt exact et la réponse brute du modèle.
    </p>
    <div id="llm-list" class="table-wrap"><div class="loading">Chargement…</div></div>
    <div class="pager" id="llm-pager"></div>
  `;
  if (!state.llm) state.llm = { page: 1 };
  document.getElementById("llm-apply").addEventListener("click", () => { state.llm.page = 1; loadLLM(); });
  // Délégation attachée une seule fois sur le conteneur persistant.
  document.getElementById("llm-list").addEventListener("click", (e) => {
    const det = e.target.closest(".llm-detail");
    if (det) {
      const row = document.querySelector(`.llm-detail-row[data-id="${det.dataset.id}"]`);
      if (row) row.style.display = row.style.display === "none" ? "" : "none";
      return;
    }
    const bandLink = e.target.closest(".llm-band");
    if (bandLink) { e.preventDefault(); openBandModal(Number(bandLink.dataset.ma)); }
  });
  await loadLLM();
}

async function loadLLM() {
  const q = document.getElementById("llm-q")?.value.trim() || "";
  const model = document.getElementById("llm-model")?.value || "";
  const onlyNull = document.getElementById("llm-null")?.checked ? "1" : "";
  const page = state.llm?.page || 1;
  const params = new URLSearchParams({ page: String(page), pageSize: "50" });
  if (q) params.set("q", q);
  if (model) params.set("model", model);
  if (onlyNull) params.set("only_null", "1");
  try {
    const r = await api(`/admin/api/llm?${params.toString()}`);
    const st = document.getElementById("llm-status");
    if (st) st.textContent = `${r.total.toLocaleString("fr-FR")} appel(s) LLM`;
    const rows = (r.items || []).map((c) => {
      const res = c.is_null
        ? `<span style="color:var(--warn)">null</span>`
        : `${esc(c.parsed_city || "?")} (${esc(c.parsed_country || "?")})`;
      const cost = c.cost_usd != null ? `$${Number(c.cost_usd).toFixed(6)}` : "—";
      const band = c.ma_id
        ? `<a href="#" data-ma="${c.ma_id}" class="llm-band">${esc(c.band_name || ("#" + c.ma_id))}</a>`
        : "—";
      return `<tr>
        <td>${fmtDate(c.created_at)}</td>
        <td>${band}</td>
        <td style="font-size:11px">${esc(c.model)}</td>
        <td>${esc(c.location_raw || "—")}${c.country ? ` <span style="color:var(--muted)">[${esc(c.country)}]</span>` : ""}</td>
        <td>${res}</td>
        <td>${(c.tokens_in || 0)}+${(c.tokens_out || 0)}</td>
        <td>${cost}</td>
        <td><button class="btn btn-mini llm-detail" data-id="${c.id}">Prompt/réponse</button></td>
      </tr>
      <tr class="llm-detail-row" data-id="${c.id}" style="display:none"><td colspan="8">
        <div style="font-size:11px;color:var(--muted);white-space:pre-wrap;background:rgba(255,255,255,0.03);padding:8px;border-radius:4px">PROMPT:\n${esc(c.prompt || "")}\n\nRESPONSE:\n${esc(c.response || "")}</div>
      </td></tr>`;
    }).join("");
    document.getElementById("llm-list").innerHTML = rows
      ? `<table><thead><tr><th>Date</th><th>Groupe</th><th>Modèle</th><th>Lieu (brut)</th><th>Extrait</th><th>Tokens</th><th>Coût</th><th></th></tr></thead><tbody>${rows}</tbody></table>`
      : `<div class="empty">Aucun appel LLM.</div>`;
    const totalPages = Math.max(1, Math.ceil(r.total / 50));
    document.getElementById("llm-pager").innerHTML = `
      <button class="btn btn-mini" id="llm-prev" ${page <= 1 ? "disabled" : ""}>← Préc.</button>
      <span>Page ${page} / ${totalPages}</span>
      <button class="btn btn-mini" id="llm-next" ${page >= totalPages ? "disabled" : ""}>Suiv. →</button>
    `;
    document.getElementById("llm-prev")?.addEventListener("click", () => { state.llm.page = page - 1; loadLLM(); });
    document.getElementById("llm-next")?.addEventListener("click", () => { state.llm.page = page + 1; loadLLM(); });
  } catch (e) {
    document.getElementById("llm-list").innerHTML = `<div class="empty">Erreur : ${esc(e.message)}</div>`;
  }
}

// ------------------------------------------------------------------
// Centre de commandes (Jobs + Géocodeur + Maintenance + Live log)
// ------------------------------------------------------------------
async function renderJobs() {
  content().innerHTML = `
    <div class="grid grid-2" style="margin-bottom:16px">

      <div class="card">
        <h2>🕷 Crawler</h2>
        <p style="font-size:12px;color:var(--muted);margin:0 0 14px">Consommés par le crawler dans sa prochaine itération (~1 min).</p>
        <div style="display:flex;flex-direction:column;gap:8px">
          <button class="btn" data-job="enrich">▶ Enrich (500 bands)</button>
          <button class="btn" data-job="incremental">▶ Crawl incrémental (créations + modifs)</button>
          <button class="btn" data-job="full_crawl" style="border-color:rgba(198,26,26,0.5)">▶ Crawl complet Europe</button>
        </div>
        <div id="job-feedback" style="margin-top:10px;font-size:12px;color:var(--muted)"></div>
      </div>

      <div class="card">
        <h2>🗺 Géocodeur</h2>
        <p style="font-size:12px;color:var(--muted);margin:0 0 14px">Le géocodeur et le worker Groq tournent en continu. L'enqueue parse les bands en band_locations.</p>
        <div style="display:flex;flex-direction:column;gap:8px">
          <button class="btn" id="cmd-enqueue" style="border-color:rgba(63,174,90,0.5);color:var(--ok)">▶ Lancer l'enqueue band_locations</button>
          <button class="btn" id="cmd-reset-errors">🔄 Reset erreurs geocode_queue</button>
          <button class="btn" id="cmd-requeue-all" style="border-color:rgba(198,26,26,0.5)">♻️ Re-géocoder tout geocode_queue</button>
        </div>
        <div id="geo-cmd-feedback" style="margin-top:10px;font-size:12px;color:var(--muted)"></div>
      </div>

      <div class="card">
        <h2>🧹 Maintenance</h2>
        <div style="display:flex;flex-direction:column;gap:8px">
          <button class="btn" id="cmd-cleanup">🧹 Annuler crawl-runs bloqués (&gt;30 min)</button>
        </div>
        <div id="maintenance-feedback" style="margin-top:10px;font-size:12px;color:var(--muted)"></div>
      </div>

    </div>

    <div class="card" style="margin-bottom:16px">
      <h2>Historique des jobs déclenchés</h2>
      <div id="jobs-list"><div class="loading">Chargement…</div></div>
    </div>

    <div class="card">
      <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
        <h2 style="margin:0">Journal en direct <span style="font-weight:400;font-size:12px;color:var(--muted)">(20 dernières lignes — auto-refresh 10s)</span></h2>
        <span id="cmd-log-ts" style="font-size:11px;color:var(--muted)"></span>
      </div>
      <div id="cmd-log-tail" class="table-wrap"><div class="loading">Chargement…</div></div>
    </div>
  `;

  // Crawler jobs
  document.querySelectorAll("[data-job]").forEach((btn) => {
    btn.addEventListener("click", async () => {
      const job_type = btn.dataset.job;
      btn.disabled = true;
      const fb = document.getElementById("job-feedback");
      fb.textContent = "Envoi…"; fb.style.color = "var(--muted)";
      try {
        const r = await api("/admin/api/job-triggers", { method: "POST", body: JSON.stringify({ job_type }) });
        fb.textContent = `✓ Job #${r.id} créé — le crawler le consommera sous ~1 min.`;
        fb.style.color = "var(--ok)";
        await Promise.all([loadJobsList(), loadCmdLogTail()]);
      } catch (e) {
        fb.textContent = `Erreur: ${e.message}`; fb.style.color = "var(--err)";
      } finally { btn.disabled = false; }
    });
  });

  // Geocoder enqueue
  document.getElementById("cmd-enqueue").addEventListener("click", async () => {
    const btn = document.getElementById("cmd-enqueue");
    btn.disabled = true;
    const fb = document.getElementById("geo-cmd-feedback");
    fb.textContent = "Envoi du job…"; fb.style.color = "var(--muted)";
    try {
      await api("/admin/api/job-triggers", { method: "POST", body: JSON.stringify({ job_type: "geocoder_enqueue" }) });
      fb.textContent = "✓ Job geocoder_enqueue créé — le daemon parse les bands en arrière-plan.";
      fb.style.color = "var(--ok)";
      await loadCmdLogTail();
    } catch (e) {
      fb.textContent = `Erreur: ${e.message}`; fb.style.color = "var(--err)";
    } finally { btn.disabled = false; }
  });

  // Geocoder reset errors
  document.getElementById("cmd-reset-errors").addEventListener("click", async () => {
    const btn = document.getElementById("cmd-reset-errors");
    btn.disabled = true;
    const fb = document.getElementById("geo-cmd-feedback");
    fb.textContent = "Réinitialisation…"; fb.style.color = "var(--muted)";
    try {
      const r = await api("/admin/api/geocoding/reset-errors", { method: "POST", body: JSON.stringify({}) });
      fb.textContent = `✓ ${r.count} entrée(s) remise(s) en queue.`;
      fb.style.color = "var(--ok)";
      await loadCmdLogTail();
    } catch (e) {
      fb.textContent = `Erreur: ${e.message}`; fb.style.color = "var(--err)";
    } finally { btn.disabled = false; }
  });

  // Geocoder requeue all
  document.getElementById("cmd-requeue-all").addEventListener("click", async () => {
    if (!confirm("Réinitialiser TOUTES les entrées (erreurs + déjà géocodées) ? Ceci relance le géocodage depuis zéro.")) return;
    const btn = document.getElementById("cmd-requeue-all");
    btn.disabled = true;
    const fb = document.getElementById("geo-cmd-feedback");
    fb.textContent = "Réinitialisation…"; fb.style.color = "var(--muted)";
    try {
      const r = await api("/admin/api/geocoding/requeue-all", { method: "POST", body: JSON.stringify({ include_done: true }) });
      fb.textContent = `✓ ${r.count} entrée(s) re-queueée(s).`;
      fb.style.color = "var(--ok)";
      await loadCmdLogTail();
    } catch (e) {
      fb.textContent = `Erreur: ${e.message}`; fb.style.color = "var(--err)";
    } finally { btn.disabled = false; }
  });

  // Cleanup
  document.getElementById("cmd-cleanup").addEventListener("click", async () => {
    const btn = document.getElementById("cmd-cleanup");
    btn.disabled = true;
    const fb = document.getElementById("maintenance-feedback");
    fb.textContent = "Nettoyage…"; fb.style.color = "var(--muted)";
    try {
      const r = await api("/admin/api/crawl-runs/cleanup", { method: "POST", body: JSON.stringify({ older_than_minutes: 30 }) });
      fb.textContent = `✓ ${r.cleaned} run(s) annulé(s).`;
      fb.style.color = "var(--ok)";
      await loadCmdLogTail();
    } catch (e) {
      fb.textContent = `Erreur: ${e.message}`; fb.style.color = "var(--err)";
    } finally { btn.disabled = false; }
  });

  await Promise.all([loadJobsList(), loadCmdLogTail()]);
  if (state.cmdLogTimer) clearInterval(state.cmdLogTimer);
  state.cmdLogTimer = setInterval(async () => {
    await loadCmdLogTail();
    const ts = document.getElementById("cmd-log-ts");
    if (ts) ts.textContent = `Actualisé à ${new Date().toLocaleTimeString("fr-FR")}`;
  }, 10000);
}

async function loadJobsList() {
  const el = document.getElementById("jobs-list");
  if (!el) return;
  try {
    const r = await api("/admin/api/job-triggers");
    el.innerHTML = `<div class="table-wrap"><table>
      <thead><tr><th>ID</th><th>Type</th><th>Statut</th><th>Demandé par</th><th>Créé le</th><th>Démarré</th><th>Terminé</th><th>Erreur</th></tr></thead>
      <tbody>
        ${r.items.map((x) => `<tr>
          <td>${x.id}</td>
          <td>${esc(x.job_type)}</td>
          <td>${statusBadge(x.status)}</td>
          <td>${esc(x.requested_by || "—")}</td>
          <td>${fmtDate(x.created_at)}</td>
          <td>${fmtDate(x.started_at)}</td>
          <td>${fmtDate(x.finished_at)}</td>
          <td style="color:var(--err)">${esc(x.error || "")}</td>
        </tr>`).join("") || `<tr><td colspan="8" class="empty">Aucun job</td></tr>`}
      </tbody>
    </table></div>`;
  } catch (e) {
    el.innerHTML = `<div class="empty">Erreur : ${esc(e.message)}</div>`;
  }
}

async function loadCmdLogTail() {
  const el = document.getElementById("cmd-log-tail");
  if (!el) return;
  try {
    const r = await api("/admin/api/logs?pageSize=20&page=1");
    el.innerHTML = `<table>
      <thead><tr><th>Date</th><th>Niveau</th><th>Message</th></tr></thead>
      <tbody>
        ${r.items.map((x) => `<tr>
          <td style="white-space:nowrap;color:var(--muted)">${fmtDate(x.created_at)}</td>
          <td><span class="badge ${x.level === "error" ? "err" : x.level === "warning" ? "warn" : "ok"}">${esc(x.level)}</span></td>
          <td style="font-family:monospace;font-size:11px;word-break:break-all">${esc(x.message)}</td>
        </tr>`).join("") || `<tr><td colspan="3" class="empty">Aucun log</td></tr>`}
      </tbody>
    </table>`;
  } catch (e) {
    el.innerHTML = `<div class="empty">Erreur : ${esc(e.message)}</div>`;
  }
}

// ------------------------------------------------------------------
// Checkpoints
// ------------------------------------------------------------------
async function renderCheckpoints() {
  content().innerHTML = `<div class="loading">Chargement…</div>`;
  try {
    const r = await api("/admin/api/crawl-checkpoints");
    content().innerHTML = `
      <div class="table-wrap"><table>
        <thead><tr><th>Clé</th><th>Valeur</th><th>Mis à jour</th></tr></thead>
        <tbody>
          ${r.items.map((x) => `<tr><td>${esc(x.key)}</td><td>${esc(x.value || "—")}</td><td>${fmtDate(x.updated_at)}</td></tr>`).join("") || `<tr><td colspan="3" class="empty">Aucun checkpoint</td></tr>`}
        </tbody>
      </table></div>
    `;
  } catch (e) {
    content().innerHTML = `<div class="empty">Erreur : ${esc(e.message)}</div>`;
  }
}

// ------------------------------------------------------------------
// Audit log
// ------------------------------------------------------------------
async function renderAudit() {
  content().innerHTML = `<div class="loading">Chargement…</div>`;
  try {
    const r = await api(`/admin/api/audit-log?page=1&pageSize=${state.audit.pageSize}`);
    content().innerHTML = `
      <div class="table-wrap"><table>
        <thead><tr><th>Date</th><th>Admin</th><th>Action</th><th>Table</th><th>ID cible</th></tr></thead>
        <tbody>
          ${r.items.map((x) => `
            <tr>
              <td>${fmtDate(x.created_at)}</td>
              <td>${esc(x.admin_username)}</td>
              <td>${esc(x.action)}</td>
              <td>${esc(x.target_table)}</td>
              <td>${esc(x.target_id)}</td>
            </tr>
          `).join("") || `<tr><td colspan="5" class="empty">Aucune action</td></tr>`}
        </tbody>
      </table></div>
    `;
  } catch (e) {
    content().innerHTML = `<div class="empty">Erreur : ${esc(e.message)}</div>`;
  }
}

// ------------------------------------------------------------------
// Boot
// ------------------------------------------------------------------
checkSession();
