/* global React */
// ============================================================
// Parcours = SOP MULTI-PHASE : suite ordonnée de phases (chacune avec sa checklist,
// son template Phase Builder, sa catégorie cible, un cooldown et une plage horaire).
// Lot 1 : store (localStorage) + éditeur. L'exécution/les verrous = Lot 2.
// ============================================================

const PARCOURS_KEY = "phonelabs.parcours";

const ParcoursStore = (function () {
  const load = () => { try { return JSON.parse(localStorage.getItem(PARCOURS_KEY)) || []; } catch (e) { return []; } };
  const ls = new Set(); const emit = () => ls.forEach(f => f());
  let _lastEdit = 0;   // anti-écrasement par un backend en retard (comme le stockage de comptes)
  const persist = (arr) => {
    try { localStorage.setItem(PARCOURS_KEY, JSON.stringify(arr)); } catch (e) {}
    _lastEdit = Date.now();
    if (window.Backend && window.Backend.saveParcours) window.Backend.saveParcours(arr).catch(() => {});   // persistance backend (survit au localStorage)
    emit();
  };
  return {
    sub(cb) { ls.add(cb); return () => ls.delete(cb); },
    list() { return load(); },
    get(id) { return load().find(p => p.id === id) || null; },
    save(p) {
      const all = load();
      const i = all.findIndex(x => x.id === p.id);
      if (i >= 0) all[i] = p; else all.unshift(p);
      persist(all);
      return p;
    },
    remove(id) { persist(load().filter(p => p.id !== id)); },
    // Récupère les parcours du backend et fusionne (backend = source de vérité, sauf édition locale récente).
    async syncFromBackend() {
      if (!window.Backend || !window.Backend.getParcours) return;
      const remote = await window.Backend.getParcours();
      if (remote == null) return;                                  // backend injoignable -> on garde le local
      const local = load();
      if (!remote.length) { if (local.length && window.Backend.saveParcours) window.Backend.saveParcours(local).catch(() => {}); return; }
      const localWins = (Date.now() - _lastEdit) < 15000;
      const byId = new Map();
      if (localWins) { local.forEach(p => byId.set(p.id, p)); remote.forEach(p => { if (!byId.has(p.id)) byId.set(p.id, p); }); }
      else { remote.forEach(p => byId.set(p.id, p)); local.forEach(p => { if (!byId.has(p.id)) byId.set(p.id, p); }); }
      const merged = [...byId.values()];
      try { localStorage.setItem(PARCOURS_KEY, JSON.stringify(merged)); } catch (e) {}
      if (localWins && window.Backend.saveParcours) window.Backend.saveParcours(merged).catch(() => {});
      emit();
    },
  };
})();
window.ParcoursStore = ParcoursStore;
// Sync au chargement de l'app (récupère les parcours du backend si le localStorage a été vidé / autre machine).
try { setTimeout(() => { try { ParcoursStore.syncFromBackend(); } catch (e) {} }, 1500); } catch (e) {}

function useParcours() { const [, f] = React.useReducer(x => x + 1, 0); React.useEffect(() => { const u = ParcoursStore.sub(f); ParcoursStore.syncFromBackend().catch(() => {}); return u; }, []); return ParcoursStore; }

// --- Options de template (phases du Phase Builder) + catégories (du stockage de comptes) ---
const PARCOURS_BUILTIN_PHASES = [
  { value: "", label: "— aucun template —" },
  { value: "default", label: "⭐ Config par défaut" },
  { value: "creation_compte", label: "📱 Création de compte" },
  { value: "login_compte", label: "🔑 Login de compte" },
  { value: "echauffement", label: "🔥 Échauffement" },
  { value: "publication", label: "🚀 Publication" },
];
function parcoursPhaseOpts() {
  let custom = []; try { custom = JSON.parse(localStorage.getItem("phonelabs.custom_phases") || "[]"); } catch (e) {}
  return [...PARCOURS_BUILTIN_PHASES, ...custom.filter(c => c && c.value)];
}
function parcoursCategories() {
  try {
    return (JSON.parse(localStorage.getItem("phonelabs.categories") || "[]"))
      .map(c => (typeof c === "string" ? c : (c && (c.label || c.name)))).filter(Boolean);
  } catch (e) { return []; }
}

function _newPhase() {
  return { id: "ph_" + Date.now() + "_" + Math.floor(Math.random() * 1e4), name: "", template: "", category: "", plug: "", cooldownMin: 0, window: { enabled: false, start: "09:00", end: "22:00" }, steps: [""] };
}
function _newParcours() {
  return { id: "parc_" + Date.now(), title: "", createdAt: new Date().toISOString(), draft: true, phases: [{ ..._newPhase(), name: "Phase 1" }] };
}

