SPB Git

Repository Public

admin.html 8.3 KB 178 lines html

1<!DOCTYPE html>2<!--3  QC Élection Forecast — Plateforme de prévision électorale du Québec 20264  Auteur : Simon-Pierre Boucher5  Contact : contact@spboucher.ai6  https://www.qc-election.com7-->8<html lang="fr">9<head>10<script>document.documentElement.dataset.theme=localStorage.getItem("qce-theme")||"dark";</script>11<meta charset="utf-8">12<meta name="viewport" content="width=device-width, initial-scale=1">13<title>Administration — QC Élection Forecast</title>14<link rel="stylesheet" href="/assets/style.css">15</head>16<body>17<header class="site"></header>18<div class="wrap">19  <div class="hero">20    <h1>Administration</h1>21    <p class="sub">Gestion des données, du pipeline et des runs du modèle.</p>22  </div>2324  <div class="card" style="max-width:520px">25    <h3>Authentification</h3>26    <div class="filters">27      <input type="password" id="token" placeholder="Jeton d'administration" style="flex:1">28      <button class="primary" id="login">Connecter</button>29    </div>30    <p class="note" id="auth-status">Non connecté.</p>31  </div>3233  <div id="panel" style="display:none">34    <h2 class="section">Actions</h2>35    <div class="filters">36      <button id="act-refresh">Rafraîchir les données + relancer le forecast</button>37      <button id="act-forecast">Relancer le forecast seul</button>38      <button id="act-backtest">Relancer le backtest 2022</button>39      <span id="act-status" style="color:var(--muted)"></span>40    </div>4142    <div class="grid two">43      <div class="card">44        <h3>Ajouter un sondage</h3>45        <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:10px">46          <input id="np-pollster" type="text" placeholder="Maison (ex. Léger)">47          <input id="np-end" type="date" title="Fin de terrain">48          <input id="np-n" type="number" placeholder="Taille n">49          <select id="np-mode"><option value="web">web</option><option value="phone">téléphone</option>50            <option value="ivr">IVR</option><option value="sms">SMS</option>51            <option value="mixed">mixte</option><option value="unknown">inconnu</option></select>52          <input id="np-url" type="text" placeholder="URL source" style="grid-column:1/3">53        </div>54        <div id="np-shares" style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:10px"></div>55        <div style="margin-top:12px"><button class="primary" id="np-save">Enregistrer</button>56          <span id="np-status" style="color:var(--muted);font-size:.85rem"></span></div>57      </div>58      <div class="card">59        <h3>Exclure / réintégrer un sondage</h3>60        <div class="filters">61          <input id="ex-id" type="number" placeholder="ID sondage" style="width:110px">62          <input id="ex-reason" type="text" placeholder="Raison" style="flex:1">63          <button id="ex-do">Exclure</button>64          <button id="ex-undo">Réintégrer</button>65        </div>66        <p class="note">Les ID sont visibles via l'API <code>/api/polls</code>.</p>67        <h3 style="margin-top:18px">Sources de données</h3>68        <table class="data" id="sources"></table>69      </div>70    </div>7172    <h2 class="section">Derniers runs du modèle</h2>73    <div class="card" style="overflow-x:auto"><table class="data" id="runs"></table></div>7475    <h2 class="section">Journal du pipeline</h2>76    <div class="card" style="overflow-x:auto"><table class="data" id="logs"></table></div>77  </div>78</div>79<footer class="site"></footer>8081<script src="/assets/app.js"></script>82<script>83(async () => {84  const meta = await initShell("/admin");85  const parties = Object.keys(meta.parties);86  document.getElementById("np-shares").innerHTML = parties.map(p =>87    `<label style="font-size:.8rem;color:var(--muted)">${p}88     <input id="np-${p}" type="number" step="0.1" min="0" max="70" style="width:100%"></label>`).join("");8990  let TOKEN = sessionStorage.getItem("qce-admin") || "";91  const H = () => ({ "Authorization": `Bearer ${TOKEN}`, "Content-Type": "application/json" });9293  async function connect() {94    try {95      await getJSON(`${API}/admin/runs?limit=1`, { headers: H() });96      document.getElementById("auth-status").textContent = "Connecté.";97      document.getElementById("panel").style.display = "block";98      sessionStorage.setItem("qce-admin", TOKEN);99      loadTables();100    } catch {101      document.getElementById("auth-status").textContent = "Jeton refusé.";102      document.getElementById("panel").style.display = "none";103    }104  }105  document.getElementById("login").onclick = () => {106    TOKEN = document.getElementById("token").value.trim(); connect();107  };108  if (TOKEN) connect();109110  async function loadTables() {111    const runs = await getJSON(`${API}/admin/runs`, { headers: H() });112    document.getElementById("runs").innerHTML =113      `<tr><th>ID</th><th>Exécuté</th><th>Vu au</th><th>Type</th><th>Sondages</th><th>Simulations</th><th>Version</th></tr>` +114      runs.map(r => `<tr><td>${r.id}</td><td>${r.run_at.slice(0, 16).replace("T", " ")}</td>115        <td>${r.as_of}</td><td>${r.is_backtest ? (r.label || "backtest") : "forecast"}</td>116        <td>${r.n_polls}</td><td>${r.n_sims.toLocaleString("fr-CA")}</td><td>${r.model_version}</td></tr>`).join("");117    const logs = await getJSON(`${API}/admin/logs`, { headers: H() });118    document.getElementById("logs").innerHTML =119      `<tr><th>Date</th><th>Étape</th><th>Statut</th><th>Message</th></tr>` +120      logs.map(l => `<tr><td>${l.at.slice(0, 16).replace("T", " ")}</td><td>${l.step}</td>121        <td style="color:${l.status === "ok" ? "var(--good)" : "var(--plq)"}">${l.status}</td>122        <td style="color:var(--muted);max-width:520px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${l.message}</td></tr>`).join("");123    const srcs = await getJSON(`${API}/admin/sources`, { headers: H() });124    document.getElementById("sources").innerHTML =125      `<tr><th>Source</th><th>Dernier accès</th><th>Statut</th></tr>` +126      srcs.map(s => `<tr><td>${s.name}</td>127        <td>${s.last_fetch ? s.last_fetch.slice(0, 16).replace("T", " ") : "—"}</td>128        <td style="color:var(--muted)">${s.last_status || "—"}</td></tr>`).join("");129  }130131  async function act(url, label) {132    document.getElementById("act-status").textContent = label + "…";133    try {134      const r = await getJSON(url, { method: "POST", headers: H() });135      document.getElementById("act-status").textContent = JSON.stringify(r);136      setTimeout(loadTables, 1500);137    } catch (e) { document.getElementById("act-status").textContent = "erreur : " + e.message; }138  }139  document.getElementById("act-refresh").onclick = () => act(`${API}/admin/refresh`, "pipeline");140  document.getElementById("act-forecast").onclick = () => act(`${API}/admin/forecast`, "forecast");141  document.getElementById("act-backtest").onclick = () => act(`${API}/admin/backtest`, "backtest");142143  document.getElementById("np-save").onclick = async () => {144    const shares = {};145    parties.forEach(p => {146      const v = parseFloat(document.getElementById(`np-${p}`).value);147      if (!isNaN(v)) shares[p] = v;148    });149    const body = {150      pollster: document.getElementById("np-pollster").value.trim(),151      field_end: document.getElementById("np-end").value,152      sample_size: parseInt(document.getElementById("np-n").value) || null,153      mode: document.getElementById("np-mode").value,154      source_url: document.getElementById("np-url").value || null,155      shares,156    };157    try {158      const r = await getJSON(`${API}/admin/poll`, { method: "POST", headers: H(), body: JSON.stringify(body) });159      document.getElementById("np-status").textContent = `Créé (id ${r.id}). Relancez le forecast.`;160    } catch (e) { document.getElementById("np-status").textContent = "erreur : " + e.message; }161  };162163  async function patchPoll(excluded) {164    const id = document.getElementById("ex-id").value;165    if (!id) return;166    try {167      await getJSON(`${API}/admin/poll/${id}`, { method: "PATCH", headers: H(),168        body: JSON.stringify({ excluded, exclusion_reason: document.getElementById("ex-reason").value || null }) });169      document.getElementById("act-status").textContent = `Sondage ${id} ${excluded ? "exclu" : "réintégré"}.`;170    } catch (e) { document.getElementById("act-status").textContent = "erreur : " + e.message; }171  }172  document.getElementById("ex-do").onclick = () => patchPoll(true);173  document.getElementById("ex-undo").onclick = () => patchPoll(false);174})();175</script>176</body>177</html>178
Files 37f4ccc0b3eb0e39b9cfda7f484dd925993656ce
Menu

Theme