#!/usr/bin/env python3
"""Build the DEF XI speakers dashboard as a single HTML file."""
import json, os

with open(os.path.join(os.path.dirname(__file__), 'speakers.json')) as f:
    speakers = json.load(f)

data_json = json.dumps(speakers, ensure_ascii=False, separators=(',', ':'))

countries = sorted(set(s['country'] for s in speakers if s['country']))
org_types = sorted(set(s['org_type'] for s in speakers))
seniorities = sorted(set(s['seniority'] for s in speakers))
sectors = sorted(set(sec for s in speakers for sec in s['sectors']))
company_types = sorted(set(s.get('company_type', '') for s in speakers if s.get('company_type')))

def opts_json(values):
    return json.dumps(values, ensure_ascii=False)

html = f'''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DEF XI Speakers — Dashboard</title>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0f1117; color: #e0e0e0; }}
a {{ color: #7cb3ff; text-decoration: none; }}
a:hover {{ text-decoration: underline; }}

.header {{ background: linear-gradient(135deg, #1a1d2e, #252a3a); padding: 24px 32px; border-bottom: 1px solid #2a2f3f; }}
.header h1 {{ font-size: 22px; font-weight: 600; color: #fff; }}
.header p {{ font-size: 13px; color: #888; margin-top: 4px; }}

.stats {{ display: flex; gap: 12px; padding: 16px 32px; background: #13151f; flex-wrap: wrap; }}
.stat {{ background: #1a1d2e; border: 1px solid #2a2f3f; border-radius: 8px; padding: 12px 18px; min-width: 120px; }}
.stat .num {{ font-size: 28px; font-weight: 700; color: #7cb3ff; }}
.stat .label {{ font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: .5px; margin-top: 2px; }}

.filters {{ display: flex; gap: 10px; padding: 16px 32px; background: #13151f; flex-wrap: wrap; align-items: end; border-bottom: 1px solid #2a2f3f; }}
.filter-group {{ display: flex; flex-direction: column; gap: 4px; }}
.filter-group > label {{ font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: .5px; }}
.filter-group input {{ background: #1a1d2e; border: 1px solid #2a2f3f; color: #e0e0e0; padding: 8px 12px; border-radius: 6px; font-size: 13px; outline: none; width: 240px; }}
.filter-group input:focus {{ border-color: #7cb3ff; }}
.filter-group select {{ background: #1a1d2e; border: 1px solid #2a2f3f; color: #e0e0e0; padding: 8px 12px; border-radius: 6px; font-size: 13px; outline: none; min-width: 130px; }}
.btn-reset {{ background: #2a2f3f; border: 1px solid #3a3f4f; color: #aaa; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; align-self: end; }}
.btn-reset:hover {{ background: #3a3f4f; color: #fff; }}

/* Multi-select dropdown */
.ms {{ position: relative; min-width: 160px; }}
.ms-toggle {{ background: #1a1d2e; border: 1px solid #2a2f3f; color: #e0e0e0; padding: 6px 28px 6px 10px; border-radius: 6px; font-size: 13px; cursor: pointer; min-height: 34px; display: flex; flex-wrap: wrap; gap: 3px; align-items: center; position: relative; }}
.ms-toggle:hover {{ border-color: #3a4f6f; }}
.ms-toggle.open {{ border-color: #7cb3ff; }}
.ms-toggle::after {{ content: "\\25BC"; font-size: 9px; color: #666; position: absolute; right: 10px; top: 50%; transform: translateY(-50%); }}
.ms-placeholder {{ color: #666; }}
.ms-chip {{ background: #253040; color: #7cb3ff; padding: 1px 6px; border-radius: 3px; font-size: 11px; display: flex; align-items: center; gap: 4px; white-space: nowrap; }}
.ms-chip-x {{ cursor: pointer; color: #557; font-size: 13px; line-height: 1; }}
.ms-chip-x:hover {{ color: #f66; }}
.ms-dropdown {{ display: none; position: absolute; top: 100%; left: 0; right: 0; margin-top: 4px; background: #1a1d2e; border: 1px solid #2a2f3f; border-radius: 6px; z-index: 100; max-height: 280px; overflow: hidden; box-shadow: 0 8px 24px rgba(0,0,0,.4); }}
.ms-dropdown.open {{ display: block; }}
.ms-search {{ width: 100%; background: #13151f; border: none; border-bottom: 1px solid #2a2f3f; color: #e0e0e0; padding: 8px 10px; font-size: 12px; outline: none; }}
.ms-actions {{ display: flex; gap: 0; border-bottom: 1px solid #2a2f3f; }}
.ms-action {{ flex: 1; padding: 5px 10px; font-size: 11px; color: #7cb3ff; cursor: pointer; text-align: center; background: none; border: none; }}
.ms-action:hover {{ background: #252a3a; }}
.ms-action + .ms-action {{ border-left: 1px solid #2a2f3f; }}
.ms-options {{ max-height: 200px; overflow-y: auto; }}
.ms-option {{ padding: 6px 10px; cursor: pointer; font-size: 12px; display: flex; align-items: center; gap: 8px; }}
.ms-option:hover {{ background: #252a3a; }}
.ms-option.selected {{ color: #7cb3ff; }}
.ms-cb {{ width: 14px; height: 14px; border: 1px solid #3a3f4f; border-radius: 3px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: 10px; }}
.ms-option.selected .ms-cb {{ background: #7cb3ff; border-color: #7cb3ff; color: #0f1117; }}
.ms-options::-webkit-scrollbar {{ width: 6px; }}
.ms-options::-webkit-scrollbar-thumb {{ background: #2a2f3f; border-radius: 3px; }}

.table-wrap {{ padding: 0 32px 32px; overflow-x: auto; }}
table {{ width: 100%; border-collapse: collapse; margin-top: 16px; font-size: 13px; }}
thead th {{ position: sticky; top: 0; background: #1a1d2e; color: #aaa; text-align: left; padding: 10px 12px; border-bottom: 2px solid #2a2f3f; cursor: pointer; user-select: none; white-space: nowrap; font-size: 11px; text-transform: uppercase; letter-spacing: .5px; }}
thead th:hover {{ color: #7cb3ff; }}
thead th.sorted-asc::after {{ content: " \\25B2"; font-size: 9px; }}
thead th.sorted-desc::after {{ content: " \\25BC"; font-size: 9px; }}
tbody tr {{ border-bottom: 1px solid #1e2130; transition: background .15s; }}
tbody tr:hover {{ background: #1a1d2e; }}
td {{ padding: 10px 12px; vertical-align: top; }}
td.name-cell {{ font-weight: 600; color: #fff; }}
td.title-cell {{ max-width: 300px; }}

.tag {{ display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 500; margin: 1px 2px; }}
.tag-org {{ background: #1e2a3a; color: #7cb3ff; }}
.tag-sen {{ background: #2a1e2a; color: #c77dba; }}
.tag-sec {{ background: #1e2a22; color: #7dbf8e; }}
.tag-country {{ background: #2a2a1e; color: #c7b77d; }}

.photo {{ width: 36px; height: 36px; border-radius: 50%; object-fit: cover; background: #2a2f3f; }}

.btn-reject {{ background: none; border: 1px solid #3a2020; color: #aa5555; border-radius: 4px; cursor: pointer; padding: 2px 8px; font-size: 11px; transition: all .15s; }}
.btn-reject:hover {{ background: #3a2020; color: #ff6666; }}
.btn-reject.rejected {{ background: #3a2020; color: #ff6666; border-color: #ff6666; }}
tr.row-rejected {{ opacity: 0.3; }}
tr.row-rejected:hover {{ opacity: 0.6; }}

.btn-star {{ background: none; border: 1px solid #2a2a1e; color: #776630; border-radius: 4px; cursor: pointer; padding: 2px 8px; font-size: 13px; transition: all .15s; line-height: 1; }}
.btn-star:hover {{ background: #2a2a1e; color: #eec040; }}
.btn-star.starred {{ background: #2a2a1e; color: #eec040; border-color: #eec040; }}
tr.row-starred {{ background: #151a12; }}

.table-actions {{ display: flex; align-items: center; gap: 12px; padding: 12px 32px 0; flex-wrap: wrap; }}
.btn-reject-all {{ background: none; border: 1px solid #3a2020; color: #aa5555; border-radius: 6px; cursor: pointer; padding: 6px 14px; font-size: 12px; transition: all .15s; }}
.btn-reject-all:hover {{ background: #3a2020; color: #ff6666; }}
.btn-star-all {{ background: none; border: 1px solid #2a2a1e; color: #776630; border-radius: 6px; cursor: pointer; padding: 6px 14px; font-size: 12px; transition: all .15s; }}
.btn-star-all:hover {{ background: #2a2a1e; color: #eec040; }}
.table-actions .info {{ font-size: 12px; color: #666; }}
.table-actions .sep {{ color: #2a2f3f; }}

.bar-chart {{ display: flex; flex-direction: column; gap: 3px; }}
.bar-row {{ display: flex; align-items: center; gap: 8px; font-size: 12px; }}
.bar-label {{ width: 140px; text-align: right; color: #aaa; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }}
.bar {{ height: 16px; background: #7cb3ff; border-radius: 3px; transition: width .3s; min-width: 2px; }}
.bar-count {{ color: #888; font-size: 11px; min-width: 30px; }}

.charts {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 20px; padding: 16px 32px; }}
.chart-card {{ background: #1a1d2e; border: 1px solid #2a2f3f; border-radius: 8px; padding: 16px; }}
.chart-card h3 {{ font-size: 13px; color: #aaa; text-transform: uppercase; letter-spacing: .5px; margin-bottom: 12px; }}

.hidden {{ display: none; }}
.tab-bar {{ display: flex; gap: 0; padding: 0 32px; background: #13151f; }}
.tab {{ padding: 10px 20px; cursor: pointer; font-size: 13px; color: #888; border-bottom: 2px solid transparent; transition: all .15s; }}
.tab:hover {{ color: #ccc; }}
.tab.active {{ color: #7cb3ff; border-bottom-color: #7cb3ff; }}
</style>
</head>
<body>

<div class="header">
  <h1>Delphi Economic Forum XI — Speakers</h1>
  <p>1 092 speakers &middot; dashboard interactif</p>
</div>

<div class="stats" id="stats"></div>

<div class="filters">
  <div class="filter-group">
    <label>Recherche</label>
    <input type="text" id="search" placeholder="Nom, titre, organisation...">
  </div>
  <div class="filter-group">
    <label>Pays</label>
    <div class="ms" id="ms-country"></div>
  </div>
  <div class="filter-group">
    <label>Type d'org</label>
    <div class="ms" id="ms-orgtype"></div>
  </div>
  <div class="filter-group">
    <label>S&eacute;niorit&eacute;</label>
    <div class="ms" id="ms-seniority"></div>
  </div>
  <div class="filter-group">
    <label>Secteur</label>
    <div class="ms" id="ms-sector"></div>
  </div>
  <div class="filter-group">
    <label>Type entreprise</label>
    <div class="ms" id="ms-comptype"></div>
  </div>
  <div class="filter-group">
    <label>Statut</label>
    <div class="ms" id="ms-status"></div>
  </div>
  <button class="btn-reset" onclick="resetFilters()">Reset</button>
</div>

<div class="tab-bar">
  <div class="tab active" data-tab="table" onclick="switchTab('table')">Table</div>
  <div class="tab" data-tab="charts" onclick="switchTab('charts')">Charts</div>
</div>

<div id="tab-table">
  <div class="table-actions" id="table-actions"></div>
  <div class="table-wrap">
    <table>
      <thead>
        <tr>
          <th data-col="reject" style="cursor:default"></th>
          <th data-col="photo" style="cursor:default"></th>
          <th data-col="name">Nom</th>
          <th data-col="title">Titre</th>
          <th data-col="organization">Organisation</th>
          <th data-col="country">Pays</th>
          <th data-col="org_type">Type</th>
          <th data-col="seniority">S&eacute;niorit&eacute;</th>
          <th data-col="sectors">Secteurs</th>
        </tr>
      </thead>
      <tbody id="tbody"></tbody>
    </table>
  </div>
</div>

<div id="tab-charts" class="hidden">
  <div class="charts">
    <div class="chart-card"><h3>Top 20 Pays</h3><div id="chart-country" class="bar-chart"></div></div>
    <div class="chart-card"><h3>Type d'organisation</h3><div id="chart-orgtype" class="bar-chart"></div></div>
    <div class="chart-card"><h3>S&eacute;niorit&eacute;</h3><div id="chart-seniority" class="bar-chart"></div></div>
    <div class="chart-card"><h3>Secteurs</h3><div id="chart-sector" class="bar-chart"></div></div>
  </div>
</div>

<script>
const DATA = {data_json};

let filtered = [...DATA];
let sortCol = 'name';
let sortDir = 1;

const $ = id => document.getElementById(id);
const qs = s => document.querySelectorAll(s);

// ── Multi-select component ──────────────────────────────────
class MultiSelect {{
  constructor(containerId, options, placeholder) {{
    this.container = $(containerId);
    this.allOptions = options;
    this.selected = new Set();
    this.placeholder = placeholder || 'Tous';
    this.open = false;
    this.searchText = '';
    this.build();
  }}

  build() {{
    this.container.innerHTML = `
      <div class="ms-toggle">
        <span class="ms-placeholder">${{this.placeholder}}</span>
      </div>
      <div class="ms-dropdown">
        <input class="ms-search" placeholder="Filtrer..." type="text">
        <div class="ms-actions">
          <button class="ms-action ms-select-all">Tout</button>
          <button class="ms-action ms-select-none">Aucun</button>
        </div>
        <div class="ms-options"></div>
      </div>`;
    this.toggle = this.container.querySelector('.ms-toggle');
    this.dropdown = this.container.querySelector('.ms-dropdown');
    this.searchInput = this.container.querySelector('.ms-search');
    this.optionsEl = this.container.querySelector('.ms-options');

    this.toggle.addEventListener('click', e => {{
      if (e.target.classList.contains('ms-chip-x')) return;
      this.setOpen(!this.open);
    }});
    this.searchInput.addEventListener('input', () => {{
      this.searchText = this.searchInput.value.toLowerCase();
      this.renderOptions();
    }});
    this.container.querySelector('.ms-select-all').addEventListener('click', () => {{
      const visible = this.allOptions.filter(o => o.toLowerCase().includes(this.searchText));
      visible.forEach(o => this.selected.add(o));
      this.renderToggle();
      this.renderOptions();
      render();
    }});
    this.container.querySelector('.ms-select-none').addEventListener('click', () => {{
      if (this.searchText) {{
        const visible = this.allOptions.filter(o => o.toLowerCase().includes(this.searchText));
        visible.forEach(o => this.selected.delete(o));
      }} else {{
        this.selected.clear();
      }}
      this.renderToggle();
      this.renderOptions();
      render();
    }});
    this.renderOptions();
  }}

  setOpen(v) {{
    this.open = v;
    this.dropdown.classList.toggle('open', v);
    this.toggle.classList.toggle('open', v);
    if (v) {{
      this.searchInput.value = '';
      this.searchText = '';
      this.renderOptions();
      setTimeout(() => this.searchInput.focus(), 0);
    }}
  }}

  renderToggle() {{
    const chips = [...this.selected].map(v =>
      `<span class="ms-chip">${{esc(v)}}<span class="ms-chip-x" data-val="${{esc(v)}}">&times;</span></span>`
    ).join('');
    this.toggle.innerHTML = this.selected.size
      ? chips
      : `<span class="ms-placeholder">${{this.placeholder}}</span>`;
    // Re-add the chevron pseudo-element is CSS-based, no action needed
    // Bind chip remove
    this.toggle.querySelectorAll('.ms-chip-x').forEach(x => {{
      x.addEventListener('click', e => {{
        e.stopPropagation();
        this.selected.delete(x.dataset.val);
        this.renderToggle();
        this.renderOptions();
        render();
      }});
    }});
  }}

  renderOptions() {{
    const filtered = this.allOptions.filter(o => o.toLowerCase().includes(this.searchText));
    this.optionsEl.innerHTML = filtered.map(o => {{
      const sel = this.selected.has(o) ? 'selected' : '';
      return `<div class="ms-option ${{sel}}" data-val="${{esc(o)}}"><div class="ms-cb">${{sel ? '&#10003;' : ''}}</div>${{esc(o)}}</div>`;
    }}).join('');
    this.optionsEl.querySelectorAll('.ms-option').forEach(el => {{
      el.addEventListener('click', () => {{
        const v = el.dataset.val;
        if (this.selected.has(v)) this.selected.delete(v);
        else this.selected.add(v);
        this.renderToggle();
        this.renderOptions();
        render();
      }});
    }});
  }}

  getSelected() {{ return this.selected; }}

  clear() {{
    this.selected.clear();
    this.renderToggle();
    this.renderOptions();
  }}
}}

// ── Init multi-selects ──────────────────────────────────────
const msCountry   = new MultiSelect('ms-country',   ['(sans pays)', ...{opts_json(countries)}],   'Tous les pays');
const msOrgType   = new MultiSelect('ms-orgtype',   {opts_json(org_types)},   'Tous les types');
const msSeniority = new MultiSelect('ms-seniority', {opts_json(seniorities)}, 'Toutes');
const msSector    = new MultiSelect('ms-sector',    {opts_json(sectors)},     'Tous les secteurs');
const msCompType  = new MultiSelect('ms-comptype',  {opts_json(company_types)},  'Tous');
const msStatus    = new MultiSelect('ms-status',    ['Non rejetés', 'Non sélectionnés', 'Sélectionnés', 'Rejetés'], 'Tous les statuts');

const allMs = [msCountry, msOrgType, msSeniority, msSector, msCompType, msStatus];

// Close dropdowns on outside click
document.addEventListener('click', e => {{
  allMs.forEach(ms => {{
    if (!ms.container.contains(e.target)) ms.setOpen(false);
  }});
}});

// ── State — synced to server + localStorage fallback ────────
let rejected = new Set();
let starred = new Set();
let syncTimeout = null;

function loadStateFromStorage() {{
  rejected = new Set(JSON.parse(localStorage.getItem('def-xi-rejected') || '[]'));
  starred = new Set(JSON.parse(localStorage.getItem('def-xi-starred') || '[]'));
}}

function saveToStorage() {{
  localStorage.setItem('def-xi-rejected', JSON.stringify([...rejected]));
  localStorage.setItem('def-xi-starred', JSON.stringify([...starred]));
}}

function syncToServer() {{
  clearTimeout(syncTimeout);
  syncTimeout = setTimeout(() => {{
    fetch('/api/state', {{
      method: 'POST',
      headers: {{'Content-Type': 'application/json'}},
      body: JSON.stringify({{ rejected: [...rejected], starred: [...starred] }})
    }}).catch(() => {{}});
  }}, 400);
}}

function saveState() {{
  saveToStorage();
  syncToServer();
}}

async function loadState() {{
  loadStateFromStorage();
  try {{
    const res = await fetch('/api/state');
    if (res.ok) {{
      const state = await res.json();
      if (state.rejected?.length || state.starred?.length) {{
        rejected = new Set(state.rejected || []);
        starred = new Set(state.starred || []);
        saveToStorage();
      }}
    }}
  }} catch(e) {{}}
}}

function toggleReject(profileUrl) {{
  if (rejected.has(profileUrl)) rejected.delete(profileUrl);
  else {{ rejected.add(profileUrl); starred.delete(profileUrl); }}
  saveState();
  render();
}}

function isRejected(s) {{ return rejected.has(s.profile_url); }}

function rejectAllFiltered() {{
  filtered.filter(s => !isRejected(s)).forEach(s => {{ rejected.add(s.profile_url); starred.delete(s.profile_url); }});
  saveState();
  render();
}}

function toggleStar(profileUrl) {{
  if (starred.has(profileUrl)) starred.delete(profileUrl);
  else {{ starred.add(profileUrl); rejected.delete(profileUrl); }}
  saveState();
  render();
}}

function isStarred(s) {{ return starred.has(s.profile_url); }}

function starAllFiltered() {{
  filtered.filter(s => !isRejected(s) && !isStarred(s)).forEach(s => starred.add(s.profile_url));
  saveState();
  render();
}}

// ── Filtering ───────────────────────────────────────────────
function getFiltered() {{
  const q = $('search').value.toLowerCase();
  const countries = msCountry.getSelected();
  const orgTypes = msOrgType.getSelected();
  const seniorities = msSeniority.getSelected();
  const sectors = msSector.getSelected();
  const compTypes = msCompType.getSelected();
  const statuses = msStatus.getSelected();

  return DATA.filter(s => {{
    if (q && !(s.name + ' ' + s.title + ' ' + s.organization).toLowerCase().includes(q)) return false;
    if (countries.size && !countries.has(s.country) && !(countries.has('(sans pays)') && !s.country)) return false;
    if (orgTypes.size && !orgTypes.has(s.org_type)) return false;
    if (seniorities.size && !seniorities.has(s.seniority)) return false;
    if (sectors.size && !s.sectors.some(sec => sectors.has(sec))) return false;
    if (compTypes.size && !compTypes.has(s.company_type || '')) return false;
    if (statuses.size) {{
      const rej = isRejected(s), star = isStarred(s);
      const active = !rej;
      const match = (statuses.has('Rejetés') && rej)
                 || (statuses.has('Sélectionnés') && star)
                 || (statuses.has('Non rejetés') && active)
                 || (statuses.has('Non sélectionnés') && !star && !rej);
      if (!match) return false;
    }}
    return true;
  }});
}}

function sortData(data) {{
  return data.sort((a, b) => {{
    let va = sortCol === 'sectors' ? a[sortCol].join(',') : (a[sortCol] || '');
    let vb = sortCol === 'sectors' ? b[sortCol].join(',') : (b[sortCol] || '');
    return va.localeCompare(vb) * sortDir;
  }});
}}

function render() {{
  filtered = sortData(getFiltered());
  renderStats();
  renderTable();
  renderCharts();
}}

function renderStats() {{
  const countries = new Set(filtered.map(s => s.country).filter(Boolean));
  const html = [
    `<div class="stat"><div class="num">${{filtered.length}}</div><div class="label">Speakers</div></div>`,
    `<div class="stat"><div class="num">${{countries.size}}</div><div class="label">Pays</div></div>`,
    `<div class="stat"><div class="num">${{filtered.filter(s => s.seniority === 'C-Level').length}}</div><div class="label">C-Level</div></div>`,
    `<div class="stat"><div class="num">${{filtered.filter(s => s.org_type === 'Corporate').length}}</div><div class="label">Corporate</div></div>`,
    `<div class="stat"><div class="num">${{filtered.filter(s => s.sectors.includes('Tech & Digital')).length}}</div><div class="label">Tech / AI</div></div>`,
    `<div class="stat"><div class="num">${{filtered.filter(s => s.org_type === 'Government & Politics').length}}</div><div class="label">Gov & Politics</div></div>`,
    `<div class="stat"><div class="num">${{starred.size}}</div><div class="label">S&eacute;lectionn&eacute;s</div></div>`,
    `<div class="stat"><div class="num">${{rejected.size}}</div><div class="label">Rejet&eacute;s</div></div>`,
  ];
  $('stats').innerHTML = html.join('');
}}

function renderTable() {{
  const nonRej = filtered.filter(s => !isRejected(s)).length;
  const nonStarred = filtered.filter(s => !isRejected(s) && !isStarred(s)).length;
  let actions = [];
  if (nonStarred > 0) actions.push(`<button class="btn-star-all" onclick="starAllFiltered()">&#x2605; S&eacute;lectionner les ${{nonStarred}}</button>`);
  if (nonRej > 0) actions.push(`<button class="btn-reject-all" onclick="rejectAllFiltered()">&#x2717; Rejeter les ${{nonRej}}</button>`);
  $('table-actions').innerHTML = actions.join('<span class="sep">|</span>');
  const limit = 200;
  const rows = filtered.slice(0, limit).map(s => {{
    const rej = isRejected(s);
    const star = isStarred(s);
    const rowClass = rej ? 'row-rejected' : star ? 'row-starred' : '';
    return `
    <tr class="${{rowClass}}">
      <td><button class="btn-star ${{star ? 'starred' : ''}}" onclick="toggleStar('${{s.profile_url}}')" title="${{star ? 'D&eacute;s&eacute;lectionner' : 'S&eacute;lectionner'}}">${{star ? '&#x2605;' : '&#x2606;'}}</button><button class="btn-reject ${{rej ? 'rejected' : ''}}" onclick="toggleReject('${{s.profile_url}}')" title="${{rej ? 'Restaurer' : 'Rejeter'}}" style="margin-left:4px">${{rej ? '&#x21a9;' : '&#x2717;'}}</button></td>
      <td>${{s.photo_url ? `<img class="photo" src="${{s.photo_url}}" loading="lazy" onerror="this.style.display='none'">` : ''}}</td>
      <td class="name-cell"><a href="${{s.profile_url}}" target="_blank">${{esc(s.name)}}</a></td>
      <td class="title-cell">${{esc(s.title)}}</td>
      <td>${{esc(s.organization)}}</td>
      <td>${{s.country ? `<span class="tag tag-country">${{esc(s.country)}}</span>` : ''}}</td>
      <td><span class="tag tag-org">${{esc(s.org_type)}}</span></td>
      <td><span class="tag tag-sen">${{esc(s.seniority)}}</span></td>
      <td>${{s.sectors.filter(x => x !== 'General / Unclassified').map(x => `<span class="tag tag-sec">${{esc(x)}}</span>`).join('')}}</td>
    </tr>`;
  }}).join('');
  $('tbody').innerHTML = rows;
  if (filtered.length > limit) {{
    $('tbody').innerHTML += `<tr><td colspan="9" style="text-align:center;color:#888;padding:16px">... et ${{filtered.length - limit}} autres (affiner les filtres)</td></tr>`;
  }}
  qs('thead th').forEach(th => {{
    th.classList.remove('sorted-asc', 'sorted-desc');
    if (th.dataset.col === sortCol) th.classList.add(sortDir === 1 ? 'sorted-asc' : 'sorted-desc');
  }});
}}

function renderCharts() {{
  renderBarChart('chart-country', countBy(filtered, 'country', 20));
  renderBarChart('chart-orgtype', countBy(filtered, 'org_type'));
  renderBarChart('chart-seniority', countBy(filtered, 'seniority'));
  renderBarChart('chart-sector', countBySectors(filtered));
}}

function countBy(data, key, limit) {{
  const counts = {{}};
  data.forEach(s => {{ const v = s[key] || '(non renseigné)'; counts[v] = (counts[v] || 0) + 1; }});
  let entries = Object.entries(counts).sort((a, b) => b[1] - a[1]);
  if (limit) entries = entries.slice(0, limit);
  return entries;
}}

function countBySectors(data) {{
  const counts = {{}};
  data.forEach(s => s.sectors.forEach(sec => {{ if (sec !== 'General / Unclassified') counts[sec] = (counts[sec] || 0) + 1; }}));
  return Object.entries(counts).sort((a, b) => b[1] - a[1]);
}}

function renderBarChart(id, entries) {{
  if (!entries.length) {{ $(id).innerHTML = '<div style="color:#666">Pas de données</div>'; return; }}
  const max = entries[0][1];
  $(id).innerHTML = entries.map(([label, count]) => `
    <div class="bar-row">
      <div class="bar-label" title="${{label}}">${{label}}</div>
      <div class="bar" style="width:${{Math.round(count / max * 180)}}px"></div>
      <div class="bar-count">${{count}}</div>
    </div>
  `).join('');
}}

function esc(s) {{ const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }}

function resetFilters() {{
  $('search').value = '';
  allMs.forEach(ms => ms.clear());
  render();
}}

function switchTab(tab) {{
  qs('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab));
  $('tab-table').classList.toggle('hidden', tab !== 'table');
  $('tab-charts').classList.toggle('hidden', tab !== 'charts');
}}

// Sort on header click
document.querySelector('thead').addEventListener('click', e => {{
  const th = e.target.closest('th');
  if (!th || !th.dataset.col || th.dataset.col === 'photo' || th.dataset.col === 'reject') return;
  if (sortCol === th.dataset.col) sortDir *= -1;
  else {{ sortCol = th.dataset.col; sortDir = 1; }}
  render();
}});

// Search filter event
$('search').addEventListener('input', render);

// Load state from server then render
loadState().then(() => render());
</script>
</body>
</html>'''

with open(os.path.join(os.path.dirname(__file__), 'dashboard.html'), 'w') as f:
    f.write(html)

print(f"Dashboard written: {len(html)} chars")