// ============================================================
// Éditeur d'un Parcours (modal)
// ============================================================
function ParcoursEditor({ parcours, onClose }) {
  const { useState, useEffect, useRef } = React;
  const [p, setP] = useState(() => parcours ? JSON.parse(JSON.stringify(parcours)) : _newParcours());
  const [openPhase, setOpenPhase] = useState(0);
  const [autoMsg, setAutoMsg] = useState("");
  const phaseOpts = parcoursPhaseOpts();
  const cats = parcoursCategories();

  // Auto-sauvegarde : dès la 1re modif, on persiste (débounce 800ms) en gardant le flag brouillon actuel
  // et SANS nettoyer les étapes (le runner ignore les vides). Le travail n'est jamais perdu.
  const _first = useRef(true);
  useEffect(() => {
    if (_first.current) { _first.current = false; return; }
    const t = setTimeout(() => {
      ParcoursStore.save({ ...p, title: (p.title || "").trim() || "Parcours sans nom",
        phases: p.phases.map((ph, i) => ({ ...ph, name: (ph.name || "").trim() || ("Phase " + (i + 1)) })) });
      setAutoMsg("✓ Auto-sauvegardé");
    }, 800);
    return () => clearTimeout(t);
  }, [p]);
  useEffect(() => { if (!autoMsg) return; const id = setTimeout(() => setAutoMsg(""), 1600); return () => clearTimeout(id); }, [autoMsg]);

  const upd = (patch) => setP(prev => ({ ...prev, ...patch }));
  const updPhase = (i, patch) => setP(prev => ({ ...prev, phases: prev.phases.map((ph, j) => j === i ? { ...ph, ...patch } : ph) }));
  const addPhase = () => setP(prev => { const ph = { ..._newPhase(), name: "Phase " + (prev.phases.length + 1) }; setOpenPhase(prev.phases.length); return { ...prev, phases: [...prev.phases, ph] }; });
  const removePhase = (i) => setP(prev => ({ ...prev, phases: prev.phases.filter((_, j) => j !== i) }));
  const movePhase = (i, dir) => setP(prev => { const a = [...prev.phases]; const j = i + dir; if (j < 0 || j >= a.length) return prev; [a[i], a[j]] = [a[j], a[i]]; return { ...prev, phases: a }; });
  const setStep = (pi, si, v) => updPhase(pi, { steps: p.phases[pi].steps.map((s, j) => j === si ? v : s) });
  const addStep = (pi) => updPhase(pi, { steps: [...p.phases[pi].steps, ""] });
  const rmStep = (pi, si) => updPhase(pi, { steps: p.phases[pi].steps.filter((_, j) => j !== si) });
  const moveStep = (pi, si, dir) => setP(prev => { const a = [...prev.phases[pi].steps]; const j = si + dir; if (j < 0 || j >= a.length) return prev; [a[si], a[j]] = [a[j], a[si]]; return { ...prev, phases: prev.phases.map((ph, k) => k === pi ? { ...ph, steps: a } : ph) }; });

  const save = (asDraft) => {
    // brouillon : on garde les étapes telles quelles (même vides) pour pouvoir reprendre l'édition.
    // prêt : on nettoie les étapes vides.
    const clean = { ...p, draft: !!asDraft, title: (p.title || "").trim() || "Parcours sans nom",
      phases: p.phases.map((ph, i) => ({ ...ph, name: (ph.name || "").trim() || ("Phase " + (i + 1)),
        steps: asDraft ? ph.steps : ph.steps.map(s => (s || "").trim()).filter(Boolean) })) };
    ParcoursStore.save(clean);
    onClose && onClose();
  };

  const inp = { width: "100%", boxSizing: "border-box", padding: "8px 10px", background: "var(--surface-2)", border: "1px solid var(--border)", borderRadius: 8, color: "var(--text)", fontSize: 13, outline: "none" };
  const lbl = { fontSize: 10.5, color: "var(--muted)", fontWeight: 700, textTransform: "uppercase", letterSpacing: ".04em", margin: "0 0 4px", display: "block" };
  const totSteps = p.phases.reduce((n, ph) => n + ph.steps.filter(s => (s || "").trim()).length, 0);

  return (
    <div onClick={onClose} style={{ position: "fixed", inset: 0, background: "rgba(2,6,15,.7)", zIndex: 500, display: "grid", placeItems: "center", padding: 16, overflowY: "auto" }}>
      <div onClick={e => e.stopPropagation()} style={{ width: "min(680px, 96vw)", maxHeight: "92vh", overflowY: "auto", background: "var(--bg,#0d1117)", border: "1px solid var(--border)", borderRadius: 16, padding: 22, boxShadow: "0 20px 60px rgba(0,0,0,.5)" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 14 }}>
          <span style={{ fontSize: 18 }}>🧭</span>
          <div style={{ fontSize: 17, fontWeight: 800, flex: 1 }}>{parcours ? "Modifier le parcours" : "Nouveau parcours"}</div>
          {autoMsg && <span style={{ fontSize: 11.5, color: "var(--accent,#1dbab4)", fontWeight: 700 }}>{autoMsg}</span>}
          <span style={{ fontSize: 11.5, color: "var(--faint)" }}>{p.phases.length} phase(s) · {totSteps} étape(s)</span>
        </div>

        <label style={lbl}>Nom du parcours</label>
        <input value={p.title} onChange={e => upd({ title: e.target.value })} style={{ ...inp, marginBottom: 16 }} placeholder="ex : Onboarding compte Instagram" />

        {/* Phases */}
        <div style={{ display: "grid", gap: 10 }}>
          {p.phases.map((ph, i) => {
            const isOpen = openPhase === i;
            return (
              <div key={ph.id} style={{ border: "1px solid var(--border)", borderRadius: 12, overflow: "hidden", background: "var(--surface)" }}>
                <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "10px 12px", background: "var(--surface-2)" }}>
                  <span style={{ width: 22, height: 22, borderRadius: 6, background: "var(--accent-soft, rgba(29,186,180,.15))", color: "var(--accent,#1dbab4)", fontWeight: 800, fontSize: 12, display: "grid", placeItems: "center", flex: "none" }}>{i + 1}</span>
                  <button onClick={() => setOpenPhase(isOpen ? -1 : i)} style={{ flex: 1, textAlign: "left", background: "none", border: "none", color: "var(--text)", fontWeight: 700, fontSize: 13.5, cursor: "pointer", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                    {ph.name || "Phase " + (i + 1)} <span style={{ fontSize: 11, color: "var(--faint)", fontWeight: 400 }}>· {ph.steps.filter(s => (s || "").trim()).length} ét.{ph.cooldownMin ? " · ⏳" + ph.cooldownMin + "min" : ""}{ph.window && ph.window.enabled ? " · 🕒" + ph.window.start + "-" + ph.window.end : ""}</span>
                  </button>
                  <button onClick={() => movePhase(i, -1)} disabled={i === 0} title="Monter" style={{ background: "none", border: "none", color: i === 0 ? "var(--faint)" : "var(--muted)", cursor: i === 0 ? "default" : "pointer", fontSize: 13, padding: "2px 4px" }}>▲</button>
                  <button onClick={() => movePhase(i, 1)} disabled={i === p.phases.length - 1} title="Descendre" style={{ background: "none", border: "none", color: i === p.phases.length - 1 ? "var(--faint)" : "var(--muted)", cursor: i === p.phases.length - 1 ? "default" : "pointer", fontSize: 13, padding: "2px 4px" }}>▼</button>
                  <button onClick={() => removePhase(i)} disabled={p.phases.length <= 1} title="Supprimer la phase" style={{ background: "none", border: "none", color: p.phases.length <= 1 ? "var(--faint)" : "#f87171", cursor: p.phases.length <= 1 ? "default" : "pointer", fontSize: 13, padding: "2px 6px" }}>✕</button>
                  <span style={{ color: "var(--faint)", fontSize: 12, transform: isOpen ? "rotate(90deg)" : "none", transition: "transform .15s" }}>▸</span>
                </div>
                {isOpen && (
                  <div style={{ padding: 12, display: "grid", gap: 12 }}>
                    <div><label style={lbl}>Nom de la phase</label>
                      <input value={ph.name} onChange={e => updPhase(i, { name: e.target.value })} style={inp} placeholder="ex : Création de compte" /></div>
                    <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 10 }}>
                      <div><label style={lbl}>Template</label>
                        <select value={ph.template} onChange={e => updPhase(i, { template: e.target.value })} style={inp}>
                          {phaseOpts.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
                        </select></div>
                      <div><label style={lbl}>Catégorie cible</label>
                        <select value={ph.category} onChange={e => updPhase(i, { category: e.target.value })} style={inp}>
                          <option value="">— inchangée —</option>
                          {(ph.category && cats.indexOf(ph.category) < 0 ? [ph.category, ...cats] : cats).map(c => <option key={c} value={c}>{c}</option>)}
                        </select></div>
                      <div><label style={lbl}>Statut Plug IG</label>
                        <select value={ph.plug || ""} onChange={e => updPhase(i, { plug: e.target.value })} style={inp}>
                          <option value="">— inchangé —</option>
                          <option value="plug">🔌 Marquer Plug</option>
                          <option value="unplug">⬜ Marquer Pas plug</option>
                        </select></div>
                    </div>
                    <div style={{ display: "grid", gridTemplateColumns: "auto 1fr", gap: 10, alignItems: "end" }}>
                      <div><label style={lbl}>Cooldown (min) avant phase suivante</label>
                        <input type="number" min="0" value={ph.cooldownMin} onChange={e => updPhase(i, { cooldownMin: Math.max(0, parseInt(e.target.value) || 0) })} style={{ ...inp, width: 130 }} /></div>
                      <div>
                        <label style={{ ...lbl, display: "inline-flex", alignItems: "center", gap: 6, cursor: "pointer", textTransform: "none", letterSpacing: 0, fontSize: 12 }}>
                          <input type="checkbox" checked={!!(ph.window && ph.window.enabled)} onChange={e => updPhase(i, { window: { ...(ph.window || { start: "09:00", end: "22:00" }), enabled: e.target.checked } })} />
                          Plage horaire (phase faisable seulement dans cette fenêtre)
                        </label>
                        {ph.window && ph.window.enabled && (
                          <div style={{ display: "grid", gap: 6, marginTop: 4 }}>
                            <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
                              <span style={{ fontSize: 11, width: 118, color: "var(--muted)", flex: "none" }}>🇺🇸 Los Angeles</span>
                              <input type="time" value={ph.window.start} onChange={e => updPhase(i, { window: { ...ph.window, start: e.target.value } })} style={{ ...inp, width: 108 }} />
                              <span style={{ color: "var(--faint)" }}>→</span>
                              <input type="time" value={ph.window.end} onChange={e => updPhase(i, { window: { ...ph.window, end: e.target.value } })} style={{ ...inp, width: 108 }} />
                            </div>
                            <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
                              <span style={{ fontSize: 11, width: 118, color: "var(--muted)", flex: "none" }}>🇫🇷 Paris (équiv.)</span>
                              <input type="time" value={pcConvertWall(ph.window.start, PC_TZ_LA, PC_TZ_PARIS)} onChange={e => updPhase(i, { window: { ...ph.window, start: pcConvertWall(e.target.value, PC_TZ_PARIS, PC_TZ_LA) } })} style={{ ...inp, width: 108 }} />
                              <span style={{ color: "var(--faint)" }}>→</span>
                              <input type="time" value={pcConvertWall(ph.window.end, PC_TZ_LA, PC_TZ_PARIS)} onChange={e => updPhase(i, { window: { ...ph.window, end: pcConvertWall(e.target.value, PC_TZ_PARIS, PC_TZ_LA) } })} style={{ ...inp, width: 108 }} />
                            </div>
                            <div style={{ fontSize: 10.5, color: "var(--faint)" }}>La fenêtre est évaluée sur l'heure de <b>Los Angeles</b> (heure locale des comptes). Édite l'une ou l'autre ligne, l'autre se synchronise.</div>
                          </div>
                        )}
                      </div>
                    </div>
                    <div>
                      <label style={lbl}>Étapes (checklist à cocher par l'opérateur)</label>
                      <div style={{ display: "grid", gap: 6 }}>
                        {ph.steps.map((s, si) => (
                          <div key={si} style={{ display: "flex", alignItems: "center", gap: 4 }}>
                            <span style={{ width: 22, height: 22, borderRadius: 6, background: "var(--surface-2)", color: "var(--faint)", fontSize: 11, fontWeight: 700, display: "grid", placeItems: "center", flex: "none" }}>{si + 1}</span>
                            <input value={s} onChange={e => setStep(i, si, e.target.value)} style={inp} placeholder={"Étape " + (si + 1) + "…"} />
                            <button onClick={() => moveStep(i, si, -1)} disabled={si === 0} title="Monter" style={{ background: "none", border: "none", color: si === 0 ? "var(--faint)" : "var(--muted)", cursor: si === 0 ? "default" : "pointer", fontSize: 12, padding: "0 2px" }}>▲</button>
                            <button onClick={() => moveStep(i, si, 1)} disabled={si === ph.steps.length - 1} title="Descendre" style={{ background: "none", border: "none", color: si === ph.steps.length - 1 ? "var(--faint)" : "var(--muted)", cursor: si === ph.steps.length - 1 ? "default" : "pointer", fontSize: 12, padding: "0 2px" }}>▼</button>
                            <button onClick={() => rmStep(i, si)} title="Retirer" style={{ background: "none", border: "none", color: "var(--faint)", cursor: "pointer", fontSize: 14, padding: "0 4px" }}>✕</button>
                          </div>
                        ))}
                        <button onClick={() => addStep(i)} style={{ justifySelf: "start", marginTop: 2, fontSize: 12, fontWeight: 600, color: "var(--accent,#1dbab4)", background: "none", border: "1px dashed var(--border)", borderRadius: 8, padding: "5px 10px", cursor: "pointer" }}>+ Ajouter une étape</button>
                      </div>
                    </div>
                  </div>
                )}
              </div>
            );
          })}
        </div>

        <button onClick={addPhase} style={{ marginTop: 12, width: "100%", padding: "9px 12px", fontSize: 13, fontWeight: 700, color: "var(--accent,#1dbab4)", background: "none", border: "1px dashed var(--accent-line, rgba(29,186,180,.4))", borderRadius: 10, cursor: "pointer" }}>+ Ajouter une phase</button>

        <div style={{ display: "flex", gap: 10, justifyContent: "flex-end", alignItems: "center", marginTop: 18 }}>
          <button onClick={onClose} style={{ padding: "9px 16px", borderRadius: 9, background: "var(--surface-2)", border: "1px solid var(--border)", color: "var(--text)", fontWeight: 600, fontSize: 13, cursor: "pointer", marginRight: "auto" }}>Annuler</button>
          <button onClick={() => save(true)} title="Sauvegarder sans finaliser — non lançable tant que c'est un brouillon" style={{ padding: "9px 16px", borderRadius: 9, border: "1px solid var(--border)", background: "var(--surface-2)", color: "var(--muted)", fontWeight: 700, fontSize: 13, cursor: "pointer" }}>📝 Brouillon</button>
          <button onClick={() => save(false)} style={{ padding: "9px 20px", borderRadius: 9, border: "none", background: "#238636", color: "#fff", fontWeight: 800, fontSize: 13, cursor: "pointer" }}>✅ Enregistrer (prêt)</button>
        </div>
      </div>
    </div>
  );
}
window.ParcoursEditor = ParcoursEditor;

// ============================================================
// Bibliothèque des Parcours (liste + créer/éditer/supprimer)
// ============================================================
function ParcoursLibrary() {
  const { useState } = React;
  const store = useParcours();
  const [editing, setEditing] = useState(null);   // { parcours } | { parcours: null } (nouveau) | null
  const list = store.list();

  return (
    <div>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 14, gap: 10 }}>
        <div style={{ fontSize: 12.5, color: "var(--faint)" }}>Un parcours = une suite ordonnée de phases (checklist + template + catégorie + cooldown + plage horaire).</div>
        <button onClick={() => setEditing({ parcours: null })} style={{ flex: "none", padding: "8px 16px", borderRadius: 9, border: "none", background: "#238636", color: "#fff", fontWeight: 700, fontSize: 13, cursor: "pointer" }}>+ Nouveau parcours</button>
      </div>

      {list.length === 0 ? (
        <div style={{ textAlign: "center", padding: "56px 0", color: "var(--faint)", border: "1px dashed var(--border)", borderRadius: 12 }}>
          <div style={{ fontSize: 32, marginBottom: 10 }}>🧭</div>
          <div style={{ fontSize: 14, fontWeight: 700, color: "var(--text)", marginBottom: 4 }}>Aucun parcours</div>
          <div style={{ fontSize: 12.5 }}>Crée ton premier parcours pour standardiser un process (création → login → échauffement → publication).</div>
        </div>
      ) : (
        <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))", gap: 14 }}>
          {list.map(p => (
            <div key={p.id} style={{ background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 14, padding: 16 }}>
              <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}>
                <span style={{ fontSize: 16 }}>🧭</span>
                <div style={{ fontWeight: 800, fontSize: 14.5, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{p.title}</div>
                {p.draft && <span title="Brouillon — non lançable tant que non finalisé" style={{ flex: "none", fontSize: 10, fontWeight: 700, padding: "2px 8px", borderRadius: 99, background: "rgba(240,160,32,.14)", color: "#f0a020", border: "1px solid rgba(240,160,32,.4)" }}>📝 Brouillon</span>}
              </div>
              <div style={{ display: "flex", flexWrap: "wrap", gap: 5, marginBottom: 12 }}>
                {p.phases.map((ph, i) => (
                  <span key={ph.id} style={{ display: "inline-flex", alignItems: "center", gap: 4, fontSize: 10.5, fontWeight: 700, padding: "3px 8px", borderRadius: 99, background: "var(--surface-2)", border: "1px solid var(--border)", color: "var(--muted)" }}>
                    {i + 1}. {ph.name}
                  </span>
                ))}
              </div>
              <div style={{ display: "flex", gap: 8 }}>
                <button onClick={() => setEditing({ parcours: p })} style={{ flex: 1, padding: "7px 10px", borderRadius: 8, border: "1px solid var(--border)", background: "var(--surface-2)", color: "var(--text)", fontWeight: 600, fontSize: 12.5, cursor: "pointer" }}>✏️ Éditer</button>
                <button onClick={() => { if (window.confirm("Supprimer le parcours « " + p.title + " » ?")) store.remove(p.id); }} style={{ padding: "7px 12px", borderRadius: 8, border: "1px solid rgba(248,81,73,.3)", background: "var(--surface-2)", color: "#f87171", fontWeight: 700, fontSize: 12.5, cursor: "pointer" }}>🗑</button>
              </div>
            </div>
          ))}
        </div>
      )}

      {editing && <ParcoursEditor parcours={editing.parcours} onClose={() => setEditing(null)} />}
    </div>
  );
}
window.ParcoursLibrary = ParcoursLibrary;

// ============================================================
// LOT 2 — Exécution : progression par profil + verrous + runner
// ============================================================
const PARCOURS_PROGRESS_KEY = "phonelabs.parcours_progress";
const ParcoursProgress = (function () {
  const load = () => { try { return JSON.parse(localStorage.getItem(PARCOURS_PROGRESS_KEY)) || {}; } catch (e) { return {}; } };
  const ls = new Set(); const emit = () => ls.forEach(f => f());
  const persist = (m) => {
    try { localStorage.setItem(PARCOURS_PROGRESS_KEY, JSON.stringify(m)); } catch (e) {}
    // Persistance backend : l'avancement (phase + cases cochées) SURVIT à la fermeture de l'app / au
    // changement d'origine (tunnel) qui vide le localStorage — même patron que parcours/phaselayouts.
    if (window.Backend && window.Backend.saveParcoursProgress) window.Backend.saveParcoursProgress(m).catch(() => {});
    emit();
  };
  return {
    sub(cb) { ls.add(cb); return () => ls.delete(cb); },
    all() { return load(); },
    get(udid) { return load()[String(udid)] || null; },
    start(udid, parcoursId, phaseIds) {
      const m = load();
      m[String(udid)] = { parcoursId, phaseIds: phaseIds.slice(), phaseIndex: 0, checks: {}, validations: [], overrides: [], abandons: [], cooldownUntil: null, _winOverrideUntil: 0, startedAt: Date.now(), updatedAt: Date.now(), done: false };
      persist(m); return m[String(udid)];
    },
    patch(udid, fn) { const m = load(); const pr = m[String(udid)]; if (!pr) return null; fn(pr); pr.updatedAt = Date.now(); persist(m); return pr; },
    reset(udid) { const m = load(); delete m[String(udid)]; persist(m); },
    // Réhydrate depuis le backend au démarrage : fusion par udid, le plus RÉCENT (updatedAt) gagne,
    // puis re-pousse l'union. Backend injoignable -> on garde le local intact.
    async syncFromBackend() {
      if (!(window.Backend && window.Backend.getParcoursProgress)) return;
      const remote = await window.Backend.getParcoursProgress();
      if (!remote || typeof remote !== "object") return;
      const local = load(); const merged = {}; const udids = {};
      Object.keys(local).forEach(u => { udids[u] = 1; });
      Object.keys(remote).forEach(u => { udids[u] = 1; });
      Object.keys(udids).forEach(u => {
        const lp = local[u], rp = remote[u];
        if (lp && rp) merged[u] = ((lp.updatedAt || 0) >= (rp.updatedAt || 0)) ? lp : rp;
        else merged[u] = lp || rp;
      });
      try { localStorage.setItem(PARCOURS_PROGRESS_KEY, JSON.stringify(merged)); } catch (e) {}
      if (window.Backend.saveParcoursProgress) window.Backend.saveParcoursProgress(merged).catch(() => {});
      emit();
    },
  };
})();
window.ParcoursProgress = ParcoursProgress;
// Sync au chargement de l'app (le pilote doit retrouver l'avancement même sans rouvrir l'éditeur).
try { setTimeout(() => { try { ParcoursProgress.syncFromBackend(); } catch (e) {} }, 1600); } catch (e) {}

function _pcUser() { return (window.__FARM_USER__ && window.__FARM_USER__.name) || "?"; }
// --- Fuseaux : la plage horaire est stockée/évaluée en heure de LOS ANGELES (heure locale des comptes US),
//     avec l'équivalent Paris affiché dans l'éditeur (comme les schedules). ---
const PC_TZ_LA = "America/Los_Angeles", PC_TZ_PARIS = "Europe/Paris";
function _pcTzParts(date, tz) {
  const dtf = new Intl.DateTimeFormat("en-US", { timeZone: tz, hour12: false, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit" });
  const o = {}; dtf.formatToParts(date).forEach(p => { if (p.type !== "literal") o[p.type] = p.value; }); return o;
}
function _pcTzOffsetMin(date, tz) { const p = _pcTzParts(date, tz); const asUTC = Date.UTC(+p.year, +p.month - 1, +p.day, +p.hour, +p.minute, +p.second); return (asUTC - date.getTime()) / 60000; }
// convertit une heure "mur" HH:MM d'un fuseau vers un autre (réf = aujourd'hui, gère le DST courant)
function pcConvertWall(hhmm, fromTz, toTz) {
  if (!/^\d{1,2}:\d{2}$/.test(hhmm || "")) return "";
  const [h, m] = hhmm.split(":").map(Number); const now = new Date();
  const guess = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), h, m, 0);
  const off = _pcTzOffsetMin(new Date(guess), fromTz);
  const tp = _pcTzParts(new Date(guess - off * 60000), toTz);
  return String(tp.hour).padStart(2, "0") + ":" + String(tp.minute).padStart(2, "0");
}
function pcNowMinInTz(tz) { const p = _pcTzParts(new Date(), tz); return (+p.hour) * 60 + (+p.minute); }
window.pcConvertWall = pcConvertWall;

function _pcInWindow(ts, start, end) {
  const cur = pcNowMinInTz(PC_TZ_LA);   // heure ACTUELLE à Los Angeles (DST géré par Intl)
  const [sh, sm] = (start || "00:00").split(":").map(Number); const [eh, em] = (end || "23:59").split(":").map(Number);
  const s = sh * 60 + sm, e = eh * 60 + em;
  return s <= e ? (cur >= s && cur <= e) : (cur >= s || cur <= e);   // gère la fenêtre qui passe minuit
}
// phases EFFECTIVES (sous-ensemble sélectionné), dans l'ordre du parcours
function pcEffectivePhases(parcours, pr) {
  if (!parcours) return [];
  const ids = (pr && pr.phaseIds) || parcours.phases.map(p => p.id);
  return ids.map(id => parcours.phases.find(ph => ph.id === id)).filter(Boolean);
}
function pcCurrentPhase(parcours, pr) { return pcEffectivePhases(parcours, pr)[pr ? pr.phaseIndex : 0] || null; }
function pcAllChecked(phase, pr) {
  if (!phase) return true;
  const c = (pr.checks && pr.checks[phase.id]) || [];
  const req = phase.steps.map((s, i) => i).filter(i => (phase.steps[i] || "").trim());   // ignore les étapes vides
  return req.length ? req.every(i => !!c[i]) : true;
}
// état de verrou de la phase COURANTE : cooldown OU hors plage horaire
function pcLockState(parcours, pr) {
  if (!pr || pr.done || !parcours) return { locked: false };
  const now = Date.now();
  if (pr._winOverrideUntil && pr._winOverrideUntil > now) return { locked: false };
  if (pr.cooldownUntil && pr.cooldownUntil > now) return { locked: true, reason: "cooldown", until: pr.cooldownUntil };
  const ph = pcCurrentPhase(parcours, pr);
  if (ph && ph.window && ph.window.enabled && !_pcInWindow(now, ph.window.start, ph.window.end))
    return { locked: true, reason: "window", window: ph.window };
  return { locked: false };
}
// applique le template (phase du Phone Cloud / Phase Builder) + la catégorie cible en ENTRANT dans une phase
// template : appliqué à l'ENTRÉE d'une phase (le bon layout de pilotage pendant qu'on la fait)
function pcApplyTemplate(udid, phase) {
  if (phase && phase.template && window.Backend && window.Backend.patchMeta) { try { window.Backend.patchMeta(udid, { phase: phase.template }); } catch (e) {} }
}
// résultat (catégorie + statut Plug) : appliqué à la FIN d'une phase (quand on la valide)
function pcApplyOutcome(udid, phase) {
  if (!phase) return;
  if (phase.category && window.AccountCreationStore && window.AccountCreationStore.setAttributedCategory) { try { window.AccountCreationStore.setAttributedCategory(String(udid), phase.category); } catch (e) {} }
  if ((phase.plug === "plug" || phase.plug === "unplug") && window.AccountCreationStore && window.AccountCreationStore.setAttributedPlug) { try { window.AccountCreationStore.setAttributedPlug(udid, phase.plug === "plug"); } catch (e) {} }
}
window.pcLockState = pcLockState; window.pcEffectivePhases = pcEffectivePhases; window.pcCurrentPhase = pcCurrentPhase;

function _fmtCountdown(ms) {
  if (ms <= 0) return "0s";
  const s = Math.floor(ms / 1000), h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;
  return (h ? h + "h " : "") + (h || m ? m + "m " : "") + sec + "s";
}

// ============================================================
// ParcoursRunner — exécution d'un parcours sur un profil (bloc du panneau de pilotage)
// ============================================================
function ParcoursRunner({ device, user }) {
  const { useState, useEffect, useReducer } = React;
  const udid = device.udid, tag = device.tag || device.name || "";
  const isOwner = window.ROLE_RANK ? window.ROLE_RANK(user && user.role) >= 3 : false;
  const [, force] = useReducer(x => x + 1, 0);
  const [, tick] = useReducer(x => x + 1, 0);
  useEffect(() => {
    const u1 = ParcoursProgress.sub(force); const u2 = ParcoursStore.sub(force);
    // Réhydrate l'avancement + les parcours depuis le backend à l'ouverture du panneau (survit à la fermeture de l'app).
    ParcoursProgress.syncFromBackend().catch(() => {}); ParcoursStore.syncFromBackend().catch(() => {});
    return () => { u1(); u2(); };
  }, []);
  useEffect(() => { const id = setInterval(tick, 1000); return () => clearInterval(id); }, []);   // countdown temps réel

  const [starter, setStarter] = useState(false);
  const [pickId, setPickId] = useState("");
  const [selPhases, setSelPhases] = useState([]);   // ids de phases sélectionnées au démarrage
  const [confirmValidate, setConfirmValidate] = useState(false);
  const [ovReason, setOvReason] = useState("");
  const [confirmOverride, setConfirmOverride] = useState(false);

  const pr = ParcoursProgress.get(udid);
  const parcours = pr ? ParcoursStore.get(pr.parcoursId) : null;

  const cardSt = { background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 12 };
  const title = (
    <div style={{ display: "flex", alignItems: "center", gap: 8, margin: "2px 2px 10px", color: "var(--muted)" }}>
      <span style={{ fontSize: 15 }}>🧭</span>
      <span style={{ fontSize: 12, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase" }}>Parcours</span>
    </div>
  );

  // ---- Aucun parcours en cours (ou terminé) : le runner ne s'affiche en grand QUE tant qu'un parcours
  //      n'est pas fini ; une fois terminé, on repasse en mode compact « démarrer » (juste une note). ----
  if (!pr || pr.done) {
    const all = ParcoursStore.list().filter(p => !p.draft);   // on ne lance PAS un brouillon
    return (
      <div style={{ marginBottom: 18 }}>
        {title}
        <div style={{ ...cardSt, padding: 14, display: "grid", gap: 10 }}>
          {!starter ? (
            <>
              {pr && pr.done
                ? <div style={{ fontSize: 12.5, color: "var(--ok,#34d399)", fontWeight: 700 }}>✅ Parcours terminé{parcours ? " (" + parcours.title + ")" : ""} — tu peux en relancer un.</div>
                : <div style={{ fontSize: 12.5, color: "var(--faint)" }}>Aucun parcours en cours sur ce profil.</div>}
              <button onClick={() => { setStarter(true); if (all[0]) { setPickId(all[0].id); setSelPhases(all[0].phases.map(p => p.id)); } }}
                disabled={all.length === 0}
                style={{ padding: "9px 14px", borderRadius: 10, border: "none", background: all.length ? "#238636" : "var(--surface-2)", color: all.length ? "#fff" : "var(--muted)", fontWeight: 700, fontSize: 13, cursor: all.length ? "pointer" : "default" }}>
                ▶ Démarrer un parcours
              </button>
              {all.length === 0 && <div style={{ fontSize: 11.5, color: "var(--faint)" }}>Crée d'abord un parcours dans Tasks / SOP → 🧭 Parcours.</div>}
            </>
          ) : (() => {
            const chosen = all.find(p => p.id === pickId);
            return (
              <div style={{ display: "grid", gap: 8 }}>
                <label style={{ fontSize: 11, color: "var(--muted)", fontWeight: 700 }}>Parcours</label>
                <select value={pickId} onChange={e => { setPickId(e.target.value); const c = all.find(p => p.id === e.target.value); setSelPhases(c ? c.phases.map(p => p.id) : []); }}
                  style={{ width: "100%", padding: "8px 10px", background: "var(--surface-2)", border: "1px solid var(--border)", borderRadius: 8, color: "var(--text)", fontSize: 13 }}>
                  {all.map(p => <option key={p.id} value={p.id}>{p.title}</option>)}
                </select>
                {chosen && (
                  <div style={{ display: "grid", gap: 4 }}>
                    <label style={{ fontSize: 11, color: "var(--muted)", fontWeight: 700 }}>Phases à inclure</label>
                    {chosen.phases.map((ph, i) => (
                      <label key={ph.id} style={{ display: "flex", alignItems: "center", gap: 7, fontSize: 12.5, cursor: "pointer" }}>
                        <input type="checkbox" checked={selPhases.includes(ph.id)} onChange={e => setSelPhases(s => e.target.checked ? [...chosen.phases.map(p => p.id).filter(id => s.includes(id) || id === ph.id)] : s.filter(id => id !== ph.id))} />
                        {i + 1}. {ph.name}
                      </label>
                    ))}
                  </div>
                )}
                <div style={{ display: "flex", gap: 8, marginTop: 4 }}>
                  <button onClick={() => setStarter(false)} style={{ padding: "7px 12px", borderRadius: 8, background: "var(--surface-2)", border: "1px solid var(--border)", color: "var(--text)", fontSize: 12.5, cursor: "pointer" }}>Annuler</button>
                  <button disabled={!chosen || selPhases.length === 0}
                    onClick={() => {
                      const ids = chosen.phases.map(p => p.id).filter(id => selPhases.includes(id));   // ordre du parcours
                      ParcoursProgress.start(udid, chosen.id, ids);
                      pcApplyTemplate(udid, chosen.phases.find(ph => ph.id === ids[0]));   // template de la 1re phase (entrée)
                      setStarter(false);
                    }}
                    style={{ flex: 1, padding: "7px 12px", borderRadius: 8, border: "none", background: (chosen && selPhases.length) ? "#238636" : "var(--surface-2)", color: (chosen && selPhases.length) ? "#fff" : "var(--muted)", fontWeight: 700, fontSize: 12.5, cursor: (chosen && selPhases.length) ? "pointer" : "default" }}>
                    ▶ Lancer
                  </button>
                </div>
              </div>
            );
          })()}
        </div>
      </div>
    );
  }

  // ---- Parcours supprimé entre-temps ----
  if (!parcours) return (
    <div style={{ marginBottom: 18 }}>{title}
      <div style={{ ...cardSt, padding: 14 }}>
        <div style={{ fontSize: 12.5, color: "#d29922", marginBottom: 8 }}>⚠️ Le parcours n'existe plus.</div>
        <button onClick={() => ParcoursProgress.reset(udid)} style={{ padding: "7px 12px", borderRadius: 8, background: "var(--surface-2)", border: "1px solid var(--border)", color: "var(--text)", fontSize: 12.5, cursor: "pointer" }}>Réinitialiser</button>
      </div>
    </div>
  );

  const effPhases = pcEffectivePhases(parcours, pr);
  const total = effPhases.length;
  const phase = effPhases[pr.phaseIndex];
  const lock = pcLockState(parcours, pr);
  const checks = (pr.checks && pr.checks[phase.id]) || [];
  const allChecked = pcAllChecked(phase, pr);

  const validate = () => {
    pcApplyOutcome(udid, phase);   // FIN de la phase validée : catégorie cible + statut Plug
    ParcoursProgress.patch(udid, p => {
      p.validations.push({ phaseId: phase.id, phaseName: phase.name, by: _pcUser(), at: Date.now() });
      const cd = Math.max(0, parseInt(phase.cooldownMin) || 0);
      p.cooldownUntil = cd > 0 ? Date.now() + cd * 60000 : null;
      p._winOverrideUntil = 0;
      p.phaseIndex += 1;
      if (p.phaseIndex >= total) p.done = true;
    });
    const nx = ParcoursProgress.get(udid);
    if (!nx.done) pcApplyTemplate(udid, effPhases[nx.phaseIndex]);   // ENTRÉE de la phase suivante : son template
    setConfirmValidate(false);
  };
  const doOverride = () => {
    ParcoursProgress.patch(udid, p => { p.overrides.push({ phaseId: phase.id, by: _pcUser(), at: Date.now(), reason: ovReason || "" }); p.cooldownUntil = null; p._winOverrideUntil = Date.now() + 2 * 3600 * 1000; });
    setConfirmOverride(false); setOvReason("");
  };

  return (
    <div style={{ marginBottom: 18 }}>
      {title}
      <div style={{ ...cardSt, padding: 12, display: "grid", gap: 10 }}>
        {/* progression */}
        <div>
          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 6 }}>
            <span style={{ fontSize: 13.5, fontWeight: 800, color: "var(--text)" }}>{phase.name}</span>
            <span style={{ fontSize: 11, color: "var(--faint)", fontWeight: 700 }}>Phase {pr.phaseIndex + 1}/{total}</span>
          </div>
          <div style={{ display: "flex", gap: 4 }}>
            {effPhases.map((ph, i) => (
              <div key={ph.id} title={ph.name} style={{ flex: 1, height: 5, borderRadius: 99, background: i < pr.phaseIndex ? "var(--ok,#34d399)" : i === pr.phaseIndex ? "var(--accent,#1dbab4)" : "var(--surface-2)" }} />
            ))}
          </div>
        </div>

        {lock.locked ? (
          /* ---- VERROU (cooldown / hors plage horaire) ---- */
          <div style={{ background: "rgba(240,160,32,.08)", border: "1px solid rgba(240,160,32,.35)", borderRadius: 10, padding: 14, textAlign: "center" }}>
            <div style={{ fontSize: 26, marginBottom: 6 }}>🔒</div>
            {lock.reason === "cooldown"
              ? <><div style={{ fontSize: 13, fontWeight: 800, color: "#f0a020" }}>En cooldown</div>
                  <div style={{ fontSize: 12, color: "var(--muted)", marginTop: 3 }}>Prochaine phase dans <b className="mono">{_fmtCountdown(lock.until - Date.now())}</b></div></>
              : <><div style={{ fontSize: 13, fontWeight: 800, color: "#f0a020" }}>Hors plage horaire</div>
                  <div style={{ fontSize: 12, color: "var(--muted)", marginTop: 3 }}>Faisable entre <b className="mono">{lock.window.start}–{lock.window.end}</b> 🇺🇸LA · <b className="mono">{pcConvertWall(lock.window.start, PC_TZ_LA, PC_TZ_PARIS)}–{pcConvertWall(lock.window.end, PC_TZ_LA, PC_TZ_PARIS)}</b> 🇫🇷Paris</div></>}
            {isOwner && (
              <button onClick={() => setConfirmOverride(true)} style={{ marginTop: 12, padding: "7px 14px", borderRadius: 8, border: "1px solid rgba(248,81,73,.4)", background: "rgba(248,81,73,.1)", color: "#f87171", fontWeight: 700, fontSize: 12, cursor: "pointer" }}>🔓 Forcer (Owner)</button>
            )}
          </div>
        ) : (
          /* ---- CHECKLIST de la phase ---- */
          <>
            <div style={{ display: "grid", gap: 5 }}>
              {phase.steps.filter(s => (s || "").trim()).length === 0
                ? <div style={{ fontSize: 12, color: "var(--faint)" }}>Aucune étape — valide directement.</div>
                : phase.steps.map((s, i) => (s || "").trim() ? (
                  <label key={i} style={{ display: "flex", alignItems: "flex-start", gap: 8, padding: "7px 9px", borderRadius: 8, background: checks[i] ? "rgba(52,211,153,.08)" : "var(--surface-2)", border: "1px solid " + (checks[i] ? "rgba(52,211,153,.3)" : "var(--border)"), cursor: "pointer" }}>
                    <input type="checkbox" checked={!!checks[i]} onChange={e => ParcoursProgress.patch(udid, p => { p.checks[phase.id] = p.checks[phase.id] || []; p.checks[phase.id][i] = e.target.checked; })} style={{ marginTop: 2 }} />
                    <span style={{ fontSize: 12.5, color: checks[i] ? "var(--muted)" : "var(--text)", textDecoration: checks[i] ? "line-through" : "none", lineHeight: 1.4 }}>{s}</span>
                  </label>
                ) : null)}
            </div>
            <button disabled={!allChecked} onClick={() => setConfirmValidate(true)}
              style={{ padding: "9px 14px", borderRadius: 10, border: "none", background: allChecked ? "#238636" : "var(--surface-2)", color: allChecked ? "#fff" : "var(--muted)", fontWeight: 800, fontSize: 13, cursor: allChecked ? "pointer" : "default" }}>
              {allChecked ? "✓ Valider la phase" : "Coche toutes les étapes pour valider"}
            </button>
          </>
        )}

        {/* phases suivantes (verrouillées, aperçu) */}
        {pr.phaseIndex + 1 < total && (
          <div style={{ display: "flex", flexWrap: "wrap", gap: 5, opacity: .6 }}>
            {effPhases.slice(pr.phaseIndex + 1).map((ph, i) => (
              <span key={ph.id} style={{ fontSize: 10.5, fontWeight: 700, padding: "3px 8px", borderRadius: 99, background: "var(--surface-2)", border: "1px solid var(--border)", color: "var(--faint)" }}>🔒 {pr.phaseIndex + 2 + i}. {ph.name}</span>
            ))}
          </div>
        )}
      </div>

      {/* popup de confirmation de validation */}
      {confirmValidate && (
        <div onClick={() => setConfirmValidate(false)} style={{ position: "fixed", inset: 0, background: "rgba(2,6,15,.72)", zIndex: 700, display: "grid", placeItems: "center", padding: 16 }}>
          <div onClick={e => e.stopPropagation()} style={{ width: "min(380px,94vw)", background: "var(--bg,#0d1117)", border: "1px solid var(--border)", borderRadius: 16, padding: 22 }}>
            <div style={{ fontSize: 15.5, fontWeight: 800, marginBottom: 8 }}>Valider « {phase.name} » ?</div>
            <div style={{ fontSize: 12.5, color: "var(--muted)", lineHeight: 1.6, marginBottom: 16 }}>Confirme que tu as bien fait <b>toutes les étapes</b>. Le profil passera à la phase suivante{phase.cooldownMin ? " après un cooldown de " + phase.cooldownMin + " min" : ""}.</div>
            <div style={{ display: "flex", gap: 10, justifyContent: "flex-end" }}>
              <button onClick={() => setConfirmValidate(false)} style={{ padding: "9px 16px", borderRadius: 9, background: "var(--surface-2)", border: "1px solid var(--border)", color: "var(--text)", fontWeight: 700, fontSize: 13, cursor: "pointer" }}>Non</button>
              <button onClick={validate} style={{ padding: "9px 18px", borderRadius: 9, border: "none", background: "#238636", color: "#fff", fontWeight: 800, fontSize: 13, cursor: "pointer" }}>Oui, valider</button>
            </div>
          </div>
        </div>
      )}

      {/* popup override Owner */}
      {confirmOverride && (
        <div onClick={() => setConfirmOverride(false)} style={{ position: "fixed", inset: 0, background: "rgba(2,6,15,.72)", zIndex: 700, display: "grid", placeItems: "center", padding: 16 }}>
          <div onClick={e => e.stopPropagation()} style={{ width: "min(400px,94vw)", background: "var(--bg,#0d1117)", border: "1px solid var(--border)", borderRadius: 16, padding: 22 }}>
            <div style={{ fontSize: 15.5, fontWeight: 800, marginBottom: 8 }}>⚠️ Forcer le verrou ?</div>
            <div style={{ fontSize: 12.5, color: "var(--muted)", lineHeight: 1.6, marginBottom: 12 }}>Tu vas passer outre le {lock.reason === "cooldown" ? "cooldown" : "créneau horaire"}. C'est déconseillé (anti-ban) et c'est <b>loggé</b> à ton nom.</div>
            <input value={ovReason} onChange={e => setOvReason(e.target.value)} placeholder="Motif (optionnel)…" style={{ width: "100%", boxSizing: "border-box", padding: "8px 10px", background: "var(--surface-2)", border: "1px solid var(--border)", borderRadius: 8, color: "var(--text)", fontSize: 13, marginBottom: 14 }} />
            <div style={{ display: "flex", gap: 10, justifyContent: "flex-end" }}>
              <button onClick={() => setConfirmOverride(false)} style={{ padding: "9px 16px", borderRadius: 9, background: "var(--surface-2)", border: "1px solid var(--border)", color: "var(--text)", fontWeight: 700, fontSize: 13, cursor: "pointer" }}>Annuler</button>
              <button onClick={doOverride} style={{ padding: "9px 18px", borderRadius: 9, border: "none", background: "#da3633", color: "#fff", fontWeight: 800, fontSize: 13, cursor: "pointer" }}>Forcer quand même</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}
window.ParcoursRunner = ParcoursRunner;

// ============================================================
// ParcoursPipeline — vue "où en est chaque profil" (groupé par phase actuelle)
// ============================================================
function ParcoursPipeline() {
  const { useReducer, useEffect } = React;
  const [, f] = useReducer(x => x + 1, 0);
  useEffect(() => { const u1 = ParcoursProgress.sub(f); const u2 = ParcoursStore.sub(f); const id = setInterval(f, 5000); return () => { u1(); u2(); clearInterval(id); }; }, []);
  const S = window.AccountCreationStore;
  const all = ParcoursProgress.all();
  const rows = Object.keys(all).map(udid => {
    const pr = all[udid]; const parc = ParcoursStore.get(pr.parcoursId);
    if (!parc) return null;
    const cur = pr.done ? null : pcCurrentPhase(parc, pr);
    const lock = pr.done ? { locked: false } : pcLockState(parc, pr);
    const acc = (S && S.getAttributed) ? S.getAttributed(udid) : null;
    const late = !pr.done && !lock.locked && (Date.now() - (pr.updatedAt || pr.startedAt) > 48 * 3600 * 1000);
    return { udid, pr, parc, cur, lock, acc, late, total: pcEffectivePhases(parc, pr).length, phaseName: pr.done ? "✅ Terminé" : (cur ? cur.name : "?") };
  }).filter(Boolean);
  const groups = {}; rows.forEach(r => { (groups[r.phaseName] = groups[r.phaseName] || []).push(r); });
  const names = Object.keys(groups);

  if (rows.length === 0) return (
    <div style={{ textAlign: "center", padding: "56px 0", color: "var(--faint)", border: "1px dashed var(--border)", borderRadius: 12 }}>
      <div style={{ fontSize: 32, marginBottom: 10 }}>📊</div>
      <div style={{ fontSize: 14, fontWeight: 700, color: "var(--text)", marginBottom: 4 }}>Aucun profil en parcours</div>
      <div style={{ fontSize: 12.5 }}>Démarre un parcours sur un profil (bloc « 🧭 Parcours » dans le pilotage) pour le voir ici.</div>
    </div>
  );

  return (
    <div>
      <div style={{ fontSize: 12.5, color: "var(--faint)", marginBottom: 14 }}>{rows.length} profil(s) en parcours · groupés par phase actuelle · les <b style={{ color: "#f87171" }}>en retard</b> (&gt;48h sans avancer) et 🔒 verrouillés sont signalés.</div>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(240px, 1fr))", gap: 14, alignItems: "start" }}>
        {names.map(name => (
          <div key={name} style={{ background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 14, padding: 14 }}>
            <div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 10 }}>
              <span style={{ fontWeight: 800, fontSize: 13.5, color: "var(--text)" }}>{name}</span>
              <span style={{ marginLeft: "auto", fontSize: 11, fontWeight: 700, color: "var(--faint)", background: "var(--surface-2)", borderRadius: 99, padding: "1px 8px" }}>{groups[name].length}</span>
            </div>
            <div style={{ display: "grid", gap: 6 }}>
              {groups[name].map(r => (
                <div key={r.udid} style={{ display: "flex", alignItems: "center", gap: 6, padding: "7px 9px", borderRadius: 8, background: r.late ? "rgba(248,81,73,.08)" : "var(--surface-2)", border: "1px solid " + (r.late ? "rgba(248,81,73,.3)" : "var(--border)") }}>
                  <span style={{ fontSize: 12.5, fontWeight: 700, color: "var(--text)", flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{r.acc ? "@" + (r.acc.username || "?") : String(r.udid).slice(0, 10)}</span>
                  {!r.pr.done && <span style={{ fontSize: 10, color: "var(--faint)", flex: "none" }}>{r.pr.phaseIndex + 1}/{r.total}</span>}
                  {r.lock.locked && <span title="Verrouillé" style={{ fontSize: 12, flex: "none" }}>🔒</span>}
                  {r.late && <span title="En retard (>48h)" style={{ fontSize: 12, color: "#f87171", flex: "none" }}>⏰</span>}
                </div>
              ))}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}
window.ParcoursPipeline = ParcoursPipeline;
