/* global React */
// ============================================================
// AutomationBuilder v2 — fullscreen · zoom · resize · autosave
// ============================================================

const AUTO_FLOWS_KEY = 'phonelabs.automations.v1';

const AUTO_BLOCKS = {
  trigger_manual:  { cat:'trigger', e:'🚀', l:'Déclencheur',         c:'#238636', f:[] },
  wda_launch:      { cat:'wda',     e:'📱', l:'Ouvrir app',           c:'#388bfd', f:[{k:'bundle',l:'Bundle / nom',v:'com.burbn.instagram'}] },
  wda_tap_text:    { cat:'wda',     e:'👆', l:'Tap — texte',          c:'#388bfd', f:[{k:'text',l:'Texte visible',v:'Créer un compte'}] },
  wda_tap_ai:      { cat:'wda',     e:'🔍', l:'Tap — IA (description)', c:'#8957e5', f:[{k:'prompt',l:'Décris l\'élément',v:'le bouton app switcher en bas à droite'}] },
  wda_tap_router:  { cat:'wda',     e:'🔀', l:'IA Router (voit quoi ?)', c:'#8957e5', f:[], routerOut:true, w:230 },
  wda_zone_tap:    { cat:'wda',     e:'📍', l:'Zones calibrées',          c:'#3fb950', f:[], noIn:true, noOut:true },
  wda_tap_xy:      { cat:'wda',     e:'🎯', l:'Tap — X/Y',            c:'#388bfd', f:[{k:'label',l:'Nom du tap',v:''},{k:'x',l:'X%',v:'50'},{k:'y',l:'Y%',v:'50'}] },
  wda_tap_repeat:  { cat:'wda',     e:'🔁', l:'Tap X fois',            c:'#388bfd', f:[{k:'label',l:'Nom du tap',v:''},{k:'x',l:'X%',v:'50'},{k:'y',l:'Y%',v:'50'},{k:'n',l:'Nombre de taps',v:'5'},{k:'delay',l:'Délai (ms)',v:'500'}] },
  wda_date_picker: { cat:'wda', e:'📅', l:'Sélecteur date', c:'#1f6feb', f:[
    {k:'x_month',l:'X Mois %',v:'17'},
    {k:'x_day',  l:'X Jour %',v:'50'},
    {k:'x_year', l:'X Année %',v:'75'},
    {k:'y_above',l:'Y au-dessus %',v:'42'},
    {k:'y_below',l:'Y en-dessous %',v:'58'},
    {k:'delay',  l:'Délai/tap (ms)',v:'80'},
  ]},
  wda_type:        { cat:'wda',     e:'⌨️',  l:'Saisir texte',         c:'#388bfd', f:[{k:'text',l:'Texte',v:'exemple.texte'},{k:'delay',l:'Délai/char (ms, 0=rapide)',v:'0'}] },
  wda_swipe:       { cat:'wda',     e:'👋', l:'Swipe',                c:'#388bfd', f:[{k:'dir',l:'Direction',v:'UP',o:['UP','DOWN','LEFT','RIGHT']}] },
  wda_long_drag: { cat:'wda', e:'🖐', l:'Long press + glisser', c:'#388bfd', f:[
    {k:'label',l:'Nom du geste',v:''},
    {k:'x1',l:'X départ%',v:'50'},{k:'y1',l:'Y départ%',v:'50'},
    {k:'x2',l:'X arrivée%',v:'50'},{k:'y2',l:'Y arrivée%',v:'70'},
    {k:'hold',l:'Durée hold (s)',v:'1.5'},
  ]},
  wda_wait_elem:   { cat:'wda',     e:'👁', l:'Attendre élément',     c:'#388bfd', f:[{k:'text',l:'Texte attendu',v:'Continuer'},{k:'timeout',l:'Timeout (s)',v:'15'}] },
  wda_home:        { cat:'wda',     e:'🏠', l:'Bouton Home',          c:'#388bfd', f:[] },
  wda_screenshot:  { cat:'wda',     e:'📸', l:'Screenshot',           c:'#8b949e', f:[{k:'name',l:'Fichier',v:'cap1'}] },
  farm_rotate_ip:  { cat:'farm',    e:'🔄', l:'Rotation IP',          c:'#1dbab4', f:[] },
  farm_buy_number: { cat:'farm',    e:'📞', l:'Acheter numéro SMS',   c:'#1dbab4', f:[{k:'prov',l:'Provider',v:'HeroSMS',o:['HeroSMS','GetAText','5sim']},{k:'country',l:'Pays',v:'USA',o:['USA','France']},{k:'apikey',l:'Clé API (GetAText)',v:''}] },
  farm_switch_provider: { cat:'farm', e:'🔀', l:'Switch Provider SMS', c:'#1dbab4',
    f:[{k:'providers',l:'Providers (virgule)',v:'HeroSMS,5sim,GetAText'}],
  },
  farm_wait_sms:   { cat:'farm',    e:'📨', l:'Attendre SMS',         c:'#1dbab4', f:[{k:'timeout',l:'Timeout (s)',v:'360'},{k:'numid',l:'ID numéro (optionnel)',v:''}], branch:true },
  farm_enter_code: { cat:'farm',    e:'🔢', l:'Saisir code SMS',      c:'#1dbab4', f:[] },
  farm_enable_2fa: { cat:'farm',    e:'🔐', l:'Activer 2FA (TOTP)',   c:'#1dbab4', f:[] },
  farm_save_acct:  { cat:'farm',    e:'💾', l:'Sauvegarder compte',   c:'#1dbab4', f:[] },
  farm_assign_account:{ cat:'farm', e:'🔗', l:'Attribuer un compte',   c:'#238636', f:[{k:'category',l:'Catégorie source',v:'Compte en attente'}] },
  farm_account_info:{ cat:'farm',   e:'📥', l:'Infos compte attribué', c:'#238636', f:[] },
  farm_set_category:{ cat:'farm',   e:'🏷️', l:'Changer catégorie compte', c:'#238636', f:[{k:'category',l:'Nouvelle catégorie',v:'Warm Up'}] },
  farm_set_plug:    { cat:'farm',   e:'🔌', l:'Marquer IG Plug', c:'#238636', f:[{k:'plug',l:'État',v:'Plug',o:['Plug','Pas plug']}] },
  // Contenu Source : spoofe (MetaFlux) un pack maître et le dépose dans le dossier du profil courant
  content_spoof:    { cat:'media',  e:'🧬', l:'Contenu spoofé → profil', c:'#c9a9ff', f:[{k:'pack',l:'Pack source',v:'— pack source —',o:['— pack source —']},{k:'mode',l:'Quantité',v:'Quota',o:['Tout','Quota','Minimal']},{k:'quota',l:'Quota',v:'20'}] },
  // (les blocs médias 📤 par catégorie sont générés dynamiquement — voir cat:'media')
  // ── Blocs ☁️ Phone Cloud (agissent sur le cloud phone sélectionné du flux) ──
  cloud_power_on:  { cat:'cloud',   e:'🔋', l:'Allumer le cloud',   c:'#3b82f6', f:[] },
  cloud_power_off: { cat:'cloud',   e:'⏻', l:'Éteindre le cloud',  c:'#3b82f6', f:[] },
  cloud_app_switch:{ cat:'cloud',   e:'▤', l:'App Switcher',       c:'#3b82f6', f:[] },
  cloud_back:      { cat:'cloud',   e:'◀', l:'Retour (Back)',      c:'#3b82f6', f:[] },
  cloud_home:      { cat:'cloud',   e:'⬤', l:'Accueil (Home)',     c:'#3b82f6', f:[] },
  cloud_create:    { cat:'cloud',   e:'➕', l:'Créer un profil',    c:'#f0a020', f:[{k:'group',l:'Catégorie (groupe)',v:'IG'},{k:'name',l:'Nom (optionnel)',v:''}] },
  cloud_rename:    { cat:'cloud',   e:'✏️', l:'Renommer le profil', c:'#3b82f6', f:[{k:'name',l:'Nouveau nom',v:''}] },
  cloud_assign_account:{ cat:'cloud', e:'🔗', l:'Attribuer un compte à un profil', c:'#238636', f:[{k:'category',l:'Catégorie compte (source)',v:'Compte en attente'},{k:'pool',l:'Groupe profils (pool)',v:'Compte en attente'}] },
  logic_wait:      { cat:'logic',   e:'⏳', l:'Attendre Xs',          c:'#d29922', f:[{k:'s',l:'Secondes',v:'2'}] },
  logic_condition: { cat:'logic',   e:'❓', l:'Condition si…',         c:'#8957e5', f:[{k:'check',l:'Si visible…',v:'erreur'}], branch:true },
  logic_loop:      { cat:'logic',   e:'🔁', l:'Boucle ×N',            c:'#8957e5', f:[{k:'n',l:'Répétitions',v:'3'}], loopOut:true },
  logic_random:    { cat:'logic',   e:'🎲', l:'Aléatoire',             c:'#f78166', f:[{k:'n',l:'Nb sorties',v:'3'}], multiOut:true },
  logic_merge:     { cat:'logic',   e:'⤵', l:'Fusionner',             c:'#8957e5', f:[] },
  logic_split:     { cat:'logic',   e:'⑃', l:'Diviser (tout)',        c:'#8957e5', f:[{k:'n',l:'Nb sorties',v:'3'}], multiOut:true },
  logic_end:       { cat:'logic',   e:'🔴', l:'Stop / Arrêt forcé',    c:'#da3633', f:[], noOut:true },
  logic_cycle_end: { cat:'logic',   e:'🔁', l:'Fin de cycle',           c:'#3fb950', f:[], noOut:true },
  logic_ai_cond:   { cat:'logic',   e:'🤖', l:'Condition IA (oui/non)', c:'#8957e5', f:[{k:'prompt',l:'Question à Gemini',v:'Y a-t-il un bouton Done visible ?'}], branch:true },
  annot_note:      { cat:'annot',   e:'📝', l:'Note',                  c:'#d29922', f:[{k:'title',l:'Titre',v:'Phase'},{k:'body',l:'Description',v:''}], noIn:true, noOut:true, w:240 },
  annot_draw:      { cat:'annot',   e:'✏️',  l:'Dessin libre',          c:'#79c0ff', f:[], noIn:true, noOut:true, w:240 },
  data_context:    { cat:'data',    e:'📋', l:'Contexte',               c:'#8957e5', f:[], noIn:true, noOut:true, w:230 },
  data_ip:         { cat:'data',    e:'🌐', l:'IP actuelle',            c:'#1dbab4', f:[], noIn:true, noOut:true, w:220 },
  gen_account:     { cat:'data',   e:'👤', l:'Générer infos compte',   c:'#238636', f:[
    {k:'style',l:'Style',v:'chloe',o:['chloe','lou','marie','alex','random']},
    {k:'emoji',l:'Emoji dans nom',v:'oui',o:['oui','non']}
  ]},
  check_color:     { cat:'wda',    e:'🔍', l:'Vérif. couleur',         c:'#e3b341', f:[
    {k:'label',l:'Nom',v:'Vérif. username'},
    {k:'x',l:'X%',v:'50'},{k:'y',l:'Y%',v:'40'},
    {k:'size',l:'Taille zone %',v:'5'},
    {k:'target',l:'Couleur cible',v:'green',o:['green','red']}
  ], branch:true },
  ai_2fa:          { cat:'data',   e:'🤖', l:'2FA via Gemini',         c:'#8957e5', f:[] },
  regen_username:  { cat:'data',   e:'🔄', l:'Régénérer username',     c:'#238636', f:[
    {k:'style',l:'Style',v:'chloe',o:['chloe','lou','marie','alex','random']}
  ]},
  // ── Blocs issus de l'import MoreLogin ──
  adb_shell:     { cat:'cloud',  e:'💻', l:'Exécuter ADB',            c:'#3b82f6', f:[{k:'cmd',l:'Commande ADB',v:'input tap 540 960'}] },
  js_script:     { cat:'cloud',  e:'📜', l:'Script JS',               c:'#8957e5', f:[{k:'code',l:'Code JavaScript',v:'// driver.shell(...)'}] },
  download_file: { cat:'cloud',  e:'⬇️', l:'Télécharger fichier',     c:'#1dbab4', f:[{k:'url',l:'URL du fichier',v:''},{k:'dest',l:'Destination',v:'gallery'}] },
};
const AUTO_CAT = {
  trigger:{ label:'Déclencheur', icon:'⚡' },
  wda:    { label:'iPhone (WDA)', icon:'📱' },
  cloud:  { label:'Phone Cloud',  icon:'☁️', platform:'android' },   // visible seulement en flux Android
  farm:   { label:'Farm',         icon:'🌾' },
  media:  { label:'Médias',       icon:'📤' },   // 1 bloc par catégorie de dossier (dynamique)
  logic:  { label:'Logique',      icon:'🔀' },
  data:   { label:'Données',      icon:'📋' },
  annot:  { label:'Annotations',  icon:'📝' },
};

// ─── Catégories de médias (dossiers dynamiques) — gérées dans Dossiers, 1 bloc auto par catégorie ───
const MEDIA_CATS_KEY = 'phonelabs.media_categories';
function loadMediaCats(){ try{ const r=JSON.parse(localStorage.getItem(MEDIA_CATS_KEY)); return (Array.isArray(r)&&r.length)?r:['Vidéo','Photo','Photo de profil']; }catch(e){ return ['Vidéo','Photo','Photo de profil']; } }
function saveMediaCats(a){ try{ localStorage.setItem(MEDIA_CATS_KEY, JSON.stringify(a)); }catch(e){} }
function mediaCatSlug(n){
  var s=String(n||'').toLowerCase();
  try{ s=s.normalize('NFD').replace(/[̀-ͯ]/g,''); }catch(e){}   // enlève les accents (é→e)
  return 'media_cat_'+s.replace(/[^a-z0-9]+/g,'_').replace(/^_+|_+$/g,'');
}
// injecte un bloc "📤 <catégorie>" dans la palette pour chaque catégorie de média
function registerMediaCatBlock(name){ var t=mediaCatSlug(name); if(t&&t!=='media_cat_') AUTO_BLOCKS[t]={cat:'media',e:'📤',l:name,c:'#1dbab4',f:[{k:'cat',l:'Dossier source',v:name}]}; return t; }
function syncMediaCatBlocks(cats){ // (re)construit tous les blocs média depuis la liste (supprime ceux qui n'existent plus)
  Object.keys(AUTO_BLOCKS).forEach(function(k){ if(k.indexOf('media_cat_')===0) delete AUTO_BLOCKS[k]; });
  (cats||loadMediaCats()).forEach(registerMediaCatBlock);
}
syncMediaCatBlocks();
window.loadMediaCats=loadMediaCats; window.saveMediaCats=saveMediaCats; window.mediaCatSlug=mediaCatSlug;
window.registerMediaCatBlock=registerMediaCatBlock; window.syncMediaCatBlocks=syncMediaCatBlocks;
// alimente le menu déroulant "Pack source" du bloc content_spoof avec les packs Contenu Source
function setContentPacks(names){ if(AUTO_BLOCKS.content_spoof) AUTO_BLOCKS.content_spoof.f[0].o=['— pack source —'].concat((names||[]).filter(Boolean)); }
window.setContentPacks=setContentPacks;
try{ if(window.Backend&&window.Backend.sourcePacks) window.Backend.sourcePacks().then(function(r){ setContentPacks((r&&r.packs||[]).map(function(p){return p.name;})); }); }catch(e){}

// Variables de contexte disponibles à runtime
const CTX_VARS = [
  { key:'phone',       label:'Numéro acheté',     src:'Acheter numéro SMS', e:'📞', c:'#1dbab4' },
  { key:'sms_code',    label:'Code SMS',           src:'Attendre SMS',       e:'📨', c:'#1dbab4' },
  { key:'2fa_secret',  label:'Clé 2FA (TOTP)',     src:'Activer 2FA',        e:'🔐', c:'#1dbab4' },
  { key:'2fa_code',    label:'Code TOTP actuel',   src:'Activer 2FA',        e:'🔐', c:'#1dbab4' },
  { key:'username',    label:'Username généré',    src:'Générer infos compte', e:'👤', c:'#238636' },
  { key:'password',    label:'Mot de passe',       src:'Générer infos compte', e:'🔑', c:'#238636' },
  { key:'name',        label:'Nom complet',        src:'Générer infos compte', e:'✍️', c:'#238636' },
  { key:'birthday',    label:'Date de naissance',  src:'Générer infos compte', e:'📅', c:'#238636' },
];
const AUTO_APPS = [
  { id:'instagram',    name:'Instagram',    bundle:'com.burbn.instagram',              e:'📸', c:'#e1306c' },
  { id:'shadowrocket', name:'Shadowrocket', bundle:'com.liguangming.Shadowrocket',     e:'🚀', c:'#e74c3c' },
  { id:'notes',        name:'Notes',        bundle:'com.apple.mobilenotes',            e:'📝', c:'#f5b942' },
  { id:'photos',       name:'Photos',       bundle:'com.apple.mobileslideshow',        e:'🖼️', c:'#5856d6' },
  { id:'settings',     name:'Réglages',     bundle:'com.apple.Preferences',            e:'⚙️', c:'#8b949e' },
  { id:'appstore',     name:'App Store',    bundle:'com.apple.AppStore',               e:'🛍️', c:'#007aff' },
  { id:'safari',       name:'Safari',       bundle:'com.apple.mobilesafari',           e:'🧭', c:'#30d158' },
  { id:'messages',     name:'Messages',     bundle:'com.apple.MobileSMS',              e:'💬', c:'#30d158' },
  { id:'tiktok',       name:'TikTok',       bundle:'com.zhiliaoapp.musically',         e:'🎵', c:'#010101' },
  { id:'twitter',      name:'X / Twitter',  bundle:'com.atebits.Tweetie2',             e:'𝕏',  c:'#1d9bf0' },
];
const AUTO_LOG_C = {
  trigger_manual:'#3fb950',wda_launch:'#58a6ff',wda_tap_text:'#58a6ff',wda_tap_xy:'#58a6ff',
  wda_type:'#58a6ff',wda_swipe:'#58a6ff',wda_wait_elem:'#58a6ff',wda_home:'#58a6ff',
  wda_screenshot:'#8b949e',farm_rotate_ip:'#1dbab4',farm_buy_number:'#1dbab4',
  farm_switch_provider:'#1dbab4',farm_wait_sms:'#1dbab4',farm_enter_code:'#1dbab4',farm_enable_2fa:'#1dbab4',
  farm_save_acct:'#1dbab4',logic_wait:'#d29922',logic_condition:'#bc8cff',
  logic_loop:'#bc8cff',logic_random:'#f78166',logic_merge:'#bc8cff',logic_split:'#bc8cff',logic_end:'#f85149',logic_cycle_end:'#3fb950',
  annot_note:'#d29922',annot_draw:'#79c0ff',wda_tap_router:'#8957e5',wda_zone_tap:'#3fb950',logic_ai_cond:'#8957e5',
  gen_account:'#3fb950',check_color:'#e3b341',ai_2fa:'#8957e5',
};

// ─── TOTP (Web Crypto HMAC-SHA1) ────────────────────────────
async function _generateTOTP(secret){
  try{
    const B32='ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
    const s=secret.toUpperCase().replace(/[^A-Z2-7]/g,'');
    let bits='';for(const c of s){const v=B32.indexOf(c);if(v<0)continue;bits+=v.toString(2).padStart(5,'0');}
    const bytes=new Uint8Array(Math.floor(bits.length/8));
    for(let i=0;i<bytes.length;i++)bytes[i]=parseInt(bits.slice(i*8,(i+1)*8),2);
    const counter=Math.floor(Date.now()/30000);
    const cb=new Uint8Array(8);let c=counter;for(let i=7;i>=0;i--){cb[i]=c&0xff;c=Math.floor(c/256);}
    const key=await crypto.subtle.importKey('raw',bytes,{name:'HMAC',hash:'SHA-1'},false,['sign']);
    const sig=new Uint8Array(await crypto.subtle.sign('HMAC',key,cb));
    const off=sig[sig.length-1]&0xf;
    const code=((sig[off]&0x7f)<<24|(sig[off+1]&0xff)<<16|(sig[off+2]&0xff)<<8|(sig[off+3]&0xff))%1000000;
    return code.toString().padStart(6,'0');
  }catch(e){return'000000';}
}

// ─── utils ──────────────────────────────────────────────────
function autoLoadFlows(){try{return JSON.parse(localStorage.getItem(AUTO_FLOWS_KEY))||[];}catch(e){return[];}}
function autoSaveFlows(f){try{localStorage.setItem(AUTO_FLOWS_KEY,JSON.stringify(f));}catch(e){}}

// ─── Groupes de blocs réutilisables (macros / phases) — partagés entre TOUS les flux ───
const BLOCK_GROUPS_KEY='phonelabs.blockgroups';
function loadBlockGroups(){try{return JSON.parse(localStorage.getItem(BLOCK_GROUPS_KEY))||[];}catch(e){return[];}}
function saveBlockGroups(g){try{localStorage.setItem(BLOCK_GROUPS_KEY,JSON.stringify(g));}catch(e){}}

// ─── Verrou de proxy PARTAGÉ entre fenêtres (localStorage, même origine) ───
// Empêche deux fenêtres de lancer deux iPhones sur le MÊME proxy en même temps
// (sinon deux runs sortent par la même IP → interférences). Chaque fenêtre a un
// id unique ; un verrou est rafraîchi tant que le run tourne, et expire après TTL
// si la fenêtre crashe (heartbeat manquant).
const PROXY_LOCK_KEY='pl.proxylocks';
const PROXY_LOCK_TTL=15000; // ms sans heartbeat → verrou considéré mort
function _plWinId(){return window.__plWinId||(window.__plWinId='w'+Math.random().toString(36).slice(2,9)+Date.now());}
function _plReadLocks(){try{return JSON.parse(localStorage.getItem(PROXY_LOCK_KEY)||'{}')||{};}catch(e){return {};}}
function _plWriteLocks(m){try{localStorage.setItem(PROXY_LOCK_KEY,JSON.stringify(m));}catch(e){}}
function _plPrune(m){const now=Date.now();for(const k in m){const v=m[k];if(!v||now-(v.ts||0)>PROXY_LOCK_TTL)delete m[k];}}
// Verrou détenu par une AUTRE fenêtre ? renvoie l'entrée {owner,udid,label,ts} sinon null.
function proxyLockedByOther(proxy){
  if(!proxy)return null;
  const m=_plReadLocks();_plPrune(m);
  const v=m[proxy];
  return (v&&v.owner!==_plWinId())?v:null;
}
// Réserve le proxy pour cette fenêtre. Renvoie false s'il est déjà pris ailleurs.
function reserveProxy(proxy,udid,label){
  if(!proxy)return true;
  const m=_plReadLocks();_plPrune(m);
  const v=m[proxy];
  if(v&&v.owner!==_plWinId())return false;
  m[proxy]={owner:_plWinId(),udid:udid||'',label:label||'',ts:Date.now()};
  _plWriteLocks(m);return true;
}
function refreshProxy(proxy){
  if(!proxy)return;
  const m=_plReadLocks();const v=m[proxy];
  if(v&&v.owner===_plWinId()){v.ts=Date.now();_plWriteLocks(m);}
}
function releaseProxy(proxy){
  if(!proxy)return;
  const m=_plReadLocks();const v=m[proxy];
  if(v&&v.owner===_plWinId()){delete m[proxy];_plWriteLocks(m);}
}
function releaseAllProxies(){
  const m=_plReadLocks();let ch=false;
  for(const k in m){if(m[k]&&m[k].owner===_plWinId()){delete m[k];ch=true;}}
  if(ch)_plWriteLocks(m);
}

// ─── Verrou d'APPAREIL partagé entre fenêtres (même mécanisme) ───
// Une fenêtre qui a un iPhone sélectionné le "réserve" → une autre fenêtre ne peut pas
// piloter le même iPhone (anti-confusion). Même id de fenêtre + TTL que le verrou proxy.
const DEVICE_LOCK_KEY='pl.devicelocks';
function _dlRead(){try{return JSON.parse(localStorage.getItem(DEVICE_LOCK_KEY)||'{}')||{};}catch(e){return {};}}
function _dlWrite(m){try{localStorage.setItem(DEVICE_LOCK_KEY,JSON.stringify(m));}catch(e){}}
function _dlPrune(m){const now=Date.now();for(const k in m){const v=m[k];if(!v||now-(v.ts||0)>PROXY_LOCK_TTL)delete m[k];}}
function deviceLockedByOther(udid){
  if(!udid)return null;
  const m=_dlRead();_dlPrune(m);
  const v=m[udid];
  return (v&&v.owner!==_plWinId())?v:null;
}
function reserveDevice(udid){
  if(!udid)return true;
  const m=_dlRead();_dlPrune(m);
  const v=m[udid];
  if(v&&v.owner!==_plWinId())return false;
  m[udid]={owner:_plWinId(),ts:Date.now()};
  _dlWrite(m);return true;
}
function refreshDevice(udid){if(!udid)return;const m=_dlRead();const v=m[udid];if(v&&v.owner===_plWinId()){v.ts=Date.now();_dlWrite(m);}}
function releaseDevice(udid){if(!udid)return;const m=_dlRead();const v=m[udid];if(v&&v.owner===_plWinId()){delete m[udid];_dlWrite(m);}}
function releaseAllDevices(){const m=_dlRead();let ch=false;for(const k in m){if(m[k]&&m[k].owner===_plWinId()){delete m[k];ch=true;}}if(ch)_dlWrite(m);}

// Libère tout à la fermeture de la fenêtre (sécurité en plus du TTL)
if(!window.__plProxyUnload){window.__plProxyUnload=true;window.addEventListener('beforeunload',()=>{releaseAllProxies();releaseAllDevices();});}

// ─── Drapeau pays d'un proxy (noms uniques pour éviter toute collision globale) ───
const AUTO_FLAG={US:"🇺🇸",GB:"🇬🇧",FR:"🇫🇷",DE:"🇩🇪",NL:"🇳🇱",JP:"🇯🇵",CA:"🇨🇦",AU:"🇦🇺",SG:"🇸🇬",BR:"🇧🇷",IT:"🇮🇹",ES:"🇪🇸",IN:"🇮🇳"};
function autoFlagFromHost(h){
  if(!h)return"";
  const s=String(h).toLowerCase();
  if(/\bus\b|usa|united.?states|new.?york|\bny\b|\bnyc\b|chicago|dallas|miami|seattle|atlanta|ashburn|losangeles|\bla\b/.test(s))return"US";
  if(/\buk\b|\bgb\b|britain|london|\blon\b|england/.test(s))return"GB";
  if(/\bfr\b|france|paris|\bpar\b/.test(s))return"FR";
  if(/\bde\b|germany|berlin|frankfurt|\bfra\b/.test(s))return"DE";
  if(/\bnl\b|netherlands|amsterdam|\bams\b/.test(s))return"NL";
  if(/\bjp\b|japan|tokyo|\btyo\b/.test(s))return"JP";
  if(/\bca\b|canada|toronto|montreal/.test(s))return"CA";
  if(/\bau\b|australia|sydney/.test(s))return"AU";
  if(/\bsg\b|singapore/.test(s))return"SG";
  if(/\bbr\b|brazil|brasil/.test(s))return"BR";
  if(/\bit\b|italy|milan|rome/.test(s))return"IT";
  if(/\bes\b|spain|madrid/.test(s))return"ES";
  if(/\bin\b|india|mumbai|delhi/.test(s))return"IN";
  return"";
}
function makeAutoNode(type,x,y,existingNodes){
  const t=AUTO_BLOCKS[type];const d={};(t.f||[]).forEach(f=>d[f.k]=f.v);
  // ID unique basé sur timestamp + aléatoire → pas de collision avec nœuds chargés
  const id='an'+Date.now()+'_'+(Math.random()*99999|0);
  return{id,type,x,y,d};
}
// BFS from fromId (or trigger_manual if null)
function autoGetOrder(nodes,edges,fromId){
  const start=fromId?nodes.find(n=>n.id===fromId):nodes.find(n=>n.type==='trigger_manual');
  if(!start)return nodes;
  const v=[],q=[start.id];
  while(q.length){const id=q.shift();if(v.includes(id))continue;v.push(id);edges.filter(e=>e.f===id).forEach(e=>q.push(e.t));}
  return v.map(id=>nodes.find(n=>n.id===id)).filter(Boolean);
}
function rv(a,b){return Math.floor(Math.random()*(b-a)+a);}
function _parseLabels(raw){try{return JSON.parse(raw||'[]').filter(l=>typeof l==='string'&&l.trim());}catch(_){return[];}}
function _parseZones(raw){try{return JSON.parse(raw||'[]').filter(z=>z&&typeof z.name==='string');}catch(_){return[];}}
function autoLogMsg(node){
  const d=node.d||{};
  if(node.type&&node.type.indexOf('media_cat_')===0){ const _c=(d.cat)||((AUTO_BLOCKS[node.type]||{}).f||[{}])[0].v||'média'; return'📤 Envoyer « '+_c+' » depuis le dossier du profil'; }
  switch(node.type){
    case'trigger_manual': return'🚀 Démarrage manuel';
    case'wda_launch':     return'📱 Ouverture '+(d.bundle||'app');
    case'wda_tap_text':   return'👆 Tap "'+(d.text||'?')+'"';
    case'wda_tap_ai':     return'🔍 Tap IA: "'+(d.prompt||'?')+'"';
    case'wda_tap_router':{const _ls=_parseLabels(d.labels);return'🔀 Détection: '+(_ls.join(' | ')||'?');}
    case'wda_zone_tap':{const _zz=_parseZones(d.zones);return'📍 Zones: '+(_zz.map(z=>z.name).join(', ')||'(aucune)');}
    case'wda_tap_xy':     return'🎯 '+(d.label?'"'+d.label+'" ':'')+'Tap ('+(d.x||50)+'%, '+(d.y||50)+'%)';
    case'wda_tap_repeat': return'🔁 '+(d.label?'"'+d.label+'" ':'')+'Tap ×'+(d.n||5)+' ('+(d.x||50)+'%, '+(d.y||50)+'%)';
    case'wda_date_picker': return'📅 Sélection date naissance…';
    case'wda_type':       return'⌨️  Saisie "'+(d.text||'?')+'"';
    case'wda_swipe':      return'👋 Swipe '+(d.dir||'UP');
    case'wda_long_drag': return'🖐 Long press '+(d.label||'')+'  ('+(d.hold||'1.5')+'s) → ('+d.x2+'%,'+d.y2+'%)';
    case'wda_wait_elem':  return'👁 Attente "'+(d.text||'?')+'" (max '+(d.timeout||15)+'s)';
    case'wda_home':       return'🏠 Bouton Home';
    case'wda_screenshot': return'📸 '+(d.name||'cap')+'.png';
    case'farm_rotate_ip': return'🔄 IP → '+rv(100,250)+'.'+rv(10,250)+'.'+rv(1,250)+'.'+rv(1,250);
    case'farm_buy_number':return'📞 N° '+(d.country||'France')+' via '+(d.prov||'HeroSMS')+' → +'+rv(10000000,99999999);
    case'farm_switch_provider':return'🔀 Switch → '+(d.providers||'HeroSMS,5sim,GetAText');
    case'farm_wait_sms':  return'📨 Attente SMS (max '+(d.timeout||360)+'s)…';
    case'farm_enter_code':return'🔢 Code SMS saisi';
    case'farm_enable_2fa':return'🔐 2FA TOTP activé';
    case'farm_save_acct': return'💾 Compte sauvegardé dans comptes.csv';
    case'farm_set_plug':  return'🔌 IG '+((d.plug||'Plug')==='Plug'?'Plug ✅':'Pas plug ⬜');
    case'media_send_reel': return'🎬 Envoyer un Reels (dossier Vidéo)';
    case'media_send_photo':return'🖼 Envoyer une photo (dossier Photo)';
    case'media_send_pfp':  return'🧑 Photo de profil (dossier dédié)';
    case'content_spoof':   return'🧬 Spoof « '+((d.pack||'?').replace(/^—.*—$/,'(pack ?)'))+' » → dossier du profil';
    case'logic_wait':     return'⏳ Attente '+(d.s||2)+'s';
    case'logic_condition':return'❓ Condition: "'+(d.check||'?')+'" ?';
    case'logic_ai_cond':  return'🤖 IA ? "'+(d.prompt||'?')+'"';
    case'logic_loop':     return'🔁 Boucle ×'+(d.n||3);
    case'logic_random':   return'🎲 Aléatoire → 1/'+(d.n||3)+' sorties';
    case'logic_merge':    return'⤵ Convergence';
    case'logic_split':    return'⑃ Diviser → '+(d.n||3)+' branches';
    case'logic_end':      return'🔴 Arrêt forcé — batch stoppé';
    case'logic_cycle_end':return'🔁 Fin de cycle — cooldown + prochain run';
    case'gen_account':    return'👤 Génération infos compte…';
    case'regen_username': return'🔄 Régénération username…';
    case'check_color':    return'🔍 '+(d.label||'Vérif. couleur')+' ('+d.target+')';
    case'ai_2fa':         return'🤖 Analyse 2FA via Gemini…';
    default:              return'▶ '+(AUTO_BLOCKS[node.type]?AUTO_BLOCKS[node.type].l:node.type);
  }
}

// ─── ResizeHandle ─────────────────────────────────────────
function ResizeHandle({onMouseDown}){
  const [h,setH]=React.useState(false);
  return(
    <div onMouseDown={onMouseDown} onMouseEnter={()=>setH(true)} onMouseLeave={()=>setH(false)}
      style={{width:4,cursor:'col-resize',flex:'none',background:h?'#388bfd33':'transparent',transition:'background .15s',zIndex:20,position:'relative'}}
      title="Glisser pour redimensionner">
      <div style={{position:'absolute',top:'50%',left:'50%',transform:'translate(-50%,-50%)',width:2,height:40,background:h?'#388bfd':'#30363d',borderRadius:2,transition:'background .15s'}}></div>
    </div>
  );
}

// ─── AutoSidebar ──────────────────────────────────────────
function AutoSidebar({onDragStart,width,onCollapse,groups,onGroupDragStart,onDeleteGroup,platform}){
  const sc=Math.max(1,Math.min(2,width/190));
  const fs=Math.round(11*sc);
  const pd=Math.round(5*sc)+'px '+Math.round(10*sc)+'px';
  const dot=Math.round(6*sc);
  const gap=Math.round(6*sc);
  return(
    <div style={{width,background:'#161b22',borderRight:'1px solid #21262d',overflowY:'auto',flex:'none',paddingBottom:12}}>
      <div style={{display:'flex',alignItems:'center',justifyContent:'space-between',padding:'6px 8px 2px',position:'sticky',top:0,background:'#161b22',zIndex:2}}>
        <span style={{fontSize:9,color:'#484f58',fontWeight:700,textTransform:'uppercase',letterSpacing:'.07em'}}>Blocs</span>
        <button onClick={onCollapse} title="Replier les blocs" data-sfx="none"
          style={{background:'#21262d',border:'1px solid #30363d',borderRadius:5,color:'#8b949e',cursor:'pointer',fontSize:11,padding:'1px 7px',lineHeight:1.4}}>◀</button>
      </div>
      {Object.keys(AUTO_CAT).filter(cat=>{
        const pf=AUTO_CAT[cat].platform;
        return !pf || pf===(platform==='android'?'android':'ios');
      }).map(cat=>{
        const blocks=Object.entries(AUTO_BLOCKS).filter(([,b])=>b.cat===cat);
        return(
          <div key={cat}>
            <div style={{padding:Math.round(8*sc)+'px '+Math.round(10*sc)+'px '+Math.round(4*sc)+'px',fontSize:Math.round(9*sc),color:'#8b949e',fontWeight:700,textTransform:'uppercase',letterSpacing:'.07em',display:'flex',alignItems:'center',gap:4}}>
              {AUTO_CAT[cat].icon} {AUTO_CAT[cat].label}
            </div>
            {blocks.map(([type,blk])=>(
              <div key={type} draggable onDragStart={e=>onDragStart(e,type)}
                style={{display:'flex',alignItems:'center',gap,padding:pd,margin:'1px '+Math.round(5*sc)+'px',borderRadius:Math.round(7*sc),border:'1px solid #30363d',background:'#21262d',fontSize:fs,fontWeight:600,cursor:'grab',userSelect:'none',transition:'border-color .1s'}}
                onMouseEnter={e=>e.currentTarget.style.borderColor=blk.c}
                onMouseLeave={e=>e.currentTarget.style.borderColor='#30363d'}>
                <div style={{width:dot,height:dot,borderRadius:'50%',background:blk.c,flex:'none'}}></div>
                {blk.e} {blk.l}
              </div>
            ))}
          </div>
        );
      })}
      {/* ── Mes groupes de blocs (macros réutilisables) ── */}
      {groups&&groups.length>0&&(
        <div>
          <div style={{padding:Math.round(8*sc)+'px '+Math.round(10*sc)+'px '+Math.round(4*sc)+'px',fontSize:Math.round(9*sc),color:'#bc8cff',fontWeight:700,textTransform:'uppercase',letterSpacing:'.07em',display:'flex',alignItems:'center',gap:4}}>
            📦 Mes groupes
          </div>
          {groups.map(g=>(
            <div key={g.id} draggable onDragStart={e=>onGroupDragStart(e,g.id)}
              title="Glisser dans le flux pour insérer ce groupe"
              style={{display:'flex',alignItems:'center',gap,padding:pd,margin:'1px '+Math.round(5*sc)+'px',borderRadius:Math.round(7*sc),border:'1px solid rgba(137,87,229,.35)',background:'rgba(137,87,229,.1)',fontSize:fs,fontWeight:600,cursor:'grab',userSelect:'none',transition:'border-color .1s'}}
              onMouseEnter={e=>e.currentTarget.style.borderColor='#8957e5'}
              onMouseLeave={e=>e.currentTarget.style.borderColor='rgba(137,87,229,.35)'}>
              <span style={{flex:1,minWidth:0,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',color:'#e6edf3'}}>📦 {g.name} <span style={{color:'#8b949e',fontSize:Math.round(9*sc)}}>·{g.count}</span></span>
              <button onClick={e=>{e.stopPropagation();if(window.confirm('Supprimer le groupe « '+g.name+' » ?'))onDeleteGroup(g.id);}} data-sfx="none"
                title="Supprimer ce groupe" style={{flex:'none',background:'none',border:'none',color:'#8b949e',cursor:'pointer',fontSize:Math.round(11*sc),padding:'0 2px'}}>✕</button>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ─── DataContext — carte référence des {{variables}} ─────────
function DataContext({node,onHeaderMouseDown,onDelete,onCtxMenu,isActive}){
  const t=AUTO_BLOCKS['data_context'];
  const [copied,setCopied]=React.useState(null);
  function copy(key){
    try{navigator.clipboard.writeText('{{'+key+'}}');}catch(e){}
    setCopied(key);setTimeout(()=>setCopied(null),1200);
  }
  return(
    <div id={'anode_'+node.id} data-node={node.id}
      style={{position:'absolute',width:230,left:node.x,top:node.y,borderRadius:10,border:'1.5px solid '+(isActive?t.c:'#30363d'),background:'#130f1e',boxShadow:isActive?('0 0 0 2px '+t.c+', 0 6px 24px rgba(0,0,0,.8)'):'0 2px 10px rgba(0,0,0,.5)',zIndex:isActive?10:2}}
      onContextMenu={e=>{e.preventDefault();e.stopPropagation();onCtxMenu(e,node.id);}}>
      <div onMouseDown={onHeaderMouseDown}
        style={{display:'flex',alignItems:'center',gap:6,padding:'7px 8px 6px',borderRadius:'9px 9px 0 0',background:'rgba(137,87,229,.15)',borderBottom:'1px solid rgba(137,87,229,.25)',cursor:'move',userSelect:'none'}}>
        <span style={{fontSize:13}}>📋</span>
        <span style={{fontSize:11,fontWeight:700,color:'#bc8cff',flex:1}}>Contexte runtime</span>
        <button onClick={e=>{e.stopPropagation();onDelete();}}
          style={{background:'none',border:'none',color:'#8b949e',cursor:'pointer',fontSize:11,padding:0,width:16,height:16,display:'flex',alignItems:'center',justifyContent:'center'}}>✕</button>
      </div>
      <div style={{padding:'6px 8px 8px',display:'flex',flexDirection:'column',gap:2}}>
        {CTX_VARS.map(v=>(
          <div key={v.key} style={{display:'flex',alignItems:'center',gap:6,padding:'4px 6px',borderRadius:5,background:'rgba(255,255,255,.03)',border:'1px solid rgba(255,255,255,.05)'}}>
            <span style={{fontSize:10,flex:'none'}}>{v.e}</span>
            <div style={{flex:1,minWidth:0}}>
              <div style={{fontSize:9,color:'#8b949e',lineHeight:1.1}}>{v.label}</div>
              <div style={{fontSize:10,fontWeight:700,color:'#bc8cff',fontFamily:'monospace',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{'{{'+v.key+'}}'}</div>
            </div>
            <button onClick={e=>{e.stopPropagation();copy(v.key);}}
              style={{padding:'2px 6px',background:copied===v.key?'rgba(63,185,80,.2)':'rgba(137,87,229,.15)',border:'1px solid '+(copied===v.key?'rgba(63,185,80,.4)':'rgba(137,87,229,.3)'),borderRadius:4,color:copied===v.key?'#3fb950':'#bc8cff',fontSize:9,cursor:'pointer',flex:'none',transition:'all .15s',fontWeight:700}}>
              {copied===v.key?'✓':'📋'}
            </button>
          </div>
        ))}
        <div style={{marginTop:3,padding:'4px 6px',borderRadius:4,background:'rgba(137,87,229,.07)',border:'1px solid rgba(137,87,229,.15)'}}>
          <div style={{fontSize:9,color:'#8b949e',lineHeight:1.4}}>
            ℹ️ Utilise <span style={{color:'#bc8cff',fontFamily:'monospace',fontWeight:700}}>{'{{variable}}'}</span> dans n'importe quel champ <span style={{color:'#e6edf3'}}>Saisir texte</span>
          </div>
        </div>
      </div>
    </div>
  );
}

// ─── DataIp — affichage IP actuelle du device ───────────────
function DataIp({node,onHeaderMouseDown,onDelete,onCtxMenu,isActive,deviceId}){
  const t=AUTO_BLOCKS['data_ip'];
  const [ip,setIp]=React.useState('—');
  const [loading,setLoading]=React.useState(false);
  async function fetchIp(){
    setLoading(true);
    try{
      // Passe par le proxy du téléphone pour avoir la vraie IP vue par Instagram
      const base=window.Backend&&window.Backend.base?window.Backend.base:'http://127.0.0.1:8787';
      const r=await fetch(base+'/api/proxy-ip?ts='+Date.now(),{cache:'no-store'});
      const j=await r.json();
      if(j.ok&&j.ip){setIp(j.ip);}
      else{setIp('Erreur: '+(j.error||'?'));}
    }catch(e){setIp('Erreur: '+(e&&e.message||'?'));}
    setLoading(false);
  }
  React.useEffect(()=>{fetchIp();},[deviceId]);
  // Auto-refresh quand une rotation IP vient d'être faite
  React.useEffect(()=>{
    const h=()=>fetchIp();
    window.addEventListener('ip-rotated',h);
    return()=>window.removeEventListener('ip-rotated',h);
  },[]);
  return(
    <div id={'anode_'+node.id} data-node={node.id}
      style={{position:'absolute',width:220,left:node.x,top:node.y,borderRadius:10,border:'1.5px solid '+(isActive?t.c:'#30363d'),background:'#0d1a1a',boxShadow:isActive?('0 0 0 2px '+t.c+', 0 6px 24px rgba(0,0,0,.8)'):'0 2px 10px rgba(0,0,0,.5)',zIndex:isActive?10:2}}
      onContextMenu={e=>{e.preventDefault();e.stopPropagation();onCtxMenu(e,node.id);}}>
      <div onMouseDown={onHeaderMouseDown}
        style={{display:'flex',alignItems:'center',gap:6,padding:'7px 8px 6px',borderRadius:'9px 9px 0 0',background:'rgba(29,186,180,.12)',borderBottom:'1px solid rgba(29,186,180,.2)',cursor:'move',userSelect:'none'}}>
        <span style={{fontSize:13}}>🌐</span>
        <span style={{fontSize:11,fontWeight:700,color:'#1dbab4',flex:1}}>IP actuelle</span>
        <button onClick={e=>{e.stopPropagation();onDelete();}}
          style={{background:'none',border:'none',color:'#8b949e',cursor:'pointer',fontSize:11,padding:0,width:16,height:16,display:'flex',alignItems:'center',justifyContent:'center'}}>✕</button>
      </div>
      <div style={{padding:'8px 10px',display:'flex',flexDirection:'column',gap:6}}>
        <div style={{fontFamily:'monospace',fontSize:13,fontWeight:700,color:'#1dbab4',textAlign:'center',letterSpacing:'.05em',minHeight:20}}>
          {loading?<span style={{color:'#8b949e',fontSize:10}}>Récupération…</span>:ip}
        </div>
        <button onClick={e=>{e.stopPropagation();fetchIp();}}
          style={{padding:'4px 0',background:'rgba(29,186,180,.12)',border:'1px solid rgba(29,186,180,.3)',borderRadius:5,color:'#1dbab4',fontSize:10,fontWeight:700,cursor:'pointer',letterSpacing:'.03em'}}
          onMouseEnter={e=>e.currentTarget.style.background='rgba(29,186,180,.22)'}
          onMouseLeave={e=>e.currentTarget.style.background='rgba(29,186,180,.12)'}>
          🔄 Actualiser
        </button>
      </div>
    </div>
  );
}

// ─── AnnotDraw — canvas libre ──────────────────────────────
function AnnotDraw({node,onChange,onHeaderMouseDown,onDelete,onCtxMenu,isActive}){
  const t=AUTO_BLOCKS[node.type];
  const cvRef=React.useRef(null);
  const drawRef=React.useRef(false);
  const lastRef=React.useRef(null);
  const [penColor,setPenColor]=React.useState('#e6edf3');
  const [penSize,setPenSize]=React.useState(2);
  React.useEffect(()=>{
    if(!cvRef.current||!node.d.img)return;
    const img=new Image();img.onload=()=>{const ctx=cvRef.current&&cvRef.current.getContext('2d');if(ctx)ctx.drawImage(img,0,0);};img.src=node.d.img;
  },[]);
  function getPos(e){const r=cvRef.current.getBoundingClientRect();return{x:e.clientX-r.left,y:e.clientY-r.top};}
  function onDraw(e){e.preventDefault();e.stopPropagation();drawRef.current=true;lastRef.current=getPos(e);}
  function onMove(e){
    if(!drawRef.current)return;e.stopPropagation();
    const pos=getPos(e);const ctx=cvRef.current.getContext('2d');
    ctx.beginPath();ctx.moveTo(lastRef.current.x,lastRef.current.y);ctx.lineTo(pos.x,pos.y);
    ctx.strokeStyle=penColor;ctx.lineWidth=penSize;ctx.lineCap='round';ctx.stroke();lastRef.current=pos;
  }
  function onUp(e){if(!drawRef.current)return;drawRef.current=false;onChange('img',cvRef.current.toDataURL());}
  function onClear(){const ctx=cvRef.current.getContext('2d');ctx.clearRect(0,0,cvRef.current.width,cvRef.current.height);onChange('img','');}
  const COLORS=['#e6edf3','#f78166','#3fb950','#79c0ff','#d29922','#bc8cff','#ff7b72'];
  return(
    <div id={'anode_'+node.id} data-node={node.id}
      style={{position:'absolute',width:240,left:node.x,top:node.y,borderRadius:10,border:'1.5px solid '+(isActive?t.c:'#30363d'),background:'#161b22',boxShadow:isActive?('0 0 0 2px '+t.c+', 0 6px 24px rgba(0,0,0,.8)'):'0 2px 10px rgba(0,0,0,.5)',zIndex:isActive?10:2}}
      onContextMenu={e=>{e.preventDefault();e.stopPropagation();onCtxMenu(e,node.id);}}>
      <div onMouseDown={onHeaderMouseDown}
        style={{display:'flex',alignItems:'center',gap:6,padding:'7px 8px 6px',borderRadius:'9px 9px 0 0',background:t.c+'22',borderBottom:'1px solid #21262d',cursor:'move',userSelect:'none'}}>
        <span style={{fontSize:13}}>{t.e}</span>
        <span style={{fontSize:11,fontWeight:700,color:t.c,flex:1}}>Dessin libre</span>
        <button onClick={e=>{e.stopPropagation();onClear();}} title="Effacer"
          style={{background:'none',border:'none',color:'#8b949e',cursor:'pointer',fontSize:10,padding:'0 4px'}}>🗑</button>
        <button onClick={e=>{e.stopPropagation();onDelete();}}
          style={{background:'none',border:'none',color:'#8b949e',cursor:'pointer',fontSize:11,padding:0,width:16,height:16,display:'flex',alignItems:'center',justifyContent:'center',borderRadius:3}}>✕</button>
      </div>
      <div style={{padding:'6px 8px 4px',display:'flex',gap:4,alignItems:'center',flexWrap:'wrap'}}>
        {COLORS.map(c=>(
          <div key={c} onClick={e=>{e.stopPropagation();setPenColor(c);}}
            style={{width:14,height:14,borderRadius:'50%',background:c,cursor:'pointer',border:penColor===c?'2px solid #fff':'2px solid transparent',boxSizing:'border-box'}} />
        ))}
        <div style={{marginLeft:'auto',display:'flex',gap:3}}>
          {[1,2,4].map(s=>(
            <div key={s} onClick={e=>{e.stopPropagation();setPenSize(s);}}
              style={{width:14,height:14,borderRadius:'50%',background:penSize===s?'#58a6ff':'#30363d',cursor:'pointer',display:'flex',alignItems:'center',justifyContent:'center'}}>
              <div style={{width:s*2.5,height:s*2.5,borderRadius:'50%',background:penSize===s?'#fff':'#8b949e'}} />
            </div>
          ))}
        </div>
      </div>
      <canvas ref={cvRef} width={224} height={160}
        style={{display:'block',margin:'0 8px 8px',borderRadius:6,background:'#0d1117',cursor:'crosshair',width:224,height:160}}
        onMouseDown={onDraw} onMouseMove={onMove} onMouseUp={onUp} onMouseLeave={onUp} />
    </div>
  );
}

// ─── TotpTimer — code TOTP live avec countdown 30s ───────────
function TotpTimer({secret}){
  const [state,setState]=React.useState({code:'------',secs:30});
  React.useEffect(()=>{
    if(!secret)return;
    let alive=true;
    async function tick(){
      if(!alive)return;
      const now=Date.now();
      const period=Math.floor(now/30000);
      const secs=30-Math.floor((now%30000)/1000);
      try{
        const B32='ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
        const s=secret.toUpperCase().replace(/[^A-Z2-7]/g,'');
        let bits='';for(const c of s){const v=B32.indexOf(c);if(v<0)continue;bits+=v.toString(2).padStart(5,'0');}
        const bytes=new Uint8Array(Math.floor(bits.length/8));
        for(let i=0;i<bytes.length;i++)bytes[i]=parseInt(bits.slice(i*8,(i+1)*8),2);
        const cb=new Uint8Array(8);let cnt=period;for(let i=7;i>=0;i--){cb[i]=cnt&0xff;cnt=Math.floor(cnt/256);}
        const key=await crypto.subtle.importKey('raw',bytes,{name:'HMAC',hash:'SHA-1'},false,['sign']);
        const sig=new Uint8Array(await crypto.subtle.sign('HMAC',key,cb));
        const off=sig[sig.length-1]&0xf;
        const code=((sig[off]&0x7f)<<24|(sig[off+1]&0xff)<<16|(sig[off+2]&0xff)<<8|(sig[off+3]&0xff))%1000000;
        if(alive)setState({code:code.toString().padStart(6,'0'),secs});
      }catch(e){if(alive)setState({code:'??????',secs});}
    }
    tick();
    const iv=setInterval(tick,1000);
    return()=>{alive=false;clearInterval(iv);};
  },[secret]);
  const pct=state.secs/30;
  const col=state.secs>10?'#3fb950':state.secs>5?'#d29922':'#f85149';
  return(
    <div style={{display:'flex',alignItems:'center',gap:6}}>
      <div style={{fontFamily:'monospace',fontSize:15,color:'#bc8cff',fontWeight:700,letterSpacing:2}}>🔢 {state.code}</div>
      <div style={{position:'relative',width:20,height:20,flex:'none'}}>
        <svg viewBox="0 0 20 20" style={{position:'absolute',top:0,left:0,transform:'rotate(-90deg)'}}>
          <circle cx="10" cy="10" r="8" fill="none" stroke="#21262d" strokeWidth="2.5"/>
          <circle cx="10" cy="10" r="8" fill="none" stroke={col} strokeWidth="2.5"
            strokeDasharray={`${2*Math.PI*8}`}
            strokeDashoffset={`${2*Math.PI*8*(1-pct)}`}
            style={{transition:'stroke-dashoffset .9s linear,stroke .3s'}}/>
        </svg>
        <div style={{position:'absolute',top:'50%',left:'50%',transform:'translate(-50%,-50%)',fontSize:7,fontWeight:700,color:col}}>{state.secs}</div>
      </div>
    </div>
  );
}

// ─── RotateIpNode — bloc avec compte à rebours 5min ────────
const ROTATE_IP_COOLDOWN=5*60*1000; // 5 minutes en ms
const ROTATE_IP_LS='pl.rotateip.lastAt';
function RotateIpNode({node,onHeaderMouseDown,onDelete,onCtxMenu,isActive,isSelected,onPortOutDown}){
  const t=AUTO_BLOCKS['farm_rotate_ip'];
  const [remaining,setRemaining]=React.useState(0);
  const [lastIp,setLastIp]=React.useState(()=>{try{return localStorage.getItem('pl.rotateip.lastIp')||'';}catch(e){return '';}});
  React.useEffect(()=>{
    function tick(){
      try{
        const last=parseInt(localStorage.getItem(ROTATE_IP_LS)||'0');
        const rem=Math.max(0,ROTATE_IP_COOLDOWN-(Date.now()-last));
        setRemaining(rem);
      }catch(e){setRemaining(0);}
    }
    tick();
    const iv=setInterval(tick,1000);
    return()=>clearInterval(iv);
  },[]);
  // Écoute la rotation pour rafraîchir
  React.useEffect(()=>{
    const h=()=>{
      try{setLastIp(localStorage.getItem('pl.rotateip.lastIp')||'');}catch(e){}
      setRemaining(ROTATE_IP_COOLDOWN);
    };
    window.addEventListener('ip-rotated',h);
    return()=>window.removeEventListener('ip-rotated',h);
  },[]);
  const mins=Math.floor(remaining/60000);
  const secs=Math.floor((remaining%60000)/1000);
  const pct=remaining/ROTATE_IP_COOLDOWN;
  const ready=remaining===0;
  return(
    <div id={'anode_'+node.id} data-node={node.id}
      style={{position:'absolute',width:200,left:node.x,top:node.y,borderRadius:10,
        border:'1.5px solid '+(isActive?t.c:ready?'#1dbab4':'#30363d'),
        background:'#0d1a1a',
        boxShadow:isActive?('0 0 0 2px '+t.c+', 0 6px 24px rgba(0,0,0,.8)'):'0 2px 10px rgba(0,0,0,.5)',
        zIndex:isActive?10:2,outline:isSelected?'2px solid #388bfd':'none',outlineOffset:2}}
      onContextMenu={e=>{e.preventDefault();e.stopPropagation();onCtxMenu(e,node.id);}}>
      <div onMouseDown={onHeaderMouseDown}
        style={{display:'flex',alignItems:'center',gap:6,padding:'7px 8px 6px',borderRadius:'9px 9px 0 0',position:'relative',
          background:'rgba(29,186,180,.12)',borderBottom:'1px solid rgba(29,186,180,.2)',cursor:'move',userSelect:'none'}}>
        <span style={{fontSize:13}}>🔄</span>
        <span style={{fontSize:11,fontWeight:700,color:'#1dbab4',flex:1}}>Rotation IP</span>
        <button onClick={e=>{e.stopPropagation();try{window.SoundBoard&&window.SoundBoard.playForce('error');}catch(_){}}} data-sfx="none"
          title="Écouter le son d'erreur de la rotation IP"
          style={{background:'none',border:'none',color:'#8b949e',cursor:'pointer',fontSize:12,padding:0,width:16,height:16,display:'flex',alignItems:'center',justifyContent:'center'}}
          onMouseEnter={e=>e.currentTarget.style.color='#1dbab4'} onMouseLeave={e=>e.currentTarget.style.color='#8b949e'}>🔊</button>
        <button onClick={e=>{e.stopPropagation();onDelete();}}
          style={{background:'none',border:'none',color:'#8b949e',cursor:'pointer',fontSize:11,padding:0,width:16,height:16,display:'flex',alignItems:'center',justifyContent:'center'}}>✕</button>
        {/* Port d'entrée (gauche) */}
        <div data-port-in={node.id}
          style={{position:'absolute',left:-6,top:'50%',transform:'translateY(-50%)',width:11,height:11,borderRadius:'50%',border:'2px solid #8b949e',background:'#0d1117',cursor:'crosshair',zIndex:10,transition:'transform .1s'}}
          onMouseEnter={e=>e.currentTarget.style.transform='translateY(-50%) scale(1.4)'}
          onMouseLeave={e=>e.currentTarget.style.transform='translateY(-50%)'} />
        {/* Port de sortie (droite) — draggable */}
        <div onMouseDown={e=>{e.preventDefault();e.stopPropagation();onPortOutDown&&onPortOutDown(e);}}
          style={{position:'absolute',right:-6,top:'50%',transform:'translateY(-50%)',width:11,height:11,borderRadius:'50%',border:'2px solid #1dbab4',background:'#0d1117',cursor:'crosshair',zIndex:10,transition:'transform .1s'}}
          onMouseEnter={e=>e.currentTarget.style.transform='translateY(-50%) scale(1.4)'}
          onMouseLeave={e=>e.currentTarget.style.transform='translateY(-50%)'} />
      </div>
      <div style={{padding:'8px 10px',display:'flex',flexDirection:'column',gap:5}}>
        {lastIp&&<div style={{fontFamily:'monospace',fontSize:10,color:'#1dbab4',textAlign:'center'}}>Dernière IP : {lastIp}</div>}
        {!ready?(
          <>
            <div style={{fontSize:10,color:'#d29922',textAlign:'center',fontWeight:700}}>
              ⏳ Cooldown : {mins}:{secs.toString().padStart(2,'0')}
            </div>
            <div style={{height:4,borderRadius:2,background:'#21262d',overflow:'hidden'}}>
              <div style={{height:'100%',width:((1-pct)*100)+'%',background:'#1dbab4',transition:'width 1s linear'}} />
            </div>
          </>
        ):(
          <div style={{fontSize:10,color:'#3fb950',textAlign:'center',fontWeight:700}}>✅ Prêt à changer</div>
        )}
      </div>
    </div>
  );
}

// ─── AutoNode ─────────────────────────────────────────────
function AutoNode({node,isActive,isSelected,onHeaderMouseDown,onPortOutDown,onDelete,onChange,onCtxMenu,onStartCapture,deviceId,nodeResult:_nr}){
  const t=AUTO_BLOCKS[node.type];if(!t)return null;
  const nodeResult=_nr;
  if(node.type==='annot_draw') return React.createElement(AnnotDraw,{node,onChange,onHeaderMouseDown,onDelete,onCtxMenu,isActive,isSelected});
  if(node.type==='data_context') return React.createElement(DataContext,{node,onHeaderMouseDown,onDelete,onCtxMenu,isActive,isSelected});
  if(node.type==='data_ip') return React.createElement(DataIp,{node,onHeaderMouseDown,onDelete,onCtxMenu,isActive,isSelected,deviceId});
  if(node.type==='farm_rotate_ip') return React.createElement(RotateIpNode,{node,onHeaderMouseDown,onDelete,onCtxMenu,isActive,isSelected,onPortOutDown});
  const [appPicker,setAppPicker]=React.useState(false);
  const [routerInput,setRouterInput]=React.useState('');
  const [zoneInput,setZoneInput]=React.useState('');
  const pickerRef=React.useRef(null);
  const currentApp=AUTO_APPS.find(a=>a.bundle===(node.d&&node.d.bundle))||null;
  React.useEffect(()=>{
    if(!appPicker)return;
    function onOutside(e){if(pickerRef.current&&!pickerRef.current.contains(e.target))setAppPicker(false);}
    document.addEventListener('mousedown',onOutside);
    return()=>document.removeEventListener('mousedown',onOutside);
  },[appPicker]);
  const nodeW=t.w||182;
  const isNote=node.type==='annot_note';
  return(
    <div id={'anode_'+node.id} data-node={node.id}
      style={{position:'absolute',width:nodeW,left:node.x,top:node.y,borderRadius:10,border:'1.5px solid '+(isActive?t.c:'#30363d'),background:isNote?'#1e1a09':'#1c2128',boxShadow:isActive?('0 0 0 2px '+t.c+', 0 6px 24px rgba(0,0,0,.8)'):'0 2px 10px rgba(0,0,0,.5)',zIndex:appPicker?50:isActive?10:2,transition:'border-color .1s,box-shadow .15s',outline:isSelected?'2px solid #388bfd':'none',outlineOffset:2}}
      onContextMenu={e=>{e.preventDefault();e.stopPropagation();onCtxMenu(e,node.id);}}>
      <div onMouseDown={onHeaderMouseDown}
        style={{display:'flex',alignItems:'center',gap:6,padding:'8px 8px 6px',borderRadius:'9px 9px 0 0',background:t.c+'18',borderBottom:'1px solid #21262d',cursor:'move',position:'relative',userSelect:'none'}}>
        <span style={{fontSize:13}}>{t.e}</span>
        <span style={{fontSize:11.5,fontWeight:700,color:t.c,flex:1,lineHeight:1.2}}>{t.l}</span>
        <button onClick={e=>{e.stopPropagation();onDelete();}}
          style={{background:'none',border:'none',color:'#8b949e',cursor:'pointer',fontSize:11,padding:0,width:16,height:16,display:'flex',alignItems:'center',justifyContent:'center',borderRadius:3,flex:'none'}}
          onMouseEnter={e=>{e.currentTarget.style.background='#da363322';e.currentTarget.style.color='#f85149';}}
          onMouseLeave={e=>{e.currentTarget.style.background='none';e.currentTarget.style.color='#8b949e';}}>✕</button>
        {!t.noOut&&!t.multiOut&&!t.branch&&!t.loopOut&&!t.routerOut&&(
          <div onMouseDown={e=>{e.preventDefault();e.stopPropagation();onPortOutDown(e);}}
            style={{position:'absolute',right:-6,top:'50%',transform:'translateY(-50%)',width:11,height:11,borderRadius:'50%',border:'2px solid '+t.c,background:'#0d1117',cursor:'crosshair',zIndex:10,transition:'transform .1s'}}
            onMouseEnter={e=>e.currentTarget.style.transform='translateY(-50%) scale(1.4)'}
            onMouseLeave={e=>e.currentTarget.style.transform='translateY(-50%)'} />
        )}
        {!t.noIn&&(<div data-port-in={node.id}
          style={{position:'absolute',left:-6,top:'50%',transform:'translateY(-50%)',width:11,height:11,borderRadius:'50%',border:'2px solid #8b949e',background:'#0d1117',cursor:'crosshair',zIndex:10,transition:'transform .1s'}}
          onMouseEnter={e=>e.currentTarget.style.transform='translateY(-50%) scale(1.4)'}
          onMouseLeave={e=>e.currentTarget.style.transform='translateY(-50%)'} />)}
      </div>
      {t.f&&t.f.length>0&&(
        <div style={{padding:'6px 8px 8px',display:'flex',flexDirection:'column',gap:4}}>
          {t.f.map(f=>{
            // Picker d'app pour le champ bundle de wda_launch
            if(node.type==='wda_launch'&&f.k==='bundle'){
              return(
                <div key={f.k} ref={pickerRef} style={{position:'relative'}}>
                  <div style={{fontSize:9.5,color:'#8b949e',fontWeight:700,textTransform:'uppercase',letterSpacing:'.05em',marginBottom:3}}>Application</div>
                  <button onMouseDown={e=>e.stopPropagation()} onClick={e=>{e.stopPropagation();setAppPicker(p=>!p);}}
                    style={{width:'100%',display:'flex',alignItems:'center',gap:5,padding:'5px 7px',background:'#0d1117',border:'1px solid '+(appPicker?'#388bfd':'#30363d'),borderRadius:6,color:'#e6edf3',fontSize:11,fontWeight:600,cursor:'pointer',transition:'border-color .15s',textAlign:'left',boxSizing:'border-box',overflow:'hidden'}}>
                    {currentApp
                      ?<><span style={{fontSize:13,flex:'none'}}>{currentApp.e}</span><span style={{flex:1,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{currentApp.name}</span></>
                      :<><span style={{flex:1,color:'#8b949e',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>Choisir une app…</span></>
                    }
                    <span style={{color:'#8b949e',fontSize:9,flex:'none'}}>{appPicker?'▲':'▼'}</span>
                  </button>
                  {appPicker&&(
                    <div onMouseDown={e=>e.stopPropagation()}
                      style={{marginTop:4,background:'#161b22',border:'1px solid #30363d',borderRadius:8,overflow:'hidden',boxShadow:'0 8px 24px rgba(0,0,0,.6)',zIndex:100,position:'absolute',width:220,left:0,maxHeight:320,overflowY:'auto'}}>
                      {AUTO_APPS.map(a=>(
                        <div key={a.id} onClick={e=>{e.stopPropagation();onChange('bundle',a.bundle);setAppPicker(false);}}
                          style={{display:'flex',alignItems:'center',gap:8,padding:'6px 10px',cursor:'pointer',borderBottom:'1px solid #21262d',transition:'background .1s',background:(node.d.bundle===a.bundle)?'rgba(56,139,253,.12)':'transparent'}}
                          onMouseEnter={e=>e.currentTarget.style.background='rgba(255,255,255,.06)'}
                          onMouseLeave={e=>e.currentTarget.style.background=(node.d.bundle===a.bundle)?'rgba(56,139,253,.12)':'transparent'}>
                          <span style={{width:24,height:24,borderRadius:6,background:a.c,display:'flex',alignItems:'center',justifyContent:'center',fontSize:12,flex:'none'}}>{a.e}</span>
                          <div style={{flex:1,minWidth:0}}>
                            <div style={{fontSize:11,fontWeight:700,color:node.d.bundle===a.bundle?'#58a6ff':'#e6edf3'}}>{a.name}</div>
                            <div style={{fontSize:8.5,color:'#484f58',fontFamily:'monospace',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{a.bundle}</div>
                          </div>
                          {node.d.bundle===a.bundle&&<span style={{color:'#58a6ff',fontSize:10,flex:'none'}}>✓</span>}
                        </div>
                      ))}
                      <div style={{padding:'5px 8px',borderTop:'1px solid #30363d'}}>
                        <input type="text" placeholder="Bundle ID personnalisé…"
                          defaultValue={AUTO_APPS.find(a=>a.bundle===node.d.bundle)?'':node.d.bundle||''}
                          onMouseDown={e=>e.stopPropagation()}
                          onBlur={e=>{if(e.target.value.trim())onChange('bundle',e.target.value.trim());}}
                          onKeyDown={e=>{if(e.key==='Enter'&&e.target.value.trim()){onChange('bundle',e.target.value.trim());setAppPicker(false);}}}
                          style={{width:'100%',background:'#21262d',border:'1px solid #30363d',borderRadius:4,padding:'4px 7px',color:'#8b949e',fontSize:10,outline:'none',boxSizing:'border-box'}}
                          onFocus={e=>e.currentTarget.style.borderColor='#58a6ff'}
                        />
                      </div>
                    </div>
                  )}
                </div>
              );
            }
            // Champ "label" sur wda_tap_xy → rendu comme un badge de nom
            if(node.type==='wda_tap_xy'&&f.k==='label'){
              return(
                <div key={f.k} style={{marginBottom:2}}>
                  <input type="text" value={node.d[f.k]||''} onChange={e=>onChange(f.k,e.target.value)}
                    placeholder="Nom du tap (ex: Cliquer Créer compte)"
                    onMouseDown={e=>e.stopPropagation()}
                    style={{background:'rgba(56,139,253,.08)',border:'1px solid rgba(56,139,253,.3)',borderRadius:5,padding:'4px 7px',color:'#79c0ff',fontSize:11,fontWeight:700,width:'100%',outline:'none',boxSizing:'border-box',fontStyle:node.d[f.k]?'normal':'italic'}}
                    onFocus={e=>e.currentTarget.style.borderColor='#58a6ff'}
                    onBlur={e=>e.currentTarget.style.borderColor='rgba(56,139,253,.3)'} />
                </div>
              );
            }
            // Champ "category" sur les blocs compte → dropdown des catégories existantes
            if((node.type==='farm_set_category'||node.type==='farm_assign_account')&&f.k==='category'){
              let cats=[];
              try{cats=(JSON.parse(localStorage.getItem('phonelabs.categories'))||[]).map(c=>c.label).filter(Boolean);}catch(e){}
              if(node.type==='farm_assign_account'&&cats.indexOf('Compte en attente')<0)cats=['Compte en attente',...cats];
              if(!cats.length)cats=['Warm Up'];
              return(
                <div key={f.k}>
                  <div style={{fontSize:9.5,color:'#8b949e',fontWeight:700,textTransform:'uppercase',letterSpacing:'.05em',marginBottom:2}}>{f.l}</div>
                  <select value={node.d[f.k]||f.v} onChange={e=>onChange(f.k,e.target.value)} onMouseDown={e=>e.stopPropagation()}
                    style={{background:'#0d1117',border:'1px solid #30363d',borderRadius:4,padding:'3px 6px',color:'#e6edf3',fontSize:11,width:'100%',outline:'none'}}>
                    {cats.map(o=>React.createElement('option',{key:o},o))}
                  </select>
                </div>
              );
            }
            // Champ "body" sur annot_note → textarea multi-lignes
            if(node.type==='annot_note'&&f.k==='body'){
              return(
                <div key={f.k}>
                  <textarea value={node.d[f.k]||''} onChange={e=>onChange(f.k,e.target.value)}
                    placeholder="Description de la phase…" rows={4}
                    onMouseDown={e=>e.stopPropagation()}
                    style={{background:'transparent',border:'none',borderTop:'1px solid rgba(210,153,34,.2)',borderRadius:0,padding:'6px 0 4px',color:'#c9b84c',fontSize:11,width:'100%',outline:'none',resize:'vertical',boxSizing:'border-box',fontFamily:'inherit',lineHeight:1.5}} />
                </div>
              );
            }
            return(
            <div key={f.k}>
              <div style={{fontSize:9.5,color:'#8b949e',fontWeight:700,textTransform:'uppercase',letterSpacing:'.05em',marginBottom:2}}>{f.l}</div>
              {f.o?(
                <select value={node.d[f.k]||f.v} onChange={e=>onChange(f.k,e.target.value)}
                  style={{background:'#0d1117',border:'1px solid #30363d',borderRadius:4,padding:'3px 6px',color:'#e6edf3',fontSize:11,width:'100%',outline:'none'}}>
                  {f.o.map(o=>React.createElement('option',{key:o},o))}
                </select>
              ):(
                <input type="text" value={node.d[f.k]||''} onChange={e=>onChange(f.k,e.target.value)} placeholder={f.l+'…'}
                  onMouseDown={e=>e.stopPropagation()}
                  style={{background:'#0d1117',border:'1px solid #30363d',borderRadius:4,padding:'3px 6px',color:'#e6edf3',fontSize:11,width:'100%',outline:'none',boxSizing:'border-box'}}
                  onFocus={e=>e.currentTarget.style.borderColor='#58a6ff'}
                  onBlur={e=>e.currentTarget.style.borderColor='#30363d'}
                  onMouseDown={e=>e.stopPropagation()} />
              )}
            </div>
            );
          })}
          {(t.branch||t.loopOut)&&(
            <div style={{display:'flex',flexDirection:'column',gap:4,marginTop:4,position:'relative'}}>
              {(t.loopOut?[{label:'🔁 BOUCLE',port:0,c:'#bc8cff',bg:'rgba(139,87,229,.15)',bc:'rgba(139,87,229,.35)'},{label:'✅ FIN',port:1,c:'#3fb950',bg:'rgba(35,134,54,.15)',bc:'rgba(35,134,54,.35)'}]:t.branchLabels||[{label:'OUI',port:0,c:'#3fb950',bg:'rgba(35,134,54,.15)',bc:'rgba(35,134,54,.35)'},{label:'NON',port:1,c:'#f85149',bg:'rgba(218,54,51,.15)',bc:'rgba(218,54,51,.35)'}]).map(br=>(
                <div key={br.port} style={{display:'flex',alignItems:'center',justifyContent:'flex-end',gap:6,position:'relative'}}>
                  <span style={{fontSize:9.5,padding:'2px 8px',borderRadius:4,background:br.bg,color:br.c,border:'1px solid '+br.bc,fontWeight:700}}>{br.label}</span>
                  <div onMouseDown={e=>{e.preventDefault();e.stopPropagation();onPortOutDown(e,br.port);}}
                    style={{width:11,height:11,borderRadius:'50%',border:'2px solid '+br.c,background:'#0d1117',cursor:'crosshair',flexShrink:0,transition:'transform .1s',marginRight:-6}}
                    onMouseEnter={e=>e.currentTarget.style.transform='scale(1.4)'}
                    onMouseLeave={e=>e.currentTarget.style.transform='scale(1)'} />
                </div>
              ))}
            </div>
          )}
          {t.multiOut&&(
            <div style={{paddingTop:4,borderTop:'1px solid #21262d',marginTop:2,display:'flex',flexDirection:'column',gap:4}}>
              <div style={{fontSize:9,color:'#8b949e',fontWeight:700,textTransform:'uppercase',letterSpacing:'.05em'}}>Sorties</div>
              {Array.from({length:Math.max(2,Math.min(8,parseInt(node.d.n)||3))},(_,i)=>(
                <div key={i} style={{display:'flex',alignItems:'center',justifyContent:'space-between',position:'relative'}}>
                  <span style={{fontSize:9.5,color:'#8b949e'}}>#{i+1}</span>
                  <div onMouseDown={e=>{e.preventDefault();e.stopPropagation();onPortOutDown(e);}}
                    style={{position:'absolute',right:-14,width:11,height:11,borderRadius:'50%',border:'2px solid '+t.c,background:'#0d1117',cursor:'crosshair',zIndex:10,transition:'transform .1s'}}
                    onMouseEnter={e=>e.currentTarget.style.transform='scale(1.4)'}
                    onMouseLeave={e=>e.currentTarget.style.transform='scale(1)'} />
                </div>
              ))}
            </div>
          )}
          {node.type==='check_color'&&onStartCapture&&(
            <button onMouseDown={e=>e.stopPropagation()} onClick={e=>{e.stopPropagation();onStartCapture({nodeId:node.id,fieldX:'x',fieldY:'y'});}}
              style={{marginTop:4,width:'100%',padding:'4px 0',background:'rgba(227,179,65,.12)',border:'1px solid rgba(227,179,65,.4)',borderRadius:5,color:'#e3b341',fontSize:10,fontWeight:700,cursor:'pointer',letterSpacing:'.03em'}}
              onMouseEnter={e=>{e.currentTarget.style.background='rgba(227,179,65,.25)';}}
              onMouseLeave={e=>{e.currentTarget.style.background='rgba(227,179,65,.12)';}}>
              🎯 Définir la zone
            </button>
          )}
          {(node.type==='wda_tap_xy'||node.type==='wda_tap_repeat')&&onStartCapture&&(
            <div style={{display:'flex',flexDirection:'column',gap:4,marginTop:4}}>
              <button onMouseDown={e=>e.stopPropagation()} onClick={e=>{e.stopPropagation();onStartCapture(node.id);}}
                style={{width:'100%',padding:'4px 0',background:'rgba(56,139,253,.15)',border:'1px solid rgba(56,139,253,.4)',borderRadius:5,color:'#58a6ff',fontSize:10,fontWeight:700,cursor:'pointer',letterSpacing:'.03em'}}
                onMouseEnter={e=>{e.currentTarget.style.background='rgba(56,139,253,.28)';}}
                onMouseLeave={e=>{e.currentTarget.style.background='rgba(56,139,253,.15)';}}>
                🎯 Enregistrer le tap
              </button>
              <button onMouseDown={e=>e.stopPropagation()} onClick={e=>{
                e.stopPropagation();
                if(window._autoTapPreview) window._autoTapPreview({
                  x:parseFloat(node.d.x||50)/100,
                  y:parseFloat(node.d.y||50)/100,
                });
              }}
                style={{width:'100%',padding:'4px 0',background:'rgba(56,139,253,.08)',border:'1px solid rgba(56,139,253,.3)',borderRadius:5,color:'#388bfd',fontSize:10,fontWeight:700,cursor:'pointer'}}
                onMouseEnter={e=>{e.currentTarget.style.background='rgba(56,139,253,.18)';}}
                onMouseLeave={e=>{e.currentTarget.style.background='rgba(56,139,253,.08)';}}>
                👁 Montrer le tap
              </button>
            </div>
          )}
          {node.type==='wda_long_drag'&&onStartCapture&&(
            <div style={{display:'flex',flexDirection:'column',gap:4,marginTop:4}}>
              <button onMouseDown={e=>e.stopPropagation()} onClick={e=>{e.stopPropagation();onStartCapture({nodeId:node.id,fieldX:'x1',fieldY:'y1'});}}
                style={{width:'100%',padding:'4px 0',background:'rgba(56,139,253,.15)',border:'1px solid rgba(56,139,253,.4)',borderRadius:5,color:'#58a6ff',fontSize:10,fontWeight:700,cursor:'pointer'}}>
                🔴 Enregistrer départ (A)
              </button>
              <button onMouseDown={e=>e.stopPropagation()} onClick={e=>{e.stopPropagation();onStartCapture({nodeId:node.id,fieldX:'x2',fieldY:'y2'});}}
                style={{width:'100%',padding:'4px 0',background:'rgba(56,139,253,.15)',border:'1px solid rgba(56,139,253,.4)',borderRadius:5,color:'#58a6ff',fontSize:10,fontWeight:700,cursor:'pointer'}}>
                🔵 Enregistrer arrivée (B)
              </button>
              <button onMouseDown={e=>e.stopPropagation()} onClick={e=>{
                e.stopPropagation();
                if(window._autoGesturePreview) window._autoGesturePreview({
                  x1:parseFloat(node.d.x1||50)/100, y1:parseFloat(node.d.y1||50)/100,
                  x2:parseFloat(node.d.x2||50)/100, y2:parseFloat(node.d.y2||70)/100,
                  hold:parseFloat(node.d.hold||1.5)
                });
              }}
                style={{width:'100%',padding:'4px 0',background:'rgba(56,139,253,.08)',border:'1px solid rgba(56,139,253,.3)',borderRadius:5,color:'#388bfd',fontSize:10,fontWeight:700,cursor:'pointer'}}>
                👁 Montrer le geste
              </button>
            </div>
          )}
          {node.type==='wda_date_picker'&&onStartCapture&&(
            <div style={{display:'flex',flexDirection:'column',gap:4,marginTop:4}}>
              <div style={{fontSize:9,color:'#8b949e',fontWeight:700,textTransform:'uppercase',letterSpacing:'.06em',paddingLeft:2}}>Calibration colonnes</div>
              {[
                {label:'📅 Calibrer Mois (X)',    fieldX:'x_month', fieldY:null},
                {label:'📅 Calibrer Jour (X)',    fieldX:'x_day',   fieldY:null},
                {label:'📅 Calibrer Année (X)',   fieldX:'x_year',  fieldY:null},
                {label:'⬆️ Calibrer Au-dessus (Y)',fieldX:null,      fieldY:'y_above'},
                {label:'⬇️ Calibrer En-dessous (Y)',fieldX:null,     fieldY:'y_below'},
              ].map(cfg=>(
                <button key={cfg.label} onMouseDown={e=>e.stopPropagation()}
                  onClick={e=>{e.stopPropagation();onStartCapture({nodeId:node.id,fieldX:cfg.fieldX,fieldY:cfg.fieldY});}}
                  style={{padding:'4px 6px',background:'rgba(31,111,235,.15)',border:'1px solid rgba(31,111,235,.4)',borderRadius:5,color:'#58a6ff',fontSize:10,fontWeight:700,cursor:'pointer',textAlign:'left'}}
                  onMouseEnter={e=>e.currentTarget.style.background='rgba(31,111,235,.28)'}
                  onMouseLeave={e=>e.currentTarget.style.background='rgba(31,111,235,.15)'}>
                  {cfg.label}
                </button>
              ))}
              <button onMouseDown={e=>e.stopPropagation()} onClick={e=>{
                e.stopPropagation();
                if(window._autoDatePickerPreview) window._autoDatePickerPreview({
                  x_month: parseFloat(node.d.x_month||17)/100,
                  x_day:   parseFloat(node.d.x_day  ||50)/100,
                  x_year:  parseFloat(node.d.x_year ||75)/100,
                  y_above: parseFloat(node.d.y_above||42)/100,
                  y_below: parseFloat(node.d.y_below||58)/100,
                });
              }}
                style={{padding:'4px 6px',background:'rgba(31,111,235,.08)',border:'1px solid rgba(31,111,235,.3)',borderRadius:5,color:'#388bfd',fontSize:10,fontWeight:700,cursor:'pointer',textAlign:'left'}}
                onMouseEnter={e=>e.currentTarget.style.background='rgba(31,111,235,.2)'}
                onMouseLeave={e=>e.currentTarget.style.background='rgba(31,111,235,.08)'}>
                👁 Montrer la calibration
              </button>
              <div style={{fontSize:9,color:'#484f58',paddingLeft:2,lineHeight:1.4}}>
                Appuie sur chaque bouton puis tape la zone correspondante sur l'iPhone
              </div>
            </div>
          )}
        </div>
      )}
      {t.routerOut&&(
        <div style={{padding:'6px 8px 8px',borderTop:'1px solid #21262d',display:'flex',flexDirection:'column',gap:4}}>
          <div style={{fontSize:9,color:'#8b949e',fontWeight:700,textTransform:'uppercase',letterSpacing:'.05em'}}>Éléments à détecter — priorité ↑</div>
          {_parseLabels(node.d.labels).map((label,i,arr)=>(
            <div key={label+i} style={{display:'flex',alignItems:'center',gap:3,position:'relative'}}>
              <div style={{display:'flex',flexDirection:'column',gap:0}}>
                <button onMouseDown={e=>e.stopPropagation()} onClick={e=>{e.stopPropagation();if(i>0){const ls=[...arr];const tmp=ls[i-1];ls[i-1]=ls[i];ls[i]=tmp;onChange('labels',JSON.stringify(ls));}}}
                  style={{background:'none',border:'none',color:i>0?'#8b949e':'#21262d',cursor:i>0?'pointer':'default',fontSize:9,padding:'0 2px',lineHeight:1.1}}>▲</button>
                <button onMouseDown={e=>e.stopPropagation()} onClick={e=>{e.stopPropagation();if(i<arr.length-1){const ls=[...arr];const tmp=ls[i+1];ls[i+1]=ls[i];ls[i]=tmp;onChange('labels',JSON.stringify(ls));}}}
                  style={{background:'none',border:'none',color:i<arr.length-1?'#8b949e':'#21262d',cursor:i<arr.length-1?'pointer':'default',fontSize:9,padding:'0 2px',lineHeight:1.1}}>▼</button>
              </div>
              <span style={{fontSize:9,color:'#484f58',minWidth:10,textAlign:'right',flexShrink:0}}>{i+1}</span>
              <div style={{flex:1,padding:'3px 6px',background:'rgba(137,87,229,.12)',border:'1px solid rgba(137,87,229,.35)',borderRadius:5,fontSize:11,color:'#bc8cff',fontWeight:600,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{label}</div>
              <button onMouseDown={e=>e.stopPropagation()} onClick={e=>{e.stopPropagation();const ls=_parseLabels(node.d.labels);ls.splice(i,1);onChange('labels',JSON.stringify(ls));}}
                style={{background:'none',border:'none',color:'#484f58',cursor:'pointer',fontSize:10,padding:'0 2px',flexShrink:0}}>✕</button>
              <div onMouseDown={e=>{e.preventDefault();e.stopPropagation();onPortOutDown(e,i);}}
                style={{width:11,height:11,borderRadius:'50%',border:'2px solid #8957e5',background:'#0d1117',cursor:'crosshair',flexShrink:0,marginRight:-14,transition:'transform .1s'}}
                onMouseEnter={e=>e.currentTarget.style.transform='scale(1.4)'}
                onMouseLeave={e=>e.currentTarget.style.transform='scale(1)'} />
            </div>
          ))}
          <div style={{display:'flex',alignItems:'center',gap:4,position:'relative',paddingTop:4,borderTop:'1px solid #21262d'}}>
            <span style={{flex:1,fontSize:10,color:'#484f58',fontStyle:'italic',paddingLeft:2}}>↪ Aucun match</span>
            <div onMouseDown={e=>{e.preventDefault();e.stopPropagation();onPortOutDown(e,_parseLabels(node.d.labels).length);}}
              style={{width:11,height:11,borderRadius:'50%',border:'2px solid #484f58',background:'#0d1117',cursor:'crosshair',flexShrink:0,marginRight:-14,transition:'transform .1s'}}
              onMouseEnter={e=>e.currentTarget.style.transform='scale(1.4)'}
              onMouseLeave={e=>e.currentTarget.style.transform='scale(1)'} />
          </div>
          <div style={{display:'flex',gap:4,marginTop:4}}>
            <input value={routerInput} onChange={e=>setRouterInput(e.target.value)}
              onKeyDown={e=>{if(e.key==='Enter'&&routerInput.trim()){const cur=_parseLabels(node.d.labels);if(!cur.includes(routerInput.trim())){onChange('labels',JSON.stringify([...cur,routerInput.trim()]));}setRouterInput('');}}}
              placeholder="Ajouter un élément…"
              style={{flex:1,padding:'3px 7px',background:'#0d1117',border:'1px solid #30363d',borderRadius:5,color:'#e6edf3',fontSize:10,outline:'none'}}
              onMouseDown={e=>e.stopPropagation()} />
            <button onMouseDown={e=>e.stopPropagation()} onClick={e=>{e.stopPropagation();const v=routerInput.trim();if(v){const cur=_parseLabels(node.d.labels);if(!cur.includes(v)){onChange('labels',JSON.stringify([...cur,v]));}setRouterInput('');}}}
              style={{padding:'3px 10px',background:'rgba(137,87,229,.25)',border:'1px solid rgba(137,87,229,.5)',borderRadius:5,color:'#bc8cff',fontSize:12,fontWeight:700,cursor:'pointer'}}>+</button>
          </div>
        </div>
      )}
      {node.type==='wda_zone_tap'&&(()=>{
        const zones=_parseZones(node.d.zones);
        return(
          <div style={{padding:'6px 8px 8px',borderTop:'1px solid #21262d',display:'flex',flexDirection:'column',gap:5}}>
            <div style={{fontSize:9,color:'#8b949e',fontWeight:700,textTransform:'uppercase',letterSpacing:'.05em'}}>Zones</div>
            {zones.map((z,i)=>(
              <div key={i} style={{background:'rgba(63,185,80,.07)',border:'1px solid rgba(63,185,80,.25)',borderRadius:6,padding:'5px 7px',display:'flex',alignItems:'center',gap:4}}>
                <span style={{flex:1,color:'#3fb950',fontWeight:700,fontSize:11,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{z.name||'Zone '+(i+1)}</span>
                <span style={{fontSize:9,color:'#484f58',flexShrink:0}}>{Math.round(z.x||50)}%,{Math.round(z.y||50)}%</span>
                <button onMouseDown={e=>e.stopPropagation()} onClick={e=>{e.stopPropagation();if(window._autoTapPreview)window._autoTapPreview({x:(z.x||50)/100,y:(z.y||50)/100});}}
                  style={{background:'none',border:'none',color:'#484f58',cursor:'pointer',fontSize:11,padding:'0 2px',lineHeight:1}} title="Montrer">👁</button>
                {onStartCapture&&<button onMouseDown={e=>e.stopPropagation()} onClick={e=>{e.stopPropagation();onStartCapture({nodeId:node.id,zoneIdx:i});}}
                  style={{background:'none',border:'none',color:'#58a6ff',cursor:'pointer',fontSize:11,padding:'0 2px',lineHeight:1}} title="Calibrer">🎯</button>}
                <button onMouseDown={e=>e.stopPropagation()} onClick={e=>{e.stopPropagation();const zs=[...zones];zs.splice(i,1);onChange('zones',JSON.stringify(zs));}}
                  style={{background:'none',border:'none',color:'#484f58',cursor:'pointer',fontSize:10,padding:'0 2px',lineHeight:1}}>✕</button>
              </div>
            ))}
            <div style={{display:'flex',gap:4}}>
              <input value={zoneInput} onChange={e=>setZoneInput(e.target.value)}
                onKeyDown={e=>{if(e.key==='Enter'&&zoneInput.trim()){const zs=_parseZones(node.d.zones);onChange('zones',JSON.stringify([...zs,{name:zoneInput.trim(),x:50,y:50}]));setZoneInput('');}}}
                placeholder="Nom de la zone (ex: Done)"
                style={{flex:1,padding:'3px 7px',background:'#0d1117',border:'1px solid #30363d',borderRadius:5,color:'#e6edf3',fontSize:10,outline:'none'}}
                onMouseDown={e=>e.stopPropagation()} />
              <button onMouseDown={e=>e.stopPropagation()} onClick={e=>{e.stopPropagation();if(zoneInput.trim()){const zs=_parseZones(node.d.zones);onChange('zones',JSON.stringify([...zs,{name:zoneInput.trim(),x:50,y:50}]));setZoneInput('');}}}
                style={{padding:'3px 8px',background:'rgba(63,185,80,.2)',border:'1px solid rgba(63,185,80,.4)',borderRadius:5,color:'#3fb950',fontSize:10,fontWeight:700,cursor:'pointer'}}>+</button>
            </div>
          </div>
        );
      })()}
      {/* ── Résultat runtime (numéro, code SMS, username…) ── */}
      {nodeResult&&(node.type==='farm_buy_number'||node.type==='farm_wait_sms'||node.type==='gen_account'||node.type==='regen_username'||node.type==='ai_2fa'||node.type==='farm_save_acct'||node.type==='farm_account_info')&&(
        <div style={{padding:'4px 8px 6px',borderTop:'1px solid #21262d',display:'flex',flexDirection:'column',gap:2}}>
          {nodeResult.phone&&<div style={{fontFamily:'monospace',fontSize:11,color:'#1dbab4',fontWeight:700}}>📞 {nodeResult.phone}</div>}
          {nodeResult.code&&<div style={{fontFamily:'monospace',fontSize:11,color:'#3fb950',fontWeight:700}}>💬 Code: {nodeResult.code}</div>}
          {nodeResult.name&&<div style={{fontFamily:'monospace',fontSize:11,color:'#e6edf3',fontWeight:700}}>✍️ {nodeResult.name}</div>}
          {nodeResult.birthday&&<div style={{fontFamily:'monospace',fontSize:11,color:'#8b949e',fontWeight:600}}>📅 {nodeResult.birthday}</div>}
          {nodeResult.username&&<div style={{fontFamily:'monospace',fontSize:11,color:'#3fb950',fontWeight:700}}>👤 {nodeResult.username}</div>}
          {nodeResult.password&&<div style={{fontFamily:'monospace',fontSize:11,color:'#8b949e',fontWeight:700}}>🔑 {nodeResult.password}</div>}
          {nodeResult['2fa_secret']&&<div style={{fontFamily:'monospace',fontSize:10,color:'#8957e5',fontWeight:700,wordBreak:'break-all'}}>🔐 {nodeResult['2fa_secret']}</div>}
          {nodeResult['2fa_secret']&&<TotpTimer secret={nodeResult['2fa_secret'].replace(/\s+/g,'')} />}
        </div>
      )}
    </div>
  );
}

// ─── AutoLivePhone — H.264 stream + tap/swipe interactif ──
function AutoLivePhone({udid,width,captureTap,onCaptureDone,cloud}){
  const {useEffect,useRef,useState}=React;
  const canvasRef=useRef(null);
  const videoRef=useRef(null);
  const swipeRef=useRef(null);
  const draggedRef=useRef(false);
  const [ripples,setRipples]=useState([]);
  const ripId=useRef(0);
  const [gesturePrev,setGesturePrev]=useState(null);
  const gesturePrevId=useRef(0);
  const [mode,setMode]=useState('h264');
  const [h264Up,setH264Up]=useState(false);
  const [connecting,setConnecting]=useState(false);
  const [reloadKey,setReloadKey]=useState(0); // refresh écran → relance la connexion du flux
  const gotFrame=useRef(false);   // a-t-on reçu au moins une image ?
  const retryRef=useRef(0);       // retries (cloud : le boot prend du temps)
  const [cloudReady,setCloudReady]=useState(!cloud);   // cloud : prêt seulement quand booté+connecté
  const [cloudMsg,setCloudMsg]=useState('');
  const [cloudErr,setCloudErr]=useState(false);
  const [cloudPow,setCloudPow]=useState(cloud?null:true);  // null=inconnu · true=allumé · false=éteint
  useEffect(()=>{retryRef.current=0;},[udid]);   // nouveau device -> compteur remis à zéro
  const HAS_WC=typeof VideoDecoder!=='undefined';

  // CLOUD : SUIT l'état on/off SANS jamais allumer le phone (c'est le bloc « Allumer » qui l'allume).
  useEffect(()=>{
    if(!cloud||!udid){setCloudPow(true);return;}
    let on=true;
    const check=()=>{ if(window.Backend.cloudphones) window.Backend.cloudphones().then(l=>{ if(!on)return; const me=(l||[]).find(c=>String(c.id)===String(udid)); setCloudPow(me?me.envStatus===4:false); }).catch(()=>{}); };
    check();const t=setInterval(check,4000);
    return()=>{on=false;clearInterval(t);};
  },[cloud,udid]);

  // CLOUD : KEEPALIVE — tant que ce panneau est ouvert, on marque l'activité (anti auto-off pendant l'automatisation).
  useEffect(()=>{
    if(!cloud||!udid||!window.Backend.cloudphoneKeepalive)return;
    const ka=()=>window.Backend.cloudphoneKeepalive(udid);
    ka();const t=setInterval(ka,20000);
    return()=>clearInterval(t);
  },[cloud,udid]);

  // CLOUD : timer "temps que le cloud est ouvert" (remis à 0 quand éteint)
  const [powSecs,setPowSecs]=useState(0);
  useEffect(()=>{
    if(!cloud){return;}
    if(cloudPow!==true){setPowSecs(0);return;}
    const id=setInterval(()=>setPowSecs(s=>s+1),1000);
    return()=>clearInterval(id);
  },[cloud,cloudPow]);

  // CLOUD : préchauffage — SEULEMENT si le phone est ALLUMÉ. On poll /connect (tunnel + boot) jusqu'à "prêt".
  useEffect(()=>{
    if(!cloud||!udid){setCloudReady(!cloud);return;}
    if(cloudPow!==true){setCloudReady(false);setCloudErr(false);return;}   // éteint/inconnu -> on ne connecte PAS
    let on=true,wasErr=false;setCloudReady(false);setCloudErr(false);setCloudMsg('Connexion…');
    const poll=async()=>{
      if(!on||!window.Backend.cloudphoneConnect)return;
      const r=await window.Backend.cloudphoneConnect(udid,wasErr);
      wasErr=false;
      if(!on)return;
      if(r&&r.state==='ready'){setCloudReady(true);setCloudErr(false);setCloudMsg('');}
      else if(r&&r.state==='error'){setCloudErr(true);setCloudMsg(r.msg||'Erreur de connexion');wasErr=true;}
      else if(r){setCloudErr(false);setCloudMsg(r.msg||'Connexion…');}
    };
    poll();const t=setInterval(poll,3000);
    return()=>{on=false;clearInterval(t);};
  },[udid,cloud,reloadKey,cloudPow]);

  const addRipple=(x,y)=>{
    const id=++ripId.current;
    setRipples(r=>[...r.slice(-6),{id,x,y}]);
    setTimeout(()=>setRipples(r=>r.filter(p=>p.id!==id)),520);
  };

  // H.264 WebSocket + WebCodecs
  useEffect(()=>{
    if(!udid||(cloud&&!cloudReady))return;   // cloud : on attend que le phone soit prêt (booté) avant de streamer
    let on=true,dec=null,ws=null,buf=new Uint8Array(0),sps=null,pps=null,configured=false,seenKey=false,tsc=0;
    gotFrame.current=false;
    setConnecting(true);setH264Up(false);setMode('h264');
    const annexb=(n)=>{const o=new Uint8Array(n.length+4);o[3]=1;o.set(n,4);return o;};
    const concat=(arr)=>{let t=0;arr.forEach(a=>t+=a.length);const o=new Uint8Array(t);let p=0;arr.forEach(a=>{o.set(a,p);p+=a.length;});return o;};
    const bail=()=>{
      if(!on)return;on=false;
      try{ws&&ws.close();}catch(e){}try{if(dec&&dec.state!=='closed')dec.close();}catch(e){}setConnecting(false);
      // CLOUD : le boot du phone (~30-40s) fait échouer la 1re connexion -> on retente jusqu'à ce que l'écran arrive.
      if(cloud&&!gotFrame.current&&retryRef.current<20){retryRef.current++;setTimeout(()=>{setReloadKey(k=>k+1);},3000);}
    };
    const draw=(frame)=>{
      try{
        const cv=canvasRef.current;
        if(cv){
          if(cv.width!==frame.displayWidth||cv.height!==frame.displayHeight){cv.width=frame.displayWidth;cv.height=frame.displayHeight;}
          cv.getContext('2d').drawImage(frame,0,0,cv.width,cv.height);
          gotFrame.current=true;
          if(!h264Up){setH264Up(true);setConnecting(false);}
        }
      }catch(e){}
      try{frame.close();}catch(e){}
    };
    const feedBytes=(value)=>{
      const nb=new Uint8Array(buf.length+value.length);nb.set(buf,0);nb.set(value,buf.length);buf=nb;
      let i=0;
      while(i+4<=buf.length){
        const len=((buf[i]<<24)>>>0)+(buf[i+1]<<16)+(buf[i+2]<<8)+buf[i+3];
        if(len<=0||i+4+len>buf.length)break;
        const nal=buf.subarray(i+4,i+4+len),nt=nal[0]&0x1f;
        if(nt===7){sps=nal.slice();if(!configured){try{if(!dec||dec.state==='closed')dec=new VideoDecoder({output:draw,error:()=>bail()});const codec='avc1.'+[sps[1],sps[2],sps[3]].map(b=>('0'+b.toString(16)).slice(-2)).join('');dec.configure({codec,optimizeForLatency:true});configured=true;}catch(e){bail();return;}}}
        else if(nt===8){pps=nal.slice();}
        else if(nt===5&&configured&&sps&&pps){try{dec.decode(new EncodedVideoChunk({type:'key',timestamp:tsc,data:concat([annexb(sps),annexb(pps),annexb(nal)])}));tsc+=33333;seenKey=true;}catch(e){bail();return;}}
        else if(nt===1&&seenKey&&configured&&dec.state==='configured'){try{dec.decode(new EncodedVideoChunk({type:'delta',timestamp:tsc,data:annexb(nal)}));tsc+=33333;}catch(e){bail();return;}}
        i+=4+len;
      }
      if(i>0)buf=buf.slice(i);
    };
    (async()=>{
      try{
        if(!window.Backend.devbase||!window.Backend.devbase())await window.Backend.resolveBackend();
        if(!on)return;
        const wsurl=window.Backend.devbase().replace(/^http/,'ws')+'/api/devices/'+encodeURIComponent(udid)+'/h264?ts='+Date.now();
        ws=new WebSocket(wsurl);ws.binaryType='arraybuffer';
        ws.onmessage=(ev)=>{if(!on)return;try{feedBytes(new Uint8Array(ev.data));}catch(e){}};
        ws.onerror=()=>{};
        ws.onclose=()=>{if(on)bail();};
      }catch(e){bail();}
    })();
    return()=>{on=false;try{ws&&ws.close();}catch(e){}try{if(dec&&dec.state!=='closed')dec.close();}catch(e){}};
  },[udid,reloadKey,cloudReady]);

  // expose le déclencheur de preview geste + tap
  React.useEffect(()=>{
    window._autoGesturePreview=(gp)=>{
      const id=++gesturePrevId.current;
      setGesturePrev({...gp,type:'drag',id,phase:'hold'});
      setTimeout(()=>setGesturePrev(prev=>prev&&prev.id===id?{...prev,phase:'drag'}:prev), gp.hold*1000);
      setTimeout(()=>setGesturePrev(prev=>prev&&prev.id===id?{...prev,phase:'fade'}:prev), gp.hold*1000+800);
      setTimeout(()=>setGesturePrev(prev=>prev&&prev.id===id?null:prev), gp.hold*1000+1300);
    };
    window._autoTapPreview=(tp)=>{
      const id=++gesturePrevId.current;
      setGesturePrev({...tp,type:'tap',id,phase:'show'});
      setTimeout(()=>setGesturePrev(prev=>prev&&prev.id===id?{...prev,phase:'fade'}:prev),1200);
      setTimeout(()=>setGesturePrev(prev=>prev&&prev.id===id?null:prev),1700);
    };
    window._autoDatePickerPreview=(dp)=>{
      const id=++gesturePrevId.current;
      setGesturePrev({...dp,type:'datepicker',id,phase:'show'});
      setTimeout(()=>setGesturePrev(prev=>prev&&prev.id===id?{...prev,phase:'fade'}:prev),2500);
      setTimeout(()=>setGesturePrev(prev=>prev&&prev.id===id?null:prev),3100);
    };
    return()=>{delete window._autoGesturePreview;delete window._autoTapPreview;delete window._autoDatePickerPreview;};
  },[]);

  // Tap
  const onTapScreen=(p)=>{
    if(draggedRef.current){draggedRef.current=false;return;}
    addRipple(p.x,p.y);
    if(captureTap&&onCaptureDone){
      // mode enregistrement : on capture les coords pour le bloc, pas de WDA
      onCaptureDone(+(p.x*100).toFixed(1),+(p.y*100).toFixed(1));
      return;
    }
    if(udid&&window.Backend&&window.Backend.action)window.Backend.action(udid,'click',{x:p.x,y:p.y,duration:0.1});
  };
  // Swipe
  const _cl=v=>Math.min(1,Math.max(0,v));
  const _now=()=>(typeof performance!=='undefined'&&performance.now)?performance.now():Date.now();
  const onDown=(e)=>{
    const r=e.currentTarget.getBoundingClientRect();
    swipeRef.current={r,t0:_now(),pts:[{x:+_cl((e.clientX-r.left)/r.width).toFixed(3),y:+_cl((e.clientY-r.top)/r.height).toFixed(3),t:0}]};
  };
  const onMove=(e)=>{
    const s=swipeRef.current;if(!s)return;const r=s.r;
    s.pts.push({x:+_cl((e.clientX-r.left)/r.width).toFixed(3),y:+_cl((e.clientY-r.top)/r.height).toFixed(3),t:Math.round(_now()-s.t0)});
  };
  const onUp=()=>{
    const s=swipeRef.current;swipeRef.current=null;
    if(!s)return;
    const a=s.pts[0],b=s.pts[s.pts.length-1],dist=Math.hypot(b.x-a.x,b.y-a.y);
    if(dist<0.06||s.pts.length<3)return;
    draggedRef.current=true;
    addRipple(b.x,b.y);
    if(udid&&window.Backend&&window.Backend.action)window.Backend.action(udid,'gesture',{points:s.pts});
  };

  if(!udid){
    return(
      <div style={{flex:1,display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',gap:10,color:'#484f58',padding:16,textAlign:'center'}}>
        <div style={{fontSize:28}}>📱</div>
        <div style={{fontSize:11,lineHeight:1.6}}>Sélectionne un iPhone dans la barre du haut pour voir l'écran en direct</div>
      </div>
    );
  }

  // Use the existing Phone component from device_view.jsx
  const phoneW=Math.max(100,width-16)+'px';
  return(
    <div style={{flex:1,display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'flex-start',padding:'6px 4px 4px',gap:6,overflowY:'auto'}}>
      {captureTap&&(
        <div style={{width:'100%',padding:'5px 8px',background:'rgba(56,139,253,.18)',border:'1px solid rgba(56,139,253,.5)',borderRadius:8,textAlign:'center',fontSize:10.5,fontWeight:700,color:'#58a6ff',letterSpacing:'.02em',animation:'captureGlow 1s ease-in-out infinite alternate'}}>
          <style>{"@keyframes captureGlow{from{box-shadow:0 0 4px rgba(56,139,253,.3)}to{box-shadow:0 0 12px rgba(56,139,253,.8)}}"}</style>
          🎯 Appuie sur l'écran pour enregistrer les coordonnées
        </div>
      )}
      {typeof Phone!=='undefined'
        ?(
          <div style={{position:'relative'}}>
            <Phone device={{udid,status:'online',battery:100,_cloud:cloud}} w={phoneW} big={false}
              mode={mode} videoRef={videoRef} canvasRef={canvasRef}
              liveSrc={null} hqSrc={null} lost={false} connecting={cloud&&!cloudReady?false:connecting}
              onDown={captureTap?undefined:onDown} onUp={captureTap?undefined:onUp} onMove={captureTap?undefined:onMove} onTap={onTapScreen}
              ripples={ripples} />
            {/* CLOUD : overlay reflétant l'état réel (éteint / connexion / erreur) — n'allume JAMAIS tout seul */}
            {cloud&&!cloudReady&&(
              <div style={{position:'absolute',inset:0,borderRadius:32,display:'grid',placeItems:'center',background:'rgba(11,15,23,.92)',padding:18,textAlign:'center',pointerEvents:(cloudErr||cloudPow===false)?'auto':'none'}}>
                <style>{"@keyframes plspin{to{transform:rotate(360deg)}}"}</style>
                {cloudPow===false?(
                  <div>
                    <div style={{fontSize:32,marginBottom:10,opacity:.7}}>⚫</div>
                    <div style={{fontSize:13,fontWeight:800,color:'#8b949e',marginBottom:4}}>Cloud phone éteint</div>
                    <div style={{fontSize:11,color:'#6e7681',lineHeight:1.5,maxWidth:220}}>Le flux l'allumera via le bloc « Allumer le cloud ». Ou allume-le manuellement :</div>
                    <button onClick={async()=>{try{await window.Backend.cloudphonePower(udid,true);}catch(e){}}} style={{marginTop:14,padding:'8px 18px',borderRadius:8,border:'none',background:'#238636',color:'#fff',fontWeight:700,fontSize:12.5,cursor:'pointer'}}>🔋 Allumer</button>
                  </div>
                ):cloudPow===null?(
                  <div style={{fontSize:12,color:'#6e7681'}}>Vérification de l'état…</div>
                ):(
                  <div>
                    <div style={{fontSize:30,marginBottom:10}}>{cloudErr?'⚠️':'☁️'}</div>
                    <div style={{fontSize:12,fontWeight:700,color:cloudErr?'#f85149':'#d29922',lineHeight:1.5,maxWidth:220}}>{cloudMsg||'Connexion…'}</div>
                    {!cloudErr&&<div style={{width:26,height:26,margin:'14px auto 0',borderRadius:'50%',border:'3px solid rgba(255,255,255,.12)',borderTopColor:'#3fb950',animation:'plspin .8s linear infinite'}} />}
                    {cloudErr&&<button onClick={()=>setReloadKey(k=>k+1)} style={{marginTop:14,padding:'7px 16px',borderRadius:8,border:'none',background:'#238636',color:'#fff',fontWeight:700,fontSize:12,cursor:'pointer'}}>↻ Réessayer</button>}
                  </div>
                )}
              </div>
            )}
            {captureTap&&(
              <div style={{position:'absolute',inset:0,borderRadius:32,border:'2.5px solid #388bfd',boxShadow:'0 0 0 3px rgba(56,139,253,.25)',pointerEvents:'none',zIndex:20}} />
            )}
            {gesturePrev&&(()=>{
              const gp=gesturePrev;
              const pw=parseFloat(phoneW)||320;
              const ph=pw*(844/390);
              const fading=gp.phase==='fade';
              if(gp.type==='datepicker'){
                const xM=gp.x_month*pw, xD=gp.x_day*pw, xY=gp.x_year*pw;
                const yA=gp.y_above*ph, yB=gp.y_below*ph;
                const cols=[{x:xM,label:'Mois',c:'#f78166'},{x:xD,label:'Jour',c:'#3fb950'},{x:xY,label:'Année',c:'#58a6ff'}];
                return(
                  <svg style={{position:'absolute',top:0,left:0,width:'100%',height:'100%',pointerEvents:'none',zIndex:30,borderRadius:32,overflow:'hidden'}}
                    viewBox={`0 0 ${pw} ${ph}`}>
                    {/* Lignes Y horizontales */}
                    <line x1={0} y1={yA} x2={pw} y2={yA} stroke="#e3b341" strokeWidth="1.5" strokeDasharray="5 4" opacity={fading?0:0.7} style={{transition:'opacity .5s'}} />
                    <line x1={0} y1={yB} x2={pw} y2={yB} stroke="#e3b341" strokeWidth="1.5" strokeDasharray="5 4" opacity={fading?0:0.7} style={{transition:'opacity .5s'}} />
                    <text x={4} y={yA-4} fill="#e3b341" fontSize="10" fontWeight="700" fontFamily="monospace" opacity={fading?0:1} style={{transition:'opacity .5s'}}>▲ au-dessus</text>
                    <text x={4} y={yB+12} fill="#e3b341" fontSize="10" fontWeight="700" fontFamily="monospace" opacity={fading?0:1} style={{transition:'opacity .5s'}}>▼ en-dessous</text>
                    {/* Zone centrale entre les deux Y */}
                    <rect x={0} y={yA} width={pw} height={yB-yA} fill="rgba(227,179,65,.07)" opacity={fading?0:1} style={{transition:'opacity .5s'}} />
                    {/* Lignes X verticales + points aux intersections */}
                    {cols.map(col=>(
                      <React.Fragment key={col.label}>
                        <line x1={col.x} y1={0} x2={col.x} y2={ph} stroke={col.c} strokeWidth="1.5" strokeDasharray="5 4" opacity={fading?0:0.65} style={{transition:'opacity .5s'}} />
                        {/* Point au-dessus */}
                        <circle cx={col.x} cy={yA} r="6" fill={col.c} opacity={fading?0:0.9} style={{transition:'opacity .5s'}} />
                        {/* Point en-dessous */}
                        <circle cx={col.x} cy={yB} r="6" fill={col.c} opacity={fading?0:0.9} style={{transition:'opacity .5s'}} />
                        <text x={col.x} y={yA-10} fill={col.c} fontSize="10" fontWeight="700" fontFamily="monospace" textAnchor="middle" opacity={fading?0:1} style={{transition:'opacity .5s'}}>{col.label}</text>
                      </React.Fragment>
                    ))}
                  </svg>
                );
              }
              if(gp.type==='tap'){
                const tx=gp.x*pw, ty=gp.y*ph;
                return(
                  <svg style={{position:'absolute',top:0,left:0,width:'100%',height:'100%',pointerEvents:'none',zIndex:30,borderRadius:32,overflow:'hidden'}}
                    viewBox={`0 0 ${pw} ${ph}`}>
                    <circle cx={tx} cy={ty} r="28" fill="rgba(63,185,80,.08)" stroke="#3fb950" strokeWidth="1.5"
                      opacity={fading?0:0.7} style={{transition:'opacity .45s'}} />
                    <circle cx={tx} cy={ty} r="14" fill="rgba(63,185,80,.2)" stroke="#3fb950" strokeWidth="2"
                      opacity={fading?0:1} style={{transition:'opacity .45s'}} />
                    <circle cx={tx} cy={ty} r="5" fill="#3fb950"
                      opacity={fading?0:1} style={{transition:'opacity .45s'}} />
                    <line x1={tx-22} y1={ty} x2={tx+22} y2={ty} stroke="#3fb950" strokeWidth="1" opacity={fading?0:0.5} style={{transition:'opacity .45s'}} />
                    <line x1={tx} y1={ty-22} x2={tx} y2={ty+22} stroke="#3fb950" strokeWidth="1" opacity={fading?0:0.5} style={{transition:'opacity .45s'}} />
                    <text x={tx+18} y={ty-12} fill="#3fb950" fontSize="10" fontWeight="700" fontFamily="monospace"
                      opacity={fading?0:1} style={{transition:'opacity .45s'}}>
                      {Math.round(gp.x*100)}%,{Math.round(gp.y*100)}%
                    </text>
                  </svg>
                );
              }
              const ax=gp.x1*pw, ay=gp.y1*ph, bx=gp.x2*pw, by=gp.y2*ph;
              return(
                <svg style={{position:'absolute',top:0,left:0,width:'100%',height:'100%',pointerEvents:'none',zIndex:30,borderRadius:32,overflow:'hidden'}}
                  viewBox={`0 0 ${pw} ${ph}`}>
                  {(gp.phase==='drag'||fading)&&(
                    <line x1={ax} y1={ay} x2={bx} y2={by}
                      stroke="#388bfd" strokeWidth="2.5" strokeDasharray="6 4"
                      opacity={fading?0:0.8}
                      style={{transition:'opacity .4s'}} />
                  )}
                  {(gp.phase==='drag'||fading)&&(
                    <circle cx={bx} cy={by} r="8" fill="rgba(56,139,253,.85)" opacity={fading?0:1} style={{transition:'opacity .4s'}} />
                  )}
                  <circle cx={ax} cy={ay} r="12" fill="rgba(248,81,73,.15)" stroke="#f85149" strokeWidth="2"
                    opacity={fading?0:1} style={{transition:'opacity .4s'}} />
                  <circle cx={ax} cy={ay} r="6" fill="#f85149"
                    opacity={fading?0:1} style={{transition:'opacity .4s'}} />
                  {gp.phase==='hold'&&(
                    <text x={ax+14} y={ay-10} fill="#f85149" fontSize="11" fontWeight="700" fontFamily="monospace">
                      hold {gp.hold}s…
                    </text>
                  )}
                  {gp.phase==='drag'&&(
                    <text x={(ax+bx)/2} y={(ay+by)/2-8} fill="#388bfd" fontSize="11" fontWeight="700" fontFamily="monospace"
                      textAnchor="middle">→ glisse</text>
                  )}
                </svg>
              );
            })()}
          </div>
        ):(
          // fallback si Phone pas encore disponible
          <div style={{width:phoneW,aspectRatio:cloud?'1080/2340':'390/844',borderRadius:cloud?16:32,background:'#0d1117',border:'2px solid '+(cloud?'#20242e':'#30363d'),overflow:'hidden',position:'relative'}}>
            <canvas ref={canvasRef} style={{position:'absolute',inset:0,width:'100%',height:'100%',objectFit:cloud?'contain':'cover'}} />
            {connecting&&<div style={{position:'absolute',inset:0,display:'grid',placeItems:'center',color:'#8b949e',fontSize:11}}>Connexion…</div>}
            {ripples.map(p=>(
              <span key={p.id} style={{position:'absolute',left:(p.x*100)+'%',top:(p.y*100)+'%',width:40,height:40,marginLeft:-20,marginTop:-20,borderRadius:'50%',border:'2px solid #00e08a',pointerEvents:'none',animation:'pl-ripple .48s ease-out forwards'}} />
            ))}
          </div>
        )
      }
      {/* Timer "temps que le cloud est ouvert" */}
      {cloud&&cloudPow&&(
        <div style={{textAlign:'center',marginBottom:6}}>
          <span title="Temps depuis l'allumage du cloud phone · coût estimé ($0,006/min)"
            style={{display:'inline-flex',alignItems:'center',gap:6,padding:'4px 12px',borderRadius:99,background:'#161b22',border:'1px solid #30363d',fontFamily:'monospace'}}>
            <span style={{fontSize:12}}>⏱</span>
            <span style={{fontSize:13,fontWeight:800,color:'#e6edf3'}}>{String(Math.floor(powSecs/60)).padStart(2,'0')}:{String(powSecs%60).padStart(2,'0')}</span>
            <span style={{fontSize:10,color:'#8b949e'}}>· ${(powSecs/60*0.006).toFixed(3)}</span>
          </span>
        </div>
      )}
      <div style={{display:'flex',gap:8,flexWrap:'wrap',justifyContent:'center'}}>
        {cloud?(<>
          {/* Boutons ANDROID (cloud phone) */}
          <button onClick={()=>setReloadKey(k=>k+1)} title="Rafraîchir / reconnecter le flux vidéo"
            style={{padding:'5px 12px',background:'rgba(56,139,253,.15)',border:'1px solid rgba(56,139,253,.4)',borderRadius:8,color:'#58a6ff',fontSize:11,cursor:'pointer',fontWeight:700}}>🔄 Refresh</button>
          <button onClick={()=>udid&&window.Backend&&window.Backend.action&&window.Backend.action(udid,'back',{})} title="Retour"
            style={{padding:'5px 14px',background:'#21262d',border:'1px solid #30363d',borderRadius:8,color:'#e6edf3',fontSize:14,cursor:'pointer'}}>◀</button>
          <button onClick={()=>udid&&window.Backend&&window.Backend.action&&window.Backend.action(udid,'home',{})} title="Accueil"
            style={{padding:'5px 14px',background:'#21262d',border:'1px solid #30363d',borderRadius:8,color:'#e6edf3',fontSize:12,cursor:'pointer'}}>⬤</button>
          <button onClick={()=>udid&&window.Backend&&window.Backend.action&&window.Backend.action(udid,'recents',{})} title="App Switcher (aperçu des apps)"
            style={{padding:'5px 14px',background:'#21262d',border:'1px solid #30363d',borderRadius:8,color:'#e6edf3',fontSize:12,cursor:'pointer'}}>■</button>
        </>):(<>
          {/* Boutons iPhone physique */}
          <button onClick={()=>udid&&window.Backend&&window.Backend.action&&window.Backend.action(udid,'wake',{})}
            title="Allumer l'écran de l'iPhone (appui Home)"
            style={{padding:'5px 12px',background:'rgba(210,153,34,.15)',border:'1px solid rgba(210,153,34,.4)',borderRadius:8,color:'#f0b429',fontSize:11,cursor:'pointer',fontWeight:700}}>🔆 Écran</button>
          <button onClick={()=>setReloadKey(k=>k+1)}
            title="Rafraîchir / reconnecter le flux vidéo de l'iPhone"
            style={{padding:'5px 12px',background:'rgba(56,139,253,.15)',border:'1px solid rgba(56,139,253,.4)',borderRadius:8,color:'#58a6ff',fontSize:11,cursor:'pointer',fontWeight:700}}>🔄 Refresh</button>
          <button onClick={()=>udid&&window.Backend&&window.Backend.action&&window.Backend.action(udid,'home',{})}
            style={{padding:'5px 12px',background:'#21262d',border:'1px solid #30363d',borderRadius:8,color:'#e6edf3',fontSize:11,cursor:'pointer'}}>⬜ Home</button>
          <button onClick={()=>udid&&window.Backend&&window.Backend.action&&window.Backend.action(udid,'back',{})}
            style={{padding:'5px 12px',background:'#21262d',border:'1px solid #30363d',borderRadius:8,color:'#e6edf3',fontSize:11,cursor:'pointer'}}>← Back</button>
        </>)}
      </div>
      <div style={{fontSize:9.5,color:h264Up?'#3fb950':(cloud&&!cloudReady)?'#d29922':connecting?'#d29922':'#484f58',fontFamily:'monospace',textAlign:'center'}}>
        {h264Up?'● H.264 live':(cloud&&!cloudReady)?('○ '+(cloudMsg||'démarrage du cloud…')):connecting?'○ connexion…':'○ hors-ligne'}
      </div>
    </div>
  );
}

// ─── AutoRightPanel ───────────────────────────────────────
function AutoRightPanel({badge,logs,running,activeType,width,udid,captureTap,onCaptureDone,onClearLogs}){
  const [phoneKey,setPhoneKey]=React.useState(0);
  // Log repliable + hauteur responsive (prend moins de place quand la fenêtre rétrécit)
  const [logCollapsed,setLogCollapsed]=React.useState(()=>{try{return localStorage.getItem('pl.auto.logcollapsed')==='1';}catch(e){return false;}});
  const toggleLog=()=>setLogCollapsed(v=>{const n=!v;try{localStorage.setItem('pl.auto.logcollapsed',n?'1':'0');}catch(e){}return n;});

  function fmtMs(ms){const s=Math.round(ms/1000);return s<60?s+'s':Math.floor(s/60)+'m'+(s%60?''+(s%60)+'s':'');}
  function fmtTotal(prog){
    if(!prog||!prog.start)return '';
    return fmtMs(Date.now()-prog.start);
  }

  return(
    <div style={{width,background:'#161b22',borderLeft:'1px solid #21262d',display:'flex',flexDirection:'column',flex:'none',overflowY:'hidden'}}>
      {/* Header */}
      <div style={{padding:'7px 10px 5px',fontSize:9,color:captureTap?'#58a6ff':'#8b949e',fontWeight:700,textTransform:'uppercase',letterSpacing:'.06em',borderBottom:'1px solid #21262d',flex:'none',transition:'color .2s',display:'flex',alignItems:'center',justifyContent:'space-between'}}>
        <span>{captureTap?'🎯 Enregistrement tap…':(/^\d{13,}$/.test(String(udid||''))?'☁️ Cloud phone Live':'📱 iPhone Live')}</span>
        <button onClick={()=>setPhoneKey(k=>k+1)} title="Rafraîchir le flux"
          style={{background:'none',border:'none',color:'#484f58',cursor:'pointer',fontSize:12,padding:'0 2px',lineHeight:1}}
          onMouseEnter={e=>{e.currentTarget.style.color='#58a6ff';}}
          onMouseLeave={e=>{e.currentTarget.style.color='#484f58';}}>↻</button>
      </div>
      {/* Badge statut */}
      <div style={{borderBottom:'1px solid #21262d',padding:'4px 8px 3px',flex:'none'}}>
        <div style={{fontSize:10,fontWeight:700,padding:'3px 8px',borderRadius:6,background:'rgba(35,134,54,.15)',color:'#3fb950',border:'1px solid rgba(35,134,54,.3)',textAlign:'center',wordBreak:'break-word',lineHeight:1.4}}>
          {badge}
        </div>
        {/* Cloud phone : bouton Allumer (remplace le bouton "écran" iPhone) */}
        {/^\d{13,}$/.test(String(udid||''))&&(
          <button onClick={async()=>{try{await window.Backend.cloudphonePower(udid,true);}catch(e){} setTimeout(()=>setPhoneKey(k=>k+1),1500);}}
            style={{marginTop:5,width:'100%',padding:'6px 0',borderRadius:7,border:'1px solid rgba(52,211,153,.4)',background:'rgba(52,211,153,.12)',color:'#34d399',fontWeight:700,fontSize:11.5,cursor:'pointer'}}>
            🔋 Allumer le cloud phone
          </button>
        )}
      </div>
      {/* Écran — prend toute la hauteur disponible, scroll si besoin */}
      <div style={{flex:1,overflowY:'auto',display:'flex',flexDirection:'column'}}>
        <AutoLivePhone key={phoneKey} udid={udid} width={width} captureTap={captureTap} onCaptureDone={onCaptureDone} cloud={/^\d{13,}$/.test(String(udid||''))} />
      </div>

      {/* ── Log (repliable + hauteur responsive) ── */}
      <div style={{flex:'none',borderTop:'1px solid #21262d',display:'flex',flexDirection:'column',height:logCollapsed?26:'clamp(78px, 20vh, 190px)'}}>
        <div onClick={toggleLog} title={logCollapsed?'Déplier le log':'Replier le log'}
          style={{display:'flex',alignItems:'center',justifyContent:'space-between',padding:'4px 8px 2px',flex:'none',cursor:'pointer',userSelect:'none'}}>
          <div style={{fontSize:9,color:'#8b949e',fontWeight:700,textTransform:'uppercase',letterSpacing:'.06em',display:'flex',alignItems:'center',gap:5}}>
            <span style={{fontSize:8,color:'#484f58'}}>{logCollapsed?'▸':'▾'}</span>📋 Log{logCollapsed&&logs.length?' · '+logs.length:''}
          </div>
          <button onClick={e=>{e.stopPropagation();onClearLogs&&onClearLogs();}} style={{fontSize:9,color:'#484f58',background:'none',border:'none',cursor:'pointer',padding:'1px 4px'}}>Effacer</button>
        </div>
        {!logCollapsed&&
        <div id="auto-log-scroll" style={{flex:1,overflowY:'auto',padding:'0 8px 6px',display:'flex',flexDirection:'column',gap:0}}>
          {logs.length===0
            ?<div style={{fontSize:10,color:'#8b949e',fontFamily:'monospace',marginTop:4}}>— vide —</div>
            :logs.map(l=>(
              l.separator
                ?<div key={l.id} style={{borderTop:'1px dashed #30363d',margin:'4px 0',fontSize:8,color:'#484f58',textAlign:'center'}}>{l.msg}</div>
                :<div key={l.id} style={{fontSize:9.5,fontFamily:'monospace',color:l.color,padding:'1px 0',lineHeight:1.5,wordBreak:'break-all'}}>{l.msg}</div>
            ))
          }
          <div id="auto-log-bottom" />
        </div>}
      </div>
    </div>
  );
}

// ─── AutomationEditor ─────────────────────────────────────
function AutomationEditor({flow:init,onBack}){
  const {useState,useEffect,useRef,useMemo}=React;

  const [flowName,setFlowName]=useState(init.name||'Nouveau flux');
  const [nodes,setNodes]=useState(()=>(init.nodes||[]).map(n=>({...n})));
  const [edges,setEdges]=useState(()=>[...(init.edges||[])]);
  const [activeNodeId,setActiveNodeId]=useState(null);
  const [activeNodeType,setActiveNodeType]=useState(null);
  const [focusActive,setFocusActive]=useState(false);
  const focusActiveRef=useRef(false);
  const [running,setRunning]=useState(false);
  const [logs,setLogs]=useState([]);
  const [badge,setBadge]=useState('En attente…');
  const [showJson,setShowJson]=useState(false);
  const [sbWidth,setSbWidth]=useState(()=>{try{const v=parseInt(localStorage.getItem('pl.auto.sbw'));return(v>=120&&v<=500)?v:190;}catch(e){return 190;}});
  const [sbCollapsed,setSbCollapsed]=useState(()=>{try{return localStorage.getItem('pl.auto.sbcollapsed')==='1';}catch(e){return false;}});
  const toggleSb=()=>setSbCollapsed(v=>{const n=!v;try{localStorage.setItem('pl.auto.sbcollapsed',n?'1':'0');}catch(e){}return n;});
  const [blockGroups,setBlockGroups]=useState(loadBlockGroups); // macros réutilisables (sidebar)
  const [groupNaming,setGroupNaming]=useState(null);            // {ids} en attente de nom (popup)
  const [groupNameInput,setGroupNameInput]=useState('');
  const [rpWidth,setRpWidth]=useState(()=>{try{const v=parseInt(localStorage.getItem('pl.auto.rpw'));return(v>=160&&v<=700)?v:280;}catch(e){return 280;}});
  const [ctxMenu,setCtxMenu]=useState(null);
  const [savedMsg,setSavedMsg]=useState('');
  const [deviceId,setDeviceId]=useState(()=>{try{return localStorage.getItem('pl.auto.dev')||'';}catch(e){return '';}});
  const [devices,setDevices]=useState([]);
  const devicesRef=useRef([]);
  useEffect(()=>{devicesRef.current=devices;},[devices]);
  // Liste des proxies (pour afficher le badge du proxy de l'iPhone sélectionné)
  const [proxies,setProxies]=useState([]);
  const proxiesRef=useRef([]);
  useEffect(()=>{proxiesRef.current=proxies;},[proxies]);
  useEffect(()=>{
    if(window.Backend&&window.Backend.proxies)window.Backend.proxies().then(p=>{if(p)setProxies(p);}).catch(()=>{});
  },[]);
  const [captureTap,setCaptureTap]=useState(null);
  const [zoomPct,setZoomPct]=useState(100);

  const [canUndo,setCanUndo]=useState(false);
  const [canRedo,setCanRedo]=useState(false);
  const [selectedIds,setSelectedIds]=useState(new Set());
  const [selRectState,setSelRectState]=useState(null);
  const [nodeResults,setNodeResults]=useState(()=>{try{return JSON.parse(localStorage.getItem('plab_noderesults'))||{};}catch(e){return {};}});
  const nodeResultsRef=useRef(nodeResults);
  const [drawMode,setDrawMode]=useState(false);
  const drawModeRef=useRef(false);
  const [drawColor,setDrawColor]=useState('#f78166');
  const [drawSize,setDrawSize]=useState(3);
  const [drawEraser,setDrawEraser]=useState(false);

  const panRef=useRef({x:60,y:60});
  const scaleRef=useRef(1);
  const dragRef=useRef(null);
  const nodesRef=useRef(nodes);
  const edgesRef=useRef(edges);
  const historyRef=useRef([{nodes:nodes.map(n=>({...n,d:{...n.d}})),edges:[...edges]}]);
  const historyIdxRef=useRef(0);
  const histDebRef=useRef(null);
  // Drapeaux de contrôle PARTAGÉS (singleton window) → survivent au démontage/remontage
  // de la page. Sinon la boucle orpheline tourne sur d'anciens refs et devient instoppable.
  const _R=window.__plAutoRefs||(window.__plAutoRefs={running:{current:false},paused:{current:false},batch:{current:false},batchPaused:{current:false}});
  const runningRef=_R.running;
  const [paused,setPaused]=useState(false);
  const pausedRef=_R.paused;
  const _waitIfPaused=()=>new Promise(res=>{const t=setInterval(()=>{if(!pausedRef.current||!runningRef.current){clearInterval(t);res();}},300);});
  const loopCountersRef=useRef({});
  const lastRunOkRef=useRef(null);
  const [batchRunning,setBatchRunning]=useState(false);
  const batchRunningRef=_R.batch;
  const [batchPaused,setBatchPaused]=useState(false);
  const batchPausedRef=_R.batchPaused;
  // Au montage : si un run tourne encore (on a quitté puis on revient), on rebranche l'UI
  // dessus pour réafficher le bouton Stop et pouvoir vraiment l'arrêter.
  useEffect(()=>{
    if(_R.batch.current)setBatchRunning(true);
    if(_R.batchPaused.current)setBatchPaused(true);
    if(_R.running.current)setRunning(true);
    if(_R.paused.current)setPaused(true);
    if(_R.batch.current||_R.running.current)setBadge('En cours…');
  },[]);
  // ── Verrou proxy inter-fenêtres ──
  const [proxyBlock,setProxyBlock]=useState(null); // popup si proxy déjà pris ailleurs
  const [lockTick,setLockTick]=useState(0);         // force le re-render sur changement de verrous
  const [proxyInfo,setProxyInfo]=useState(null);    // popup infos du proxy (clic sur le badge)
  const [proxyCopied,setProxyCopied]=useState('');
  const [showProxyPass,setShowProxyPass]=useState(false);
  const _copyProxy=(val,key)=>{try{navigator.clipboard.writeText(String(val));}catch(e){}setProxyCopied(key);setTimeout(()=>setProxyCopied(''),1200);};
  function _proxyFor(udid){const d=(devicesRef.current||[]).find(x=>x.udid===udid);return d?d.proxy:null;}
  function _labelFor(udid){const d=(devicesRef.current||[]).find(x=>x.udid===udid);return (d&&(d.tag||d.name))||String(udid||'').slice(0,8);}
  // Réserve le proxy du device courant avant un run. false + popup si pris ailleurs.
  function _acquireProxy(){
    const p=_proxyFor(deviceId);
    if(!p)return true;                    // device sans proxy → rien à verrouiller
    const other=proxyLockedByOther(p);
    if(other){
      setProxyBlock({proxy:p,tried:_labelFor(deviceId),taken:other.label||(other.udid?other.udid.slice(0,8):'une autre fenêtre')});
      return false;
    }
    reserveProxy(p,deviceId,_labelFor(deviceId));
    return true;
  }
  // Heartbeat : garde le verrou vivant tant qu'un run tourne (single ou batch)
  useEffect(()=>{
    if(!(running||batchRunning))return;
    const p=_proxyFor(deviceId);if(!p)return;
    refreshProxy(p);
    const t=setInterval(()=>refreshProxy(p),5000);
    return ()=>clearInterval(t);
  },[running,batchRunning,deviceId]);
  // Réserve l'iPhone sélectionné pour CETTE fenêtre (verrou inter-fenêtres). Si un autre
  // onglet le pilote déjà, on désélectionne → jamais deux fenêtres sur le même iPhone.
  useEffect(()=>{
    if(!deviceId)return;
    // Device hérité (localStorage partagé) ou race : si déjà pris ailleurs, on désélectionne
    // en silence (la pop-up n'apparaît que lors d'une sélection active via le menu).
    if(deviceLockedByOther(deviceId)){
      setDeviceId('');try{localStorage.removeItem('pl.auto.dev');}catch(e){}
      return;
    }
    reserveDevice(deviceId);
    const t=setInterval(()=>refreshDevice(deviceId),5000);
    return ()=>{clearInterval(t);releaseDevice(deviceId);};
  },[deviceId]);
  // Réagit aux verrous posés/retirés par d'autres fenêtres (+ purge périodique des verrous morts)
  useEffect(()=>{
    const h=e=>{if(!e||e.key===PROXY_LOCK_KEY||e.key===DEVICE_LOCK_KEY)setLockTick(t=>t+1);};
    window.addEventListener('storage',h);
    const t=setInterval(()=>setLockTick(x=>x+1),4000);
    return ()=>{window.removeEventListener('storage',h);clearInterval(t);};
  },[]);
  // Libère les verrous de cette fenêtre au démontage du moteur
  useEffect(()=>()=>{releaseAllProxies();releaseAllDevices();},[]);
  const [batchCount,setBatchCount]=useState(5);
  const [batchCooldown,setBatchCooldown]=useState(20);
  const [batchProgress,setBatchProgress]=useState(null);
  const [campRun,setCampRun]=useState(null); // auto-run déclenché par une Campagne
  const [soundEnabled,setSoundEnabled]=useState(true);
  const soundEnabledRef=useRef(true);
  const runTimerRef=useRef(null);
  const sbTypeRef=useRef(null);
  const autoSaveT=useRef(null);
  const resizeRef=useRef(null);
  const canvasRef=useRef();
  const innerRef=useRef();
  const svgRef=useRef();
  const drawCvRef=useRef();
  const drawingRef=useRef(false);
  const drawLastRef=useRef(null);
  const drawColorRef=useRef('#f78166');
  const drawSizeRef=useRef(3);
  const drawEraserRef=useRef(false);
  const doUndoRef=useRef(null);
  const doRedoRef=useRef(null);
  const selRectRef=useRef(null);

  useEffect(()=>{nodesRef.current=nodes;},[nodes]);
  // Au chargement, restaure les nodeResults des blocs gen_account depuis localStorage
  // migration one-shot : copie les anciennes clés plab_acct_/plab_num_ dans plab_noderesults
  useEffect(()=>{
    let changed=false;
    nodes.filter(n=>n.type==='gen_account'||n.type==='farm_buy_number').forEach(n=>{
      const key='plab_'+(n.type==='gen_account'?'acct':'num')+'_'+n.id;
      try{const s=localStorage.getItem(key);if(s&&!nodeResultsRef.current[n.id]){nodeResultsRef.current={...nodeResultsRef.current,[n.id]:JSON.parse(s)};changed=true;}}catch(_){}
    });
    if(changed){setNodeResults({...nodeResultsRef.current});try{localStorage.setItem('plab_noderesults',JSON.stringify(nodeResultsRef.current));}catch(_){}}
  },[]);
  useEffect(()=>{drawColorRef.current=drawColor;},[drawColor]);
  useEffect(()=>{drawSizeRef.current=drawSize;},[drawSize]);
  useEffect(()=>{drawEraserRef.current=drawEraser;},[drawEraser]);
  useEffect(()=>{edgesRef.current=edges;},[edges]);

  // ── auto-save (debounced 1.5s) ─────────────────────────
  useEffect(()=>{
    if(autoSaveT.current)clearTimeout(autoSaveT.current);
    autoSaveT.current=setTimeout(()=>{
      _persist(nodesRef.current,edgesRef.current,flowName);
      setSavedMsg('✓ Auto-sauvegardé');
      setTimeout(()=>setSavedMsg(''),2500);
    },1500);
    return()=>clearTimeout(autoSaveT.current);
  },[nodes,edges,flowName]);

  function _persist(ns,es,name){
    const flows=autoLoadFlows();
    const idx=flows.findIndex(f=>f.id===init.id);
    const saved={...init,name,nodes:ns,edges:es,updatedAt:new Date().toISOString()};
    if(idx>=0)flows[idx]=saved;else flows.push(saved);
    autoSaveFlows(flows);
  }

  // ── transform ──────────────────────────────────────────
  function applyT(){
    if(innerRef.current)innerRef.current.style.transform='translate('+panRef.current.x+'px,'+panRef.current.y+'px) scale('+scaleRef.current+')';
  }

  // ── SVG ────────────────────────────────────────────────
  function epath(x1,y1,x2,y2){const dx=Math.max(60,Math.abs(x2-x1)*0.5);return'M'+x1+','+y1+' C'+(x1+dx)+','+y1+' '+(x2-dx)+','+y2+' '+x2+','+y2;}
  function updateSVG(ons){
    const svg=svgRef.current;if(!svg)return;
    const ns=ons||nodesRef.current,es=edgesRef.current;
    svg.innerHTML='';
    const dr=dragRef.current;
    if(dr&&dr.type==='conn'&&dr.cur){
      const fn=ns.find(n=>n.id===dr.fromId);
      const _ft=fn&&AUTO_BLOCKS[fn.type];const _isLoop=_ft&&_ft.loopOut;const dragCol=_isLoop?(dr.fromPort===0?'#bc8cff':'#3fb950'):(dr.fromPort===0?'#3fb950':dr.fromPort===1?'#f85149':'#58a6ff');
      if(fn){const sx=dr.fromX!=null?dr.fromX:fn.x+182;const sy=dr.fromY!=null?dr.fromY:fn.y+22;const p=document.createElementNS('http://www.w3.org/2000/svg','path');p.setAttribute('d',epath(sx,sy,dr.cur.x,dr.cur.y));p.setAttribute('stroke',dragCol);p.setAttribute('stroke-width','2');p.setAttribute('fill','none');p.setAttribute('stroke-dasharray','5,4');svg.appendChild(p);}
    }
    es.forEach(function(e,idx){
      const fn=ns.find(n=>n.id===e.f),tn=ns.find(n=>n.id===e.t);if(!fn||!tn)return;
      const _isLoopBlk=AUTO_BLOCKS[fn.type]&&AUTO_BLOCKS[fn.type].loopOut;
      const col=_isLoopBlk?(e.port===0?'#bc8cff':e.port===1?'#3fb950':(AUTO_BLOCKS[fn.type].c)):(e.port===0?'#3fb950':e.port===1?'#f85149':(AUTO_BLOCKS[fn.type]?AUTO_BLOCKS[fn.type].c:'#8b949e'));
      const nw=AUTO_BLOCKS[fn.type]?.w||182;const ex=fn.x+nw;const ey=fn.y+22+(e.fdy||0);const d=epath(ex,ey,tn.x,tn.y+22);
      const g=document.createElementNS('http://www.w3.org/2000/svg','g');
      g.style.cursor='context-menu';g.setAttribute('data-ei',String(idx));
      const hit=document.createElementNS('http://www.w3.org/2000/svg','path');hit.setAttribute('d',d);hit.setAttribute('stroke','transparent');hit.setAttribute('stroke-width','14');hit.setAttribute('fill','none');
      const vis=document.createElementNS('http://www.w3.org/2000/svg','path');vis.setAttribute('d',d);vis.setAttribute('stroke',col);vis.setAttribute('stroke-width','2');vis.setAttribute('fill','none');vis.setAttribute('opacity','0.85');
      // pointer-events:auto sur le groupe pour capter les events malgré pointer-events:none sur le SVG parent
      g.style.pointerEvents='auto';
      const capturedIdx=idx;
      g.addEventListener('contextmenu',function(ev){
        ev.preventDefault();ev.stopPropagation();
        setCtxMenu({x:ev.clientX,y:ev.clientY,edgeIdx:capturedIdx});
      });
      g.appendChild(hit);g.appendChild(vis);svg.appendChild(g);
    });
  }

  // ── global mouse/resize ────────────────────────────────
  useEffect(()=>{
    function onMM(e){
      const dr=dragRef.current;
      if(dr){
        if(dr.type==='node'){
          const s=scaleRef.current;const dx=(e.clientX-dr.startX)/s;const dy=(e.clientY-dr.startY)/s;
          if(dr.multi){
            dr.multi.forEach(({id,origX,origY})=>{const el=document.getElementById('anode_'+id);if(el){el.style.left=Math.max(0,origX+dx)+'px';el.style.top=Math.max(0,origY+dy)+'px';}});
            updateSVG(nodesRef.current.map(n=>{const m=dr.multi.find(x=>x.id===n.id);return m?{...n,x:Math.max(0,m.origX+dx),y:Math.max(0,m.origY+dy)}:n;}));
          } else {
            const nx=Math.max(0,dr.origX+dx);const ny=Math.max(0,dr.origY+dy);
            const el=document.getElementById('anode_'+dr.id);if(el){el.style.left=nx+'px';el.style.top=ny+'px';}
            updateSVG(nodesRef.current.map(n=>n.id===dr.id?{...n,x:nx,y:ny}:n));
          }
        }
        if(dr.type==='sel'){
          const rect=canvasRef.current&&canvasRef.current.getBoundingClientRect();
          if(rect){const s=scaleRef.current;const x=(e.clientX-rect.left-panRef.current.x)/s;const y=(e.clientY-rect.top-panRef.current.y)/s;const sr={x1:Math.min(dr.x0,x),y1:Math.min(dr.y0,y),x2:Math.max(dr.x0,x),y2:Math.max(dr.y0,y)};selRectRef.current=sr;setSelRectState({...sr});}
        }
        if(dr.type==='pan'){panRef.current={x:e.clientX-dr.startX,y:e.clientY-dr.startY};applyT();}
        if(dr.type==='conn'){
          const rect=canvasRef.current&&canvasRef.current.getBoundingClientRect();
          if(rect){const s=scaleRef.current;dr.cur={x:(e.clientX-rect.left-panRef.current.x)/s,y:(e.clientY-rect.top-panRef.current.y)/s};updateSVG();}
        }
      }
      const rz=resizeRef.current;
      if(rz){
        if(rz.side==='sb'){const w=Math.max(120,Math.min(500,rz.initW+(e.clientX-rz.startX)));setSbWidth(w);try{localStorage.setItem('pl.auto.sbw',String(w));}catch(e){}}
        if(rz.side==='rp'){const w=Math.max(160,Math.min(700,rz.initW-(e.clientX-rz.startX)));setRpWidth(w);try{localStorage.setItem('pl.auto.rpw',String(w));}catch(e){}}
      }
    }
    function onMU(e){
      const dr=dragRef.current;
      if(dr){
        if(dr.type==='node'){
          const s=scaleRef.current;const dx=(e.clientX-dr.startX)/s;const dy=(e.clientY-dr.startY)/s;
          if(dr.multi){
            nodesRef.current=nodesRef.current.map(n=>{const m=dr.multi.find(x=>x.id===n.id);return m?{...n,x:Math.max(0,m.origX+dx),y:Math.max(0,m.origY+dy)}:n;});
          } else {
            nodesRef.current=nodesRef.current.map(n=>n.id===dr.id?{...n,x:Math.max(0,dr.origX+dx),y:Math.max(0,dr.origY+dy)}:n);
          }
          setNodes([...nodesRef.current]);pushHistory(nodesRef.current,edgesRef.current);
        }
        if(dr.type==='sel'){
          const sr=selRectRef.current;
          if(sr){const sel=new Set();nodesRef.current.forEach(n=>{const nw=AUTO_BLOCKS[n.type]?.w||182;if(n.x+nw>=sr.x1&&n.x<=sr.x2&&n.y+80>=sr.y1&&n.y<=sr.y2)sel.add(n.id);});setSelectedIds(sel);}
          selRectRef.current=null;setSelRectState(null);
        }
        if(dr.type==='conn'){
          const portEl=e.target&&e.target.closest&&e.target.closest('[data-port-in]');
          if(portEl){
            const toId=portEl.getAttribute('data-port-in');
            if(toId&&toId!==dr.fromId&&!edgesRef.current.find(x=>x.f===dr.fromId&&x.t===toId&&x.port===dr.fromPort)){
              const ne=[...edgesRef.current,{f:dr.fromId,t:toId,...(dr.fromPort!==undefined?{port:dr.fromPort}:{}),...(dr.fdy!=null?{fdy:dr.fdy}:{})}];edgesRef.current=ne;setEdges([...ne]);
              pushHistory(nodesRef.current,ne);
            }
          }
        }
      }
      dragRef.current=null;resizeRef.current=null;updateSVG();
    }
    document.addEventListener('mousemove',onMM);
    document.addEventListener('mouseup',onMU);
    return()=>{document.removeEventListener('mousemove',onMM);document.removeEventListener('mouseup',onMU);};
  },[]);

  useEffect(()=>{applyT();updateSVG();},[nodes,edges]);
  useEffect(()=>{applyT();},[]);

  // ── Focus mode : centrer le canvas sur le bloc actif ──
  useEffect(()=>{
    if(!focusActiveRef.current||!activeNodeId)return;
    const node=nodes.find(n=>n.id===activeNodeId);
    if(!node)return;
    const canvas=canvasRef.current;
    if(!canvas)return;
    const rect=canvas.getBoundingClientRect();
    const s=scaleRef.current;
    const nx=(node.x||0)+(node.w||160)/2;
    const ny=(node.y||0)+(node.h||52)/2;
    panRef.current={x:rect.width/2-nx*s, y:rect.height/2-ny*s};
    applyT();updateSVG();
  },[activeNodeId]);

  // ── auto-start batch si lancé depuis PilotageAuto ──────
  useEffect(()=>{
    try{
      const raw=localStorage.getItem('pl.auto.batchauto');
      if(raw){
        localStorage.removeItem('pl.auto.batchauto');
        const cfg=JSON.parse(raw);
        if(cfg&&cfg.count>0){
          setBatchCount(cfg.count);
          setBatchCooldown(cfg.cooldown||0);
          // Démarrer après que le canvas soit prêt
          setTimeout(()=>startBatch(),800);
        }
      }
    }catch(e){}
  },[]);

  // ── auto-run déclenché par une Campagne d'automatisation ──
  // La campagne écrit pl.campaign.run {flowId,device,signalId}. Ce flux (popup) le consomme,
  // applique le device, lance UN run, puis signale la fin via pl.campaign.done.<id> et se ferme.
  useEffect(()=>{
    try{
      const raw=localStorage.getItem('pl.campaign.run');
      if(!raw)return;
      const cfg=JSON.parse(raw);
      if(!cfg||cfg.flowId!==init.id)return;          // pas pour ce flux
      if(Date.now()-(cfg.at||0)>20000)return;         // périmé
      localStorage.removeItem('pl.campaign.run');     // consomme (1 seul popup le prend)
      if(cfg.device){setDeviceId(cfg.device);try{localStorage.setItem('pl.auto.dev',cfg.device);}catch(e){}}
      setCampRun({signalId:cfg.signalId,device:cfg.device||''});
    }catch(e){}
  },[]);
  // Lance le run quand le device cible est bien appliqué (closure doRun à jour)
  useEffect(()=>{
    if(!campRun)return;
    if(campRun.device&&deviceId!==campRun.device)return; // attend l'application du device
    const sig='pl.campaign.done.'+campRun.signalId;
    let done=false;
    const finish=(ok)=>{if(done)return;done=true;try{localStorage.setItem(sig,JSON.stringify({ok,at:Date.now()}));}catch(e){}setTimeout(()=>{try{window.close();}catch(e){}},1500);};
    setCampRun(null); // évite de relancer
    const t=setTimeout(async()=>{ try{await doRun(null);finish(true);}catch(e){finish(false);} },1500);
    return ()=>clearTimeout(t);
  },[campRun,deviceId]);

  // ── charger dessin sauvegardé ──────────────────────────
  useEffect(()=>{
    try{
      const saved=localStorage.getItem('pl.auto.draw.'+flowName);
      if(saved&&drawCvRef.current){
        const img=new Image();
        img.onload=()=>{const ctx=drawCvRef.current&&drawCvRef.current.getContext('2d');if(ctx)ctx.drawImage(img,0,0);};
        img.src=saved;
      }
    }catch(e){}
  },[]);

  // ── handlers dessin canvas global ─────────────────────
  function onDrawStart(e){
    if(!drawModeRef.current)return;
    e.preventDefault();e.stopPropagation();
    drawingRef.current=true;
    const r=canvasRef.current.getBoundingClientRect();
    const sc=scaleRef.current;
    drawLastRef.current={x:(e.clientX-r.left-panRef.current.x)/sc,y:(e.clientY-r.top-panRef.current.y)/sc};
  }
  function onDrawMove(e){
    if(!drawingRef.current||!drawModeRef.current)return;
    e.preventDefault();e.stopPropagation();
    const r=canvasRef.current.getBoundingClientRect();
    const sc=scaleRef.current;
    const pos={x:(e.clientX-r.left-panRef.current.x)/sc,y:(e.clientY-r.top-panRef.current.y)/sc};
    const ctx=drawCvRef.current.getContext('2d');
    if(drawEraserRef.current){
      const sz=drawSizeRef.current*3/sc;
      ctx.clearRect(pos.x-sz,pos.y-sz,sz*2,sz*2);
    } else {
      ctx.beginPath();ctx.moveTo(drawLastRef.current.x,drawLastRef.current.y);ctx.lineTo(pos.x,pos.y);
      ctx.strokeStyle=drawColorRef.current;ctx.lineWidth=drawSizeRef.current/sc;ctx.lineCap='round';ctx.lineJoin='round';ctx.stroke();
    }
    drawLastRef.current=pos;
  }
  function onDrawEnd(e){
    if(!drawingRef.current)return;
    drawingRef.current=false;
    try{localStorage.setItem('pl.auto.draw.'+flowName,drawCvRef.current.toDataURL());}catch(ex){}
  }
  function clearDraw(){
    if(!drawCvRef.current)return;
    const ctx=drawCvRef.current.getContext('2d');ctx.clearRect(0,0,8000,8000);
    try{localStorage.removeItem('pl.auto.draw.'+flowName);}catch(e){}
  }

  // ── clavier undo/redo ─────────────────────────────────
  useEffect(()=>{
    function onKey(e){
      const tag=document.activeElement&&document.activeElement.tagName;
      if(tag==='INPUT'||tag==='TEXTAREA'||tag==='SELECT')return;
      if(e.ctrlKey&&!e.shiftKey&&e.key==='z'){e.preventDefault();doUndoRef.current&&doUndoRef.current();}
      if(e.ctrlKey&&(e.key==='y'||(e.shiftKey&&e.key==='z'))){e.preventDefault();doRedoRef.current&&doRedoRef.current();}
      if(e.key==='Escape'){drawModeRef.current=false;setDrawMode(false);setDrawEraser(false);}
    }
    document.addEventListener('keydown',onKey);
    return()=>document.removeEventListener('keydown',onKey);
  },[]);

  // ── wheel zoom ─────────────────────────────────────────
  useEffect(()=>{
    const el=canvasRef.current;if(!el)return;
    function onWheel(e){
      e.preventDefault();
      if(drawModeRef.current)return;
      const delta=e.deltaY<0?1.12:0.9;
      const ns=Math.max(0.2,Math.min(3,scaleRef.current*delta));
      const rect=el.getBoundingClientRect();
      const mx=e.clientX-rect.left,my=e.clientY-rect.top;
      panRef.current={x:mx-(mx-panRef.current.x)*(ns/scaleRef.current),y:my-(my-panRef.current.y)*(ns/scaleRef.current)};
      scaleRef.current=ns;applyT();updateSVG();
      setZoomPct(Math.round(ns*100));
    }
    el.addEventListener('wheel',onWheel,{passive:false});
    return()=>el.removeEventListener('wheel',onWheel);
  },[]);

  // ── fetch device list — filtrée par la plateforme du flux (iOS -> iPhones, Android -> cloud) ───
  const flowPlat=init.platform==='android'?'android':'ios';
  useEffect(()=>{
    if(!window.Backend||!window.Backend.devices)return;
    Promise.all([
      flowPlat==='android'?Promise.resolve([]):window.Backend.devices().catch(()=>[]),
      (flowPlat==='android'&&window.Backend.cloudphones)?window.Backend.cloudphones().catch(()=>[]):Promise.resolve([]),
    ]).then(([ios,cloud])=>{
      const cloudDevs=(cloud||[]).map(cp=>({udid:cp.id,tag:'☁️ '+(cp.name||cp.id),name:cp.name||'',proxy:cp.proxyId,proxyName:cp.proxyName||'',proxyCountry:cp.countryCode||'',proxyUrl:cp.refreshUrl||'',_cloud:true}));
      setDevices([...(ios||[]),...cloudDevs]);
    }).catch(()=>{});
  },[flowPlat]);

  // ── context menu dismiss ───────────────────────────────
  useEffect(()=>{
    if(!ctxMenu)return;
    function close(){setCtxMenu(null);}
    document.addEventListener('mousedown',close);
    return()=>document.removeEventListener('mousedown',close);
  },[ctxMenu]);

  // ── historique undo/redo ───────────────────────────────
  function pushHistory(ns,es){
    historyRef.current=historyRef.current.slice(0,historyIdxRef.current+1);
    historyRef.current.push({nodes:ns.map(n=>({...n,d:{...n.d}})),edges:[...es]});
    if(historyRef.current.length>50)historyRef.current.shift();
    historyIdxRef.current=historyRef.current.length-1;
    setCanUndo(historyIdxRef.current>0);
    setCanRedo(false);
  }
  function _applyHistory(snapshot){
    const ns=snapshot.nodes.map(n=>({...n,d:{...n.d}}));
    const es=[...snapshot.edges];
    nodesRef.current=ns;edgesRef.current=es;
    setNodes(ns);setEdges(es);
    if(svgRef.current)svgRef.current.innerHTML='';
    setTimeout(()=>updateSVG(),30);
  }
  function doUndo(){
    if(historyIdxRef.current<=0){setCanUndo(false);return;}
    historyIdxRef.current--;
    _applyHistory(historyRef.current[historyIdxRef.current]);
    setCanUndo(historyIdxRef.current>0);
    setCanRedo(true);
  }
  function doRedo(){
    if(historyIdxRef.current>=historyRef.current.length-1){setCanRedo(false);return;}
    historyIdxRef.current++;
    _applyHistory(historyRef.current[historyIdxRef.current]);
    setCanUndo(true);
    setCanRedo(historyIdxRef.current<historyRef.current.length-1);
  }
  // refs toujours à jour pour les closures stales (useEffect keydown)
  doUndoRef.current=doUndo;
  doRedoRef.current=doRedo;

  // ── handlers ──────────────────────────────────────────
  function onSbDragStart(e,type){sbTypeRef.current=type;e.dataTransfer.setData('text/plain',type);}
  function onGroupDragStart(e,gid){sbTypeRef.current='GRP:'+gid;e.dataTransfer.setData('text/plain','GRP:'+gid);}
  function onDeleteGroup(gid){const g=loadBlockGroups().filter(x=>x.id!==gid);saveBlockGroups(g);setBlockGroups(g);}
  // Ouvre la pop-up de nommage (Electron désactive window.prompt → on utilise une vraie modale)
  function saveSelectionAsGroup(){
    const ids=Array.from(selectedIds);
    if(!ids.length)return;
    setGroupNameInput('Fermer Instagram');
    setGroupNaming({ids});
  }
  // Crée réellement le groupe une fois le nom validé
  function commitGroup(){
    const name=(groupNameInput||'').trim();
    if(!name||!groupNaming){setGroupNaming(null);return;}
    const sel=(groupNaming.ids||[]).map(id=>nodesRef.current.find(n=>n.id===id)).filter(Boolean);
    if(!sel.length){setGroupNaming(null);return;}
    const minX=Math.min.apply(null,sel.map(n=>n.x)),minY=Math.min.apply(null,sel.map(n=>n.y));
    const idSet=new Set(sel.map(n=>n.id));
    const gnodes=sel.map(n=>({type:n.type,dx:n.x-minX,dy:n.y-minY,d:{...n.d},oid:n.id}));
    const gedges=edgesRef.current.filter(eg=>idSet.has(eg.f)&&idSet.has(eg.t)).map(eg=>({f:eg.f,t:eg.t,port:eg.port}));
    const groups=loadBlockGroups();
    groups.push({id:'grp'+Date.now(),name,nodes:gnodes,edges:gedges,count:sel.length,createdAt:new Date().toISOString()});
    saveBlockGroups(groups);setBlockGroups(groups);
    setGroupNaming(null);
  }
  function onCanvasDrop(e){
    e.preventDefault();
    const type=sbTypeRef.current||e.dataTransfer.getData('text/plain');
    const rect=canvasRef.current.getBoundingClientRect();
    const s=scaleRef.current;
    const dropX=Math.max(0,(e.clientX-rect.left-panRef.current.x)/s-91);
    const dropY=Math.max(0,(e.clientY-rect.top-panRef.current.y)/s-22);
    // ── Groupe de blocs : instancie tous les nœuds + liens internes ──
    if(type&&type.indexOf('GRP:')===0){
      sbTypeRef.current=null;
      const grp=loadBlockGroups().find(g=>g.id===type.slice(4));
      if(!grp)return;
      const ts=Date.now();const idMap={};
      (grp.nodes||[]).forEach((n,i)=>{idMap[n.oid]='an'+(ts+i)+'_'+(Math.random()*99999|0);});
      const newNodes=(grp.nodes||[]).map(n=>({id:idMap[n.oid],type:n.type,x:dropX+(n.dx||0),y:dropY+(n.dy||0),d:{...n.d}}));
      const newEdges=(grp.edges||[]).filter(eg=>idMap[eg.f]&&idMap[eg.t]).map(eg=>({id:'ae'+(ts+(Math.random()*9999|0))+'_'+(Math.random()*999|0),f:idMap[eg.f],t:idMap[eg.t],port:eg.port}));
      const nn=[...nodesRef.current,...newNodes];const ne=[...edgesRef.current,...newEdges];
      nodesRef.current=nn;edgesRef.current=ne;setNodes(nn);setEdges(ne);
      pushHistory(nn,ne);setSelectedIds(new Set(newNodes.map(x=>x.id)));
      return;
    }
    if(!type||!AUTO_BLOCKS[type])return;
    const nn=makeAutoNode(type,dropX,dropY);
    nodesRef.current=[...nodesRef.current,nn];setNodes([...nodesRef.current]);sbTypeRef.current=null;
    pushHistory(nodesRef.current,edgesRef.current);
  }
  function onCanvasDown(e){
    if(drawModeRef.current)return;
    if(e.target.closest&&(e.target.closest('[data-node]')||e.target.closest('.auto-sb-chip')))return;
    if(e.ctrlKey||e.metaKey){
      const rect=canvasRef.current&&canvasRef.current.getBoundingClientRect();
      if(rect){const s=scaleRef.current;const x0=(e.clientX-rect.left-panRef.current.x)/s;const y0=(e.clientY-rect.top-panRef.current.y)/s;dragRef.current={type:'sel',startX:e.clientX,startY:e.clientY,x0,y0};}
    } else {
      setSelectedIds(new Set());
      dragRef.current={type:'pan',startX:e.clientX-panRef.current.x,startY:e.clientY-panRef.current.y};
    }
  }
  function onNodeHead(e,node){
    e.preventDefault();e.stopPropagation();
    if(e.ctrlKey||e.metaKey){
      // CTRL + clic sur un bloc déjà sélectionné dans un groupe → démarrer le drag du groupe
      if(selectedIds.has(node.id)&&selectedIds.size>1){
        const multi=Array.from(selectedIds).map(id=>{const nd=nodesRef.current.find(x=>x.id===id);return nd?{id,origX:nd.x,origY:nd.y}:null;}).filter(Boolean);
        dragRef.current={type:'node',id:node.id,startX:e.clientX,startY:e.clientY,origX:node.x,origY:node.y,multi};
      } else {
        // toggle sélection
        setSelectedIds(prev=>{const n=new Set(prev);n.has(node.id)?n.delete(node.id):n.add(node.id);return n;});
      }
      return;
    }
    const isMulti=selectedIds.has(node.id)&&selectedIds.size>1;
    if(isMulti){
      const multi=Array.from(selectedIds).map(id=>{const nd=nodesRef.current.find(x=>x.id===id);return nd?{id,origX:nd.x,origY:nd.y}:null;}).filter(Boolean);
      dragRef.current={type:'node',id:node.id,startX:e.clientX,startY:e.clientY,origX:node.x,origY:node.y,multi};
    } else {
      dragRef.current={type:'node',id:node.id,startX:e.clientX,startY:e.clientY,origX:node.x,origY:node.y};
    }
  }
  function onPortOut(e,nodeId,port){
    const rect=canvasRef.current&&canvasRef.current.getBoundingClientRect();
    const s=scaleRef.current,p=panRef.current;
    const el=e.currentTarget;
    const er=el.getBoundingClientRect();
    const fromX=rect?((er.left+er.width/2-rect.left-p.x)/s):null;
    const fromY=rect?((er.top+er.height/2-rect.top-p.y)/s):null;
    // stocker offset relatif au nœud pour que le câble suive quand le nœud bouge
    const fn=nodesRef.current.find(n=>n.id===nodeId);
    const fdy=fromY!=null&&fn?fromY-(fn.y+22):null;
    dragRef.current={type:'conn',fromId:nodeId,fromPort:port,fromX,fromY,fdy,cur:null};
  }
  function deleteNode(id){
    nodesRef.current=nodesRef.current.filter(n=>n.id!==id);
    edgesRef.current=edgesRef.current.filter(e=>e.f!==id&&e.t!==id);
    setNodes([...nodesRef.current]);setEdges([...edgesRef.current]);
    pushHistory(nodesRef.current,edgesRef.current);
  }
  function updateNodeData(id,k,v){
    const u=nodesRef.current.map(n=>n.id===id?{...n,d:{...n.d,[k]:v}}:n);
    nodesRef.current=u;setNodes([...u]);
    // debounce history push pour les champs texte (évite une entrée par frappe)
    if(histDebRef.current)clearTimeout(histDebRef.current);
    histDebRef.current=setTimeout(()=>pushHistory(nodesRef.current,edgesRef.current),600);
  }
  function clearCanvas(){
    nodesRef.current=[];edgesRef.current=[];setNodes([]);setEdges([]);
    if(svgRef.current)svgRef.current.innerHTML='';
    pushHistory([],[]);
  }

  // ── run ────────────────────────────────────────────────
  function addLog(msg,color){
    setLogs(prev=>[...prev,{msg,color:color||'#8b949e',id:Date.now()+Math.random()}]);
    setTimeout(()=>{const el=document.getElementById('auto-log-bottom');if(el)el.scrollIntoView({block:'end'});},30);
  }

  function _sleep(ms){return new Promise(res=>{runTimerRef.current=setTimeout(res,ms);});}
  function _setNodeResult(nodeId,data){
    nodeResultsRef.current={...nodeResultsRef.current,[nodeId]:{...(nodeResultsRef.current[nodeId]||{}),...data}};
    setNodeResults({...nodeResultsRef.current});
    try{localStorage.setItem('plab_noderesults',JSON.stringify(nodeResultsRef.current));}catch(e){}
  }

  function _swipePts(dir){
    const maps={UP:[{x:.5,y:.78,t:0},{x:.5,y:.55,t:120},{x:.5,y:.22,t:260}],DOWN:[{x:.5,y:.22,t:0},{x:.5,y:.5,t:130},{x:.5,y:.78,t:260}],LEFT:[{x:.8,y:.5,t:0},{x:.5,y:.5,t:130},{x:.2,y:.5,t:260}],RIGHT:[{x:.2,y:.5,t:0},{x:.5,y:.5,t:130},{x:.8,y:.5,t:260}]};
    return maps[dir]||maps.UP;
  }

  // substitue {{variable}} par la valeur dans le contexte runtime
  // {{2fa_code}} est toujours recalculé live depuis ctx['2fa_secret'] pour éviter les codes expirés
  async function _ctx(str,ctx){
    if(!str||!ctx)return str||'';
    let result=String(str);
    if(result.includes('{{2fa_code}}')&&ctx['2fa_secret']){
      // Normalise la clé stockée (au cas où elle aurait des espaces d'affichage)
      const cleanSecret=ctx['2fa_secret'].toUpperCase().replace(/[^A-Z2-7]/g,'');
      // Attend le prochain début de période TOTP (30s restantes) pour avoir un code frais
      const msInPeriod=Date.now()%30000;
      const secsLeft=Math.ceil((30000-msInPeriod)/1000);
      if(secsLeft<28){
        addLog('  ⏳ Attente renouvellement TOTP dans '+secsLeft+'s…','#8957e5');
        await _sleep(30000-msInPeriod+300);
      }
      const fresh=await _generateTOTP(cleanSecret);
      ctx['2fa_code']=fresh;
      addLog('  🔐 Code TOTP (clé: '+cleanSecret.length+'chars, période neuve): '+fresh,'#8957e5');
    }
    return result.replace(/\{\{(\w+)\}\}/g,(_,k)=>ctx[k]!==undefined?ctx[k]:'{{'+k+'}}');
  }

  async function _execBlock(node,udid,live,ctx){
    const d=node.d||{};
    const B=window.Backend;
    if(live){
      // ── Blocs médias dynamiques (media_cat_<slug>) : envoient un fichier du sous-dossier <catégorie> du profil ──
      if(node.type&&node.type.indexOf('media_cat_')===0){
        if(!udid){addLog('  ⚠️ Aucun téléphone sélectionné','#d29922');return;}
        const _cat=(d.cat)||((AUTO_BLOCKS[node.type]||{}).f||[{}])[0].v||'Vidéo';
        const _tag=((devicesRef.current.find(x=>x.udid===udid)||{}).tag||'').replace(/^☁️\s*/,'').trim();
        addLog('  📤 Envoi média « '+_cat+' » depuis le dossier du profil…','#1dbab4');
        const r=B.mediaSendFromFolder?await B.mediaSendFromFolder(udid,_tag,_cat):{ok:false,error:'indisponible'};
        if(r&&r.ok)addLog('  ✅ Média « '+r.name+' » envoyé (dossier '+(r.sub||_cat)+')','#3fb950');
        else addLog('  ⚠️ '+((r&&r.error)||'envoi impossible'),'#d29922');
        await _sleep(1500); return;
      }
      switch(node.type){
        case'trigger_manual': await _sleep(300); break;
        case'wda_launch':
          addLog('  → launchApp: '+(d.bundle||'?'),'#484f58');
          await B.action(udid,'launchApp',{content:d.bundle||'com.burbn.instagram'});
          await _sleep(2500); break;
        case'wda_date_picker':{
          if(!ctx.birthday){addLog('  ⚠️ Pas de birthday dans ctx — passe d\'abord dans Générer infos compte','#d29922');break;}
          const xM=parseFloat(d.x_month||17)/100;
          const xD=parseFloat(d.x_day||50)/100;
          const xY=parseFloat(d.x_year||75)/100;
          const yA=parseFloat(d.y_above||42)/100;
          const yB=parseFloat(d.y_below||58)/100;
          const parts=ctx.birthday.split('-');
          const tYear=parseInt(parts[0]),tMonth=parseInt(parts[1]),tDay=parseInt(parts[2]);
          const now=new Date();
          const todayYear=now.getFullYear(),todayMonth=now.getMonth()+1,todayDay=now.getDate();
          const diffYear=tYear-todayYear;
          const diffMonth=tMonth-todayMonth;
          const diffDay=tDay-todayDay;
          addLog('  📅 Birthday: '+ctx.birthday+' (aujourd\'hui: '+todayYear+'-'+String(todayMonth).padStart(2,'0')+'-'+String(todayDay).padStart(2,'0')+')','#1f6feb');
          addLog('  → Année: '+diffYear+' | Mois: '+diffMonth+' | Jour: '+diffDay,'#484f58');
          const tapDelay=Math.max(50,parseInt(d.delay||80));
          const tapN=async(x,y,n,label)=>{
            const absN=Math.abs(n);
            addLog('  ⟳ '+label+': '+absN+' taps '+(n<0?'▲ dessus':'▼ dessous')+' ('+tapDelay+'ms/tap)','#1f6feb');
            for(let i=0;i<absN;i++){
              if(!runningRef.current)return;
              await B.action(udid,'click',{x,y,duration:0.05});
              await _sleep(tapDelay);
            }
            await _sleep(400);
          };
          // Ordre : Année → Jour → Mois
          if(diffYear!==0)  await tapN(xY,diffYear<0?yA:yB,diffYear,'Année');
          if(diffDay!==0)   await tapN(xD,diffDay<0?yA:yB,diffDay,'Jour');
          if(diffMonth!==0) await tapN(xM,diffMonth<0?yA:yB,diffMonth,'Mois');
          addLog('  ✅ Date sélectionnée','#3fb950');
          break;}
        case'wda_tap_xy':{
          const tx=Math.min(1,Math.max(0,parseFloat(d.x||50)/100));
          const ty=Math.min(1,Math.max(0,parseFloat(d.y||50)/100));
          addLog('  → click ('+tx.toFixed(2)+', '+ty.toFixed(2)+')','#484f58');
          await B.action(udid,'click',{x:tx,y:ty,duration:0.1});
          await _sleep(700); break;}
        case'wda_tap_repeat':{
          const tx=Math.min(1,Math.max(0,parseFloat(d.x||50)/100));
          const ty=Math.min(1,Math.max(0,parseFloat(d.y||50)/100));
          const n=Math.max(1,parseInt(d.n||5));
          const delay=Math.max(100,parseInt(d.delay||500));
          addLog('  → tap ×'+n+' ('+tx.toFixed(2)+', '+ty.toFixed(2)+') délai '+delay+'ms','#484f58');
          for(let i=0;i<n;i++){
            if(!runningRef.current)break;
            await B.action(udid,'click',{x:tx,y:ty,duration:0.05});
            if(i<n-1)await _sleep(delay);
          }
          await _sleep(400); break;}
        case'wda_tap_text':{
          const txt=await _ctx(d.text,ctx);
          addLog('  → tap text: "'+txt+'"','#484f58');
          const wdaR=await B.action(udid,'tapByText',{content:txt});
          if(wdaR&&wdaR.ok===false){
            addLog('  ⚠️ WDA: "'+txt+'" non trouvé — fallback Gemini…','#d29922');
            try{
              const base=window.Backend&&window.Backend.base?window.Backend.base:'http://127.0.0.1:8787';
              const keysResp=await fetch(base+'/api/apikeys');
              const keysData=keysResp.ok?await keysResp.json():{};
              const geminiKey=(keysData.keys||{}).gemini||'';
              if(!geminiKey){addLog('  ❌ Clé Gemini manquante (Paramètres → Clés API)','#f85149');break;}
              const surl=base+'/api/devices/'+encodeURIComponent(udid)+'/screen?hq=1&ts='+Date.now();
              const sb=await(await fetch(surl,{cache:'no-store'})).blob();
              const b64=await new Promise(res=>{const fr=new FileReader();fr.onload=()=>res(fr.result.split(',')[1]||'');fr.onerror=()=>res('');fr.readAsDataURL(sb);});
              if(!b64){addLog('  ❌ Screenshot impossible','#f85149');break;}
              const prompt='iPhone screenshot. Find UI element with text "'+txt+'". Return ONLY JSON: {"x":0.5,"y":0.3} with center as 0.0-1.0 fractions of screen. No explanation.';
              const gr=await fetch('https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key='+geminiKey,{
                method:'POST',headers:{'Content-Type':'application/json'},
                body:JSON.stringify({contents:[{parts:[{text:prompt},{inline_data:{mime_type:'image/jpeg',data:b64}}]}]})
              });
              const gj=await gr.json();
              const raw=(gj?.candidates?.[0]?.content?.parts?.[0]?.text||'').trim();
              const m=raw.match(/\{[^}]+\}/);
              if(m){
                const c=JSON.parse(m[0]);const gx=parseFloat(c.x),gy=parseFloat(c.y);
                if(gx>=0&&gx<=1&&gy>=0&&gy<=1){
                  addLog('  🤖 Gemini → tap ('+Math.round(gx*100)+'%, '+Math.round(gy*100)+'%)','#8957e5');
                  await B.action(udid,'click',{x:gx,y:gy,duration:0.05});
                } else addLog('  ❌ Coords Gemini invalides','#f85149');
              } else addLog('  ❌ Gemini: "'+txt+'" introuvable sur l\'écran','#f85149');
            }catch(e){addLog('  ❌ Gemini fallback: '+e.message,'#f85149');}
          }
          await _sleep(700); break;}
        case'wda_tap_ai':{
          const desc=await _ctx(d.prompt||'',ctx);
          addLog('  🔍 Tap IA (grille 2 phases): "'+desc+'"','#8957e5');
          if(live){
            try{
              const base=window.Backend&&window.Backend.base?window.Backend.base:'http://127.0.0.1:8787';
              const keysResp=await fetch(base+'/api/apikeys');
              const keysData=keysResp.ok?await keysResp.json():{};
              const geminiKey=(keysData.keys||{}).gemini||'';
              if(!geminiKey){addLog('  ❌ Clé Gemini manquante','#f85149');break;}

              // ── Helper : dessiner une grille lisible sur une image (base64 jpeg → base64 jpeg)
              const _drawGrid=(srcB64,cols,rows,colLabels,opts={})=>new Promise(res=>{
                const img=new Image();
                img.onload=()=>{
                  const cv=document.createElement('canvas');
                  cv.width=img.width; cv.height=img.height;
                  const gc=cv.getContext('2d');
                  gc.drawImage(img,0,0);
                  const cw=img.width/cols, rh=img.height/rows;
                  const lw=opts.lw||Math.max(28,Math.round(cw*0.22));
                  const lh=opts.lh||Math.max(22,Math.round(rh*0.22));
                  // Fond des bandeaux labels
                  gc.fillStyle='rgba(0,0,0,0.72)';
                  gc.fillRect(0,0,img.width,lh);
                  gc.fillRect(0,lh,lw,img.height-lh);
                  // Lignes de grille
                  gc.strokeStyle='rgba(255,80,80,0.8)'; gc.lineWidth=opts.lineW||2;
                  for(let c=1;c<cols;c++){gc.beginPath();gc.moveTo(lw+c*((img.width-lw)/cols),lh);gc.lineTo(lw+c*((img.width-lw)/cols),img.height);gc.stroke();}
                  for(let r=1;r<rows;r++){gc.beginPath();gc.moveTo(0,lh+r*((img.height-lh)/rows));gc.lineTo(img.width,lh+r*((img.height-lh)/rows));gc.stroke();}
                  // Labels colonnes
                  const fw=(img.width-lw)/cols, fh=(img.height-lh)/rows;
                  gc.fillStyle='#fff'; gc.textAlign='center'; gc.textBaseline='middle';
                  gc.font='bold '+Math.round(lh*0.65)+'px Arial';
                  for(let c=0;c<cols;c++) gc.fillText(colLabels[c]||String(c+1), lw+(c+0.5)*fw, lh/2);
                  // Labels lignes
                  gc.font='bold '+Math.round(lw*0.55)+'px Arial';
                  for(let r=0;r<rows;r++) gc.fillText(String(r+1), lw/2, lh+(r+0.5)*fh);
                  // Croix au centre de chaque cellule (aide Gemini à pointer)
                  gc.strokeStyle='rgba(255,255,0,0.35)'; gc.lineWidth=1;
                  for(let c=0;c<cols;c++) for(let r=0;r<rows;r++){
                    const cx=lw+(c+0.5)*fw, cy=lh+(r+0.5)*fh;
                    gc.beginPath();gc.moveTo(cx-5,cy);gc.lineTo(cx+5,cy);gc.stroke();
                    gc.beginPath();gc.moveTo(cx,cy-5);gc.lineTo(cx,cy+5);gc.stroke();
                  }
                  res(cv.toDataURL('image/jpeg',0.93).split(',')[1]);
                };
                img.onerror=()=>res(srcB64);
                img.src='data:image/jpeg;base64,'+srcB64;
              });

              // ── Helper : appel Gemini Vision
              const _gemini=async(b64,prompt)=>{
                const r=await fetch('https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key='+geminiKey,{
                  method:'POST',headers:{'Content-Type':'application/json'},
                  body:JSON.stringify({contents:[{parts:[{text:prompt},{inline_data:{mime_type:'image/jpeg',data:b64}}]}],generationConfig:{maxOutputTokens:256}})
                });
                const j=await r.json();
                if(j?.error)throw new Error(j.error.message);
                const pts=j?.candidates?.[0]?.content?.parts||[];
                return(pts.find(p=>!p.thought)||pts[0]||{}).text||'';
              };

              // ── 1. Screenshot HQ
              const surl=base+'/api/devices/'+encodeURIComponent(udid)+'/screen?hq=1&ts='+Date.now();
              const rawB64=await new Promise(res=>{
                fetch(surl,{cache:'no-store'}).then(r=>r.blob()).then(b=>{
                  const fr=new FileReader();fr.onload=()=>res(fr.result.split(',')[1]||'');fr.onerror=()=>res('');fr.readAsDataURL(b);
                }).catch(()=>res(''));
              });
              if(!rawB64){addLog('  ❌ Screenshot impossible','#f85149');break;}

              // ── PHASE 1 : grille grossière 6×12 — grandes cases, grands labels, facile à lire
              const C1=6, R1=12, L1='ABCDEF';
              addLog('  📐 Phase 1 — grille '+C1+'×'+R1+'…','#484f58');
              const g1=await _drawGrid(rawB64,C1,R1,L1);
              const p1prompt=
                'iPhone screenshot with grid. Columns A-F (left→right), rows 1-12 (top→bottom). Content area starts after the labels.\n'+
                'TASK: "'+desc+'"\n'+
                'Look at the screenshot. Decide what to tap. Give the grid cell of its CENTER.\n'+
                'Reply ONLY with JSON, no text before or after: {"col":"B","row":4}\n'+
                'If nothing matches: {"col":null,"row":null}';
              const r1=await _gemini(g1,p1prompt);
              addLog('  ← Phase 1: '+r1.replace(/\s+/g,' ').slice(0,80),'#484f58');
              const m1=r1.match(/\{[^}]*\}/);
              if(!m1){addLog('  ❌ Phase 1 non parseable','#f85149');break;}
              const p1=JSON.parse(m1[0]);
              const c1str=(p1.col||'').toString().toUpperCase().trim();
              const r1num=parseInt(p1.row||0);
              const c1idx=L1.indexOf(c1str);
              if(c1idx<0||r1num<1||r1num>R1){addLog('  ❌ Élément introuvable (phase 1)','#f85149');break;}
              addLog('  ✓ Phase 1 → case '+c1str+r1num,'#d29922');

              // ── PHASE 2 : zoom sur la zone trouvée + grille fine 5×5
              // La zone de contenu (hors bandeau labels) fait ~85% de la largeur et ~88% de la hauteur
              // On convertit la case grossière en coordonnées 0-1 dans l'image complète
              const margin=0.08; // approximation du bandeau label en fraction
              const cellW=(1-margin)/C1, cellH=(1-margin)/R1;
              const zx0=margin+c1idx*cellW, zy0=margin+(r1num-1)*cellH;
              // Zone de crop : ±1 cellule autour de la case, clampé à [0,1]
              const pad=1.5;
              const cx0=Math.max(0,zx0-pad*cellW), cy0=Math.max(0,zy0-pad*cellH);
              const cx1=Math.min(1,zx0+(1+pad)*cellW), cy1=Math.min(1,zy0+(1+pad)*cellH);

              // Crop de l'image originale (sans grille) autour de la zone
              const cropB64=await new Promise(res=>{
                const img=new Image();
                img.onload=()=>{
                  const sw=img.width,sh=img.height;
                  const sx=Math.round(cx0*sw),sy=Math.round(cy0*sh);
                  const sw2=Math.round((cx1-cx0)*sw),sh2=Math.round((cy1-cy0)*sh);
                  const cv=document.createElement('canvas');
                  // Upscale 2× pour que les détails soient plus nets pour Gemini
                  cv.width=sw2*2; cv.height=sh2*2;
                  cv.getContext('2d').drawImage(img,sx,sy,sw2,sh2,0,0,sw2*2,sh2*2);
                  res(cv.toDataURL('image/jpeg',0.95).split(',')[1]);
                };
                img.onerror=()=>res(rawB64);
                img.src='data:image/jpeg;base64,'+rawB64;
              });

              const C2=5, R2=5, L2='12345';
              addLog('  📐 Phase 2 — zoom ×2 + grille fine '+C2+'×'+R2+'…','#484f58');
              const g2=await _drawGrid(cropB64,C2,R2,L2,{lineW:3});
              const p2prompt=
                'Zoomed-in iPhone screenshot with a fine '+C2+'×'+R2+' grid. Columns labeled 1-'+C2+' (left→right), rows labeled 1-'+R2+' (top→bottom).\n'+
                'TASK: "'+desc+'"\n'+
                'Find the CENTER of the target element in this zoomed view.\n'+
                'Reply ONLY with JSON: {"col":3,"row":2}\n'+
                'If not visible: {"col":null,"row":null}';
              const r2=await _gemini(g2,p2prompt);
              addLog('  ← Phase 2: '+r2.replace(/\s+/g,' ').slice(0,80),'#484f58');
              const m2=r2.match(/\{[^}]*\}/);
              // Si phase 2 échoue on reste au centre de la case grossière
              let gx=(c1idx+0.5)/C1*(1-margin)+margin;
              let gy=(r1num-0.5)/R1*(1-margin)+margin;
              if(m2){
                try{
                  const p2=JSON.parse(m2[0]);
                  const c2=parseInt(p2.col||0), r2n=parseInt(p2.row||0);
                  if(c2>=1&&c2<=C2&&r2n>=1&&r2n<=R2){
                    // Position fine dans la zone cropée (0-1 dans le crop)
                    const fx=(c2-0.5)/C2, fy=(r2n-0.5)/R2;
                    // Reconvertir en coordonnées écran complètes
                    gx=cx0+fx*(cx1-cx0);
                    gy=cy0+fy*(cy1-cy0);
                    addLog('  ✓ Phase 2 → col '+c2+' row '+r2n,'#d29922');
                  }
                }catch(_){}
              }
              addLog('  🎯 Tap final → ('+Math.round(gx*100)+'%, '+Math.round(gy*100)+'%)','#3fb950');
              if(window._autoTapPreview) window._autoTapPreview({x:gx,y:gy});
              await _sleep(300);
              await B.action(udid,'click',{x:gx,y:gy,duration:0.05});
            }catch(e){addLog('  ❌ Tap IA: '+e.message,'#f85149');}
          } else {await _sleep(600);}
          await _sleep(700); break;}
        case'wda_tap_router':{
          const labels=_parseLabels(node.d.labels);
          if(!labels.length){addLog('  ⚠️ Router: aucun label configuré','#f85149');break;}
          // Helper: cherche une zone par nom dans tous les blocs wda_zone_tap du canvas
          const _findZone=(name)=>{for(const n of nodesRef.current){if(n.type==='wda_zone_tap'){const z=_parseZones(n.d.zones).find(z=>z.name===name);if(z)return z;}}return null;};
          if(live){
            try{
              const base=window.Backend&&window.Backend.base?window.Backend.base:'http://127.0.0.1:8787';
              const keysResp=await fetch(base+'/api/apikeys');
              const keysData=keysResp.ok?await keysResp.json():{};
              const geminiKey=(keysData.keys||{}).gemini||'';
              if(!geminiKey){addLog('  ❌ Clé Gemini manquante','#f85149');break;}
              const surl=base+'/api/devices/'+encodeURIComponent(udid)+'/screen?hq=1&ts='+Date.now();
              const rawB64=await new Promise(res=>{
                fetch(surl,{cache:'no-store'}).then(r=>r.blob()).then(b=>{
                  const fr=new FileReader();fr.onload=()=>res(fr.result.split(',')[1]||'');fr.onerror=()=>res('');fr.readAsDataURL(b);
                }).catch(()=>res(''));
              });
              if(!rawB64){addLog('  ❌ Screenshot impossible','#f85149');break;}
              const rPrompt=
                'Look at this iPhone screenshot.\n'+
                'Which of these UI elements is currently VISIBLE on screen?\n'+
                'Options: '+labels.map((l,i)=>(i+1)+'. "'+l+'"').join(', ')+'\n'+
                'Reply ONLY with the number of the matching option (e.g. 1 or 2). If none visible, reply 0.';
              const gr=await fetch('https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key='+geminiKey,{
                method:'POST',headers:{'Content-Type':'application/json'},
                body:JSON.stringify({contents:[{parts:[{text:rPrompt},{inline_data:{mime_type:'image/jpeg',data:rawB64}}]}],generationConfig:{maxOutputTokens:64,thinkingConfig:{thinkingBudget:0}}})
              });
              const gj=await gr.json();
              if(gj?.error)throw new Error(gj.error.message);
              const pts=gj?.candidates?.[0]?.content?.parts||[];
              const ans=((pts.find(p=>!p.thought)||pts[0]||{}).text||'').trim();
              addLog('  ← Gemini router: "'+ans+'"','#8957e5');
              const num=parseInt(ans);
              if(num>=1&&num<=labels.length){
                const detectedLabel=labels[num-1];
                ctx._routerPort=num-1;
                addLog('  🎯 Détecté: "'+detectedLabel+'" → port '+(num-1),'#3fb950');
                // Tap sur la zone calibrée si elle existe
                const zone=_findZone(detectedLabel);
                if(zone){
                  const xp=(zone.x||50)/100, yp=(zone.y||50)/100;
                  addLog('  📍 Tap zone → ('+Math.round(xp*100)+'%, '+Math.round(yp*100)+'%)','#3fb950');
                  if(window._autoTapPreview)window._autoTapPreview({x:xp,y:yp});
                  await _sleep(200);
                  await B.action(udid,'click',{x:xp,y:yp,duration:0.05});
                  await _sleep(500);
                } else {addLog('  ⚠️ Aucune zone calibrée pour "'+detectedLabel+'" — tap ignoré','#d29922');}
              } else {addLog('  ↪ Aucun label reconnu → port par défaut','#d29922');ctx._routerPort=labels.length;}
            }catch(e){addLog('  ❌ Router IA: '+e.message,'#f85149');}
          } else {
            ctx._routerPort=0;
            addLog('  🔀 [sim] → port 0 ('+labels[0]+')','#484f58');
          }
          break;}
        case'wda_type':{
          const txt=await _ctx(d.text,ctx);
          const charDelay=parseInt(d.delay||0);
          addLog('  → inputText: "'+txt+'"'+(txt.includes('{{')?'  ⚠️ var non résolue':'')+(charDelay>0?' ('+charDelay+'ms/char)':''),'#484f58');
          if(charDelay>0){
            for(const ch of txt){
              if(!runningRef.current)break;
              await B.action(udid,'inputText',{content:ch});
              await _sleep(charDelay);
            }
          } else {
            await B.action(udid,'inputText',{content:txt});
          }
          await _sleep(600); break;}
        case'wda_swipe':
          addLog('  → swipe '+(d.dir||'UP'),'#484f58');
          await B.action(udid,'gesture',{points:_swipePts(d.dir||'UP')});
          await _sleep(900); break;
        case'wda_long_drag':{
          const x1p=parseFloat(d.x1||50)/100, y1p=parseFloat(d.y1||50)/100;
          const x2p=parseFloat(d.x2||50)/100, y2p=parseFloat(d.y2||70)/100;
          const holdMs=Math.round(parseFloat(d.hold||1.5)*1000);
          addLog('  🖐 Long press ('+holdMs+'ms) de ('+Math.round(x1p*100)+'%,'+Math.round(y1p*100)+'%) → ('+Math.round(x2p*100)+'%,'+Math.round(y2p*100)+'%)','#388bfd');
          if(live){
            // gesture: maintient le doigt sur A pendant holdMs, puis glisse vers B
            const pts=[
              {x:x1p,y:y1p,t:0},
              {x:x1p,y:y1p,t:holdMs},
              {x:x2p,y:y2p,t:holdMs+300},
            ];
            await B.action(udid,'gesture',{points:pts});
            await _sleep(holdMs+800);
          } else {
            await _sleep(Math.max(500,holdMs));
          }
          break;}
        case'wda_home':
          addLog('  → home','#484f58');
          await B.action(udid,'home');
          await _sleep(1200); break;
        case'wda_wait_elem':
          addLog('  → attente élément "'+(d.text||'?')+'"','#484f58');
          await _sleep(Math.min(parseFloat(d.timeout||15)*1000,15000)); break;
        case'wda_screenshot': await _sleep(800); break;
        case'farm_rotate_ip':{
          // URL de rotation = celle du PROXY. iPhone -> store app ; Cloud -> proxy MoreLogin porté par le device.
          const _dev=(devicesRef.current||[]).find(x=>x.udid===udid);
          let _rotUrl='',_pxName='',_cdId='';
          if(_dev&&_dev._cloud){
            _rotUrl=_dev.proxyUrl||'';_pxName=_dev.proxyName||'proxy cloud';_cdId=_dev.proxy||udid;
          }else{
            const _px=(_dev&&_dev.proxy)?(proxiesRef.current||[]).find(p=>String(p.id)===String(_dev.proxy)):null;
            _rotUrl=(_px&&_px.url)?_px.url:'';_pxName=_px?_px.name:'';_cdId=_px?_px.id:'';
          }
          if(!_rotUrl){
            addLog('  ❌ Rotation IP : aucun lien de rotation trouvé pour '+(_pxName?('le proxy « '+_pxName+' »'):'ce device (aucun proxy assigné)')+(_dev&&_dev._cloud?' — renseigne l\'URL de rotation du proxy dans MoreLogin':' — renseigne l\'URL de rotation dans Admin → Proxys'),'#f85149');
            try{window.SoundBoard&&window.SoundBoard.playForce('error');}catch(_){}
            lastRunOkRef.current=false;
            break;
          }
          addLog('  🔌 Proxy « '+_pxName+' » → rotation via son URL','#484f58');
          // Cooldown 5min PAR PROXY (deux proxys différents ne se bloquent pas)
          const _cdKey=ROTATE_IP_LS+(_cdId?('.'+_cdId):'');
          const _lastRot=parseInt(localStorage.getItem(_cdKey)||'0');
          const _elapsed=Date.now()-_lastRot;
          if(_elapsed<ROTATE_IP_COOLDOWN){
            const _rem=Math.ceil((ROTATE_IP_COOLDOWN-_elapsed)/1000);
            const _m=Math.floor(_rem/60),_s=_rem%60;
            addLog('  ⏳ Cooldown actif — attendre encore '+_m+'m'+_s+'s avant de changer l\'IP','#d29922');
            addLog('  ⏳ Attente du cooldown…','#d29922');
            // Attendre la fin du cooldown
            const _wait=ROTATE_IP_COOLDOWN-_elapsed;
            await _sleep(_wait);
          }
          addLog('  🔄 Changement IP…','#1dbab4');
          try{
            const rr=await B.fetchUrl(_rotUrl);
            const raw=(rr.raw||'').trim();
            addLog('  ← API: '+raw.slice(0,120),'#484f58');
            // Sauvegarder le timestamp pour le cooldown (par proxy)
            localStorage.setItem(_cdKey,String(Date.now()));
            // Récupérer la nouvelle IP via proxy
            try{
              const base=window.Backend&&window.Backend.base?window.Backend.base:'http://127.0.0.1:8787';
              await _sleep(4000); // laisser le proxy s'établir
              const ipR=await fetch(base+'/api/proxy-ip?ts='+Date.now(),{cache:'no-store'});
              const ipJ=await ipR.json();
              if(ipJ.ok&&ipJ.ip){
                localStorage.setItem('pl.rotateip.lastIp',ipJ.ip);
                addLog('  ✅ Nouvelle IP : '+ipJ.ip,'#3fb950');
              }else{addLog('  ✅ Rotation envoyée (IP non vérifiable)','#3fb950');}
            }catch(_){addLog('  ✅ Rotation envoyée','#3fb950');}
          }catch(e){addLog('  ❌ rotate-ip: '+e.message,'#f85149');try{window.SoundBoard&&window.SoundBoard.playForce('error');}catch(_){}lastRunOkRef.current=false;}
          // Notifie les blocs IP pour qu'ils se rafraîchissent
          window.dispatchEvent(new CustomEvent('ip-rotated'));
          break;}
        case'farm_switch_provider':{
          const list=(d.providers||'HeroSMS,5sim,GetAText').split(',').map(s=>s.trim()).filter(Boolean);
          if(!list.length)break;
          ctx._switchProvIdx=((ctx._switchProvIdx??-1)+1)%list.length;
          const prov=list[ctx._switchProvIdx];
          ctx._smsProvider=prov;
          addLog('  🔀 Switch provider → '+prov+' ('+(ctx._switchProvIdx+1)+'/'+list.length+')','#1dbab4');
          // Mise à jour visuelle de TOUS les blocs farm_buy_number du flux
          nodesRef.current.filter(n=>n.type==='farm_buy_number').forEach(n=>{
            updateNodeData(n.id,'prov',prov);
          });
          break;}
        case'farm_buy_number':{
          const prov=ctx._smsProvider||d.prov||'HeroSMS';
          const country=d.country||'USA';
          addLog('  📞 Achat numéro '+country+' via '+prov+'…','#1dbab4');
          try{
            let numId='',phone='';
            if(prov==='GetAText'){
              let GAT_KEY=d.apikey||ctx._gatKey||'';
              if(!GAT_KEY){try{const _k=JSON.parse(localStorage.getItem('farm_apikeys')||'{}');GAT_KEY=_k.getatext||'';}catch(_){}}
              if(!GAT_KEY){addLog('  ❌ GetAText: clé API manquante (renseigne-la dans Paramètres → Clés API)','#f85149');break;}
              const rr=await B.relay('https://getatext.com/api/v1/rent-a-number',{method:'POST',headers:{'Auth':GAT_KEY,'Content-Type':'application/json'},body:JSON.stringify({service:'instagram/threads'})});
              const j=rr.body||{};
              if(!j.id){addLog('  ❌ GetAText: '+(rr.raw||JSON.stringify(j)),'#f85149');break;}
              numId=String(j.id);phone=(j.number||'').replace(/^\+1/,'');
              ctx._gatKey=GAT_KEY;
            } else {
              // clé API HeroSMS : renseignée dans l'app (Réglages → Clés API), jamais en dur
              let HERO_KEY='';try{HERO_KEY=(JSON.parse(localStorage.getItem('farm_apikeys')||'{}').herosms)||'';}catch(_){}
              if(!HERO_KEY){addLog('  ❌ HeroSMS : clé API manquante (Réglages → Clés API)','#f85149');break;}
              const HERO_BASE='https://hero-sms.com/stubs/handler_api.php';
              const heroCountry=country==='France'?'33':'187';
              const rr=await B.fetchUrl(HERO_BASE+'?api_key='+HERO_KEY+'&action=getNumber&service=ig&country='+heroCountry);
              const txt=(rr.raw||'').trim();
              if(!txt.startsWith('ACCESS_NUMBER')){addLog('  ❌ HeroSMS: '+txt,'#f85149');break;}
              const parts=txt.split(':');
              numId=parts[1].trim();phone=(parts[2]||'').trim();
            }
            ctx.phone=phone;ctx._heroNumId=numId;ctx._smsProvider=prov;
            const _numSaved={phone:(phone.startsWith('+')?'':'+1')+phone,numId,prov};
            _setNodeResult(node.id,_numSaved);
            try{localStorage.setItem('plab_num_'+node.id,JSON.stringify(_numSaved));}catch(_){}
            addLog('  ✅ Numéro: +1'+phone+'  (ID: '+numId+')','#3fb950');
          }catch(e){addLog('  ❌ '+prov+': '+e.message,'#f85149');}
          break;}
        case'farm_wait_sms':{
          const timeout2=Math.max(30,parseFloat(d.timeout||360));
          const numId=(d.numid&&d.numid.trim())||ctx._heroNumId||'';
          if(!numId){addLog('  ⚠️ Pas de numId — relie au bloc Acheter numéro','#d29922');break;}
          const prov2=ctx._smsProvider||'HeroSMS';
          addLog('  ⏳ ['+prov2+'] Attente SMS id='+numId+' (max '+timeout2+'s)…','#1dbab4');
          const t0=Date.now();let code=null;let pollN=0;
          while(Date.now()-t0<timeout2*1000){
            if(!runningRef.current)break;
            try{
              if(prov2==='GetAText'){
                let GAT_KEY=d.apikey||ctx._gatKey||'';
                if(!GAT_KEY){try{const _k=JSON.parse(localStorage.getItem('farm_apikeys')||'{}');GAT_KEY=_k.getatext||'';}catch(_){}}
                const rr=await B.relay('https://getatext.com/api/v1/rental-status',{method:'POST',headers:{'Auth':GAT_KEY,'Content-Type':'application/json'},body:JSON.stringify({id:parseInt(numId)})});
                const j=rr.body||{};
                pollN++;
                addLog('  ↻ ['+pollN+'] status='+j.status+' code='+j.code,'#484f58');
                if(j.code){code=String(j.code);break;}
                if(j.status==='expired'||j.status==='cancelled'){addLog('  ❌ Numéro expiré','#f85149');break;}
              } else {
                // clé API HeroSMS : renseignée dans l'app (Réglages → Clés API), jamais en dur
              let HERO_KEY='';try{HERO_KEY=(JSON.parse(localStorage.getItem('farm_apikeys')||'{}').herosms)||'';}catch(_){}
              if(!HERO_KEY){addLog('  ❌ HeroSMS : clé API manquante (Réglages → Clés API)','#f85149');break;}
                const HERO_BASE='https://hero-sms.com/stubs/handler_api.php';
                const rr=await B.fetchUrl(HERO_BASE+'?api_key='+HERO_KEY+'&action=getStatus&id='+numId);
                const raw=(rr.raw||'').trim();
                pollN++;
                addLog('  ↻ ['+pollN+'] '+raw,'#484f58');
                if(raw.indexOf('STATUS_OK')!==-1){
                  const afterOk=raw.split('STATUS_OK:')[1]||'';
                  const m=afterOk.match(/\d{4,8}/);
                  code=m?m[0]:afterOk.split(':').pop().trim();
                  break;
                }
                if(raw==='STATUS_CANCEL'||raw==='STATUS_FINISH'||raw==='STATUS_EXPIRED'){addLog('  ❌ Numéro expiré/annulé','#f85149');break;}
              }
            }catch(e){addLog('  ⚠️ poll: '+e.message,'#d29922');}
            await _sleep(5000);
          }
          if(code){ctx.sms_code=code;ctx._branchResult=true;_setNodeResult(node.id,{code});addLog('  ✅ Code SMS: '+code,'#3fb950');}
          else{ctx._branchResult=false;if(runningRef.current)addLog('  ⏱️ Timeout ('+timeout2+'s) → sortie NON','#f85149');}
          break;}
        case'logic_wait': await _sleep(Math.max(200,parseFloat(d.s||2)*1000)); break;
        case'logic_random': case'logic_merge': case'logic_split': await _sleep(200); break;
        case'gen_account':{
          const _SFXS=['lea','lou','lia','ana','eva','mia','zia','rue','eve','aya','mla','ola','sia','ema','ila'];
          const _SYMS=['!','#','$','&','*','@'];
          const _EMJS=['🌸','🦋','✨','🌙','💫','🌿','🎀','🌺','💎','🪷','🌟','🍀','🦢','🌻','🐚','🎐'];
          const _rnd=a=>a[Math.floor(Math.random()*a.length)];
          const ubases={chloe:['chloe','chloee','chlo','chloé','chloe'],lou:['lou','louu','lo','loux'],marie:['marie','mariee','mari','marye'],alex:['alex','alexx','alx','alexe'],random:['nova','luna','zara','ivy','mila','aria','nora']};
          const nbases={chloe:['Chloé','Chloe','Chloë','Chloea','Chloée'],lou:['Lou','Loue','Louée'],marie:['Marie','Mariée'],alex:['Alex','Alexa'],random:['Nova','Luna','Zara','Ivy','Mila']};
          const letters='abcdefghijklmnopqrstuvwxyz';
          const _rnd3=()=>letters[Math.floor(Math.random()*26)]+letters[Math.floor(Math.random()*26)]+letters[Math.floor(Math.random()*26)];
          const sty=d.style||'chloe';
          const ubase=_rnd(ubases[sty]||ubases.chloe);
          const username=ubase+'.'+_rnd3();
          const nbase=_rnd(nbases[sty]||nbases.chloe);
          const emoji=d.emoji==='non'?'':_rnd(_EMJS)+' ';
          const fullname=emoji+nbase;
          const password=(username.replace(/\.$/,''))+_rnd(_SYMS)+String(Math.floor(Math.random()*90+10));
          const bstart=new Date(2000,0,1).getTime();const bend=new Date(2007,11,31).getTime();
          const birthday=new Date(bstart+Math.random()*(bend-bstart)).toISOString().slice(0,10);
          ctx.username=username;ctx.password=password;ctx.name=fullname;ctx.birthday=birthday;
          const _saved={username,name:fullname,password,birthday};
          try{localStorage.setItem('plab_acct_'+node.id,JSON.stringify(_saved));}catch(_){}
          _setNodeResult(node.id,_saved);
          addLog('  👤 '+fullname+' (@'+username+')','#3fb950');
          addLog('  🔑 '+password+'  📅 '+birthday,'#3fb950');
          if(!live)await _sleep(300);
          break;}
        case'regen_username':{
          const _rnd2=a=>a[Math.floor(Math.random()*a.length)];
          const ubases2={chloe:['chloe','chloee','chlo','chloé','chloe'],lou:['lou','louu','lo','loux'],marie:['marie','mariee','mari','marye'],alex:['alex','alexx','alx','alexe'],random:['nova','luna','zara','ivy','mila','aria','nora']};
          const ltrs='abcdefghijklmnopqrstuvwxyz';
          const r3=()=>ltrs[Math.floor(Math.random()*26)]+ltrs[Math.floor(Math.random()*26)]+ltrs[Math.floor(Math.random()*26)];
          const sty2=d.style||'chloe';
          const newUsername=_rnd2(ubases2[sty2]||ubases2.chloe)+'.'+r3();
          ctx.username=newUsername;
          // Mettre à jour le localStorage du bloc gen_account pour synchroniser
          const _genNode=nodesRef.current.find(n=>n.type==='gen_account');
          if(_genNode){
            try{const _prev=JSON.parse(localStorage.getItem('plab_acct_'+_genNode.id)||'{}');
            const _upd={..._prev,username:newUsername};
            localStorage.setItem('plab_acct_'+_genNode.id,JSON.stringify(_upd));
            _setNodeResult(_genNode.id,_upd);}catch(_){}
          }
          _setNodeResult(node.id,{username:newUsername});
          addLog('  🔄 Nouveau username: @'+newUsername,'#3fb950');
          if(!live)await _sleep(300);
          break;}
        case'check_color':{
          const cx=parseFloat(d.x||50)/100;const cy=parseFloat(d.y||40)/100;
          const half=parseFloat(d.size||5)/100/2;
          const x1p=Math.max(0,cx-half);const y1p=Math.max(0,cy-half);
          const x2p=Math.min(1,cx+half);const y2p=Math.min(1,cy+half);
          const target=d.target||'green';
          addLog('  🔍 '+(d.label||'Vérif. couleur')+' — centre ('+Math.round(cx*100)+'%,'+Math.round(cy*100)+'%) taille '+Math.round(parseFloat(d.size||5))+'%','#e3b341');
          if(live){
            try{
              const _surl=B.screenUrl?B.screenUrl(udid,1,true):(window.Backend&&window.Backend.base?window.Backend.base:'')+'/api/devices/'+encodeURIComponent(udid)+'/screen?hq=1&ts='+Date.now();
              const _sr=await fetch(_surl,{cache:'no-store'});
              const _sblob=await _sr.blob();
              const b64=await new Promise(r=>{const fr=new FileReader();fr.onload=()=>r(fr.result.split(',')[1]||'');fr.onerror=()=>r('');fr.readAsDataURL(_sblob);});
              const shot={image:b64};
              if(b64){
                const result=await new Promise(resolve=>{
                  const img=new Image();
                  img.onload=()=>{
                    const cv=document.createElement('canvas');cv.width=img.width;cv.height=img.height;
                    const cx2=cv.getContext('2d');cx2.drawImage(img,0,0);
                    const rx=Math.floor(x1p*img.width),ry=Math.floor(y1p*img.height);
                    const rw=Math.max(1,Math.floor((x2p-x1p)*img.width)),rh=Math.max(1,Math.floor((y2p-y1p)*img.height));
                    const pixels=cx2.getImageData(rx,ry,rw,rh).data;
                    let gScore=0,rScore=0;
                    for(let i=0;i<pixels.length;i+=4){const r=pixels[i],g=pixels[i+1],b=pixels[i+2];if(g>r+40&&g>b+40)gScore++;else if(r>g+40&&r>b+40)rScore++;}
                    const total=pixels.length/4;
                    const gPct=gScore/total;const rPct=rScore/total;
                    addLog('  🔍 vert:'+Math.round(gPct*100)+'% rouge:'+Math.round(rPct*100)+'%','#8b949e');
                    resolve(target==='green'?gPct>0.05:rPct>0.05);
                  };
                  img.onerror=()=>resolve(false);
                  img.src='data:image/png;base64,'+b64;
                });
                ctx._branchResult=result;
                addLog(result?'  ✅ Condition vraie':'  ❌ Condition fausse',result?'#3fb950':'#f85149');
              } else { ctx._branchResult=false; addLog('  ⚠️ Pas de screenshot','#d29922'); }
            }catch(err){ctx._branchResult=false;addLog('  ⚠️ '+err.message,'#f85149');}
          } else {
            ctx._branchResult=Math.random()>0.5;
            addLog('  🔍 simulation → '+(ctx._branchResult?'✅ OUI':'❌ NON'),'#484f58');
          }
          break;}
        case'logic_ai_cond':{
          const question=await _ctx(d.prompt||'',ctx);
          addLog('  🤖 IA ? "'+question+'"','#8957e5');
          ctx._branchResult=false;
          if(live){
            try{
              const base=window.Backend&&window.Backend.base?window.Backend.base:'http://127.0.0.1:8787';
              const keysResp=await fetch(base+'/api/apikeys');
              const keysData=keysResp.ok?await keysResp.json():{};
              const geminiKey=(keysData.keys||{}).gemini||'';
              if(!geminiKey){addLog('  ❌ Clé Gemini manquante','#f85149');break;}
              const surl=base+'/api/devices/'+encodeURIComponent(udid)+'/screen?hq=1&ts='+Date.now();
              const rawB64=await new Promise(res=>{fetch(surl,{cache:'no-store'}).then(r=>r.blob()).then(b=>{const fr=new FileReader();fr.onload=()=>res(fr.result.split(',')[1]||'');fr.onerror=()=>res('');fr.readAsDataURL(b);}).catch(()=>res(''));});
              if(!rawB64){addLog('  ❌ Screenshot impossible','#f85149');break;}
              const prompt='Look at this iPhone screenshot carefully.\nQuestion: "'+question+'"\nAnswer ONLY with YES or NO. Do not explain.';
              const gr=await fetch('https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key='+geminiKey,{
                method:'POST',headers:{'Content-Type':'application/json'},
                body:JSON.stringify({contents:[{parts:[{text:prompt},{inline_data:{mime_type:'image/jpeg',data:rawB64}}]}],generationConfig:{maxOutputTokens:64,thinkingConfig:{thinkingBudget:0}}})
              });
              const gj=await gr.json();
              if(gj?.error)throw new Error(gj.error.message);
              const pts=gj?.candidates?.[0]?.content?.parts||[];
              const ans=((pts.find(p=>!p.thought)||pts[0]||{}).text||'').trim();
              addLog('  ← Gemini: "'+ans+'"','#8957e5');
              ctx._branchResult=/yes/i.test(ans);
              addLog(ctx._branchResult?'  ✅ OUI':'  ❌ NON',ctx._branchResult?'#3fb950':'#f85149');
            }catch(e){addLog('  ❌ IA condition: '+e.message,'#f85149');}
          } else {
            ctx._branchResult=true;
            addLog('  🤖 [sim] → OUI','#484f58');
          }
          break;}
        case'ai_2fa':{
          if(live){
            try{
              addLog('  🤖 Screenshot → Gemini → extraction clé TOTP…','#8957e5');
              const base=window.Backend&&window.Backend.base?window.Backend.base:'http://127.0.0.1:8787';
              const resp=await fetch(base+'/api/devices/'+encodeURIComponent(udid)+'/gemini-totp',{
                method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({})
              });
              const data=await resp.json();
              if(data.error){addLog('  ❌ '+data.error,'#f85149');break;}
              const rawKey=(data.totp_key||'').trim();
              // Normalisation stricte : majuscules, on garde uniquement les caractères base32 valides (A-Z, 2-7)
              const secretClean=rawKey.toUpperCase().replace(/[^A-Z2-7]/g,'');
              // On stocke la clé avec espaces toutes les 4 lettres pour la lisibilité (affichage)
              const secretDisplay=secretClean.match(/.{1,4}/g)?.join(' ')||secretClean;
              const secret=secretDisplay;
              if(data.reads&&data.reads.length){
                data.reads.forEach((r,i)=>addLog('  📖 Lecture '+(i+1)+': '+(r||'—'),'#484f58'));
              }
              addLog('  🔑 Vote final: '+rawKey,'#484f58');
              addLog('  ✅ Clé normalisée: '+secretClean+' ('+secretClean.length+' chars)','#8957e5');
              if(secretClean&&secretClean.length>=16){
                ctx['2fa_secret']=secretClean;
                const code=await _generateTOTP(secretClean);
                ctx['2fa_code']=code;
                _setNodeResult(node.id,{'2fa_secret':secretDisplay,'2fa_code':code});
                // transmet aussi au bloc gen_account pour qu'il soit visible sur le bloc info compte
                const genNode=nodesRef.current.find(n=>n.type==='gen_account');
                if(genNode){
                  _setNodeResult(genNode.id,{'2fa_secret':secret});
                  try{const _prev=JSON.parse(localStorage.getItem('plab_acct_'+genNode.id)||'{}');localStorage.setItem('plab_acct_'+genNode.id,JSON.stringify({..._prev,'2fa_secret':secret}));}catch(_){}
                }
                addLog('  🔐 Clé: '+secret,'#3fb950');
                addLog('  🔐 Code TOTP: '+code,'#3fb950');
              } else {
                addLog('  ⚠️ Clé TOTP non trouvée — vérifie que l\'écran 2FA Instagram est visible','#f85149');
              }
            }catch(err){addLog('  ⚠️ Erreur: '+err.message,'#f85149');}
          } else {
            ctx['2fa_secret']='JBSWY3DPEHPK3PXP';
            ctx['2fa_code']='123456';
            _setNodeResult(node.id,{'2fa_secret':'JBSWY3DP…','2fa_code':'123456'});
            addLog('  🤖 simulation → secret fictif','#484f58');
          }
          break;}
        case'farm_save_acct':{
          const _dev=(devicesRef.current&&devicesRef.current.find(d=>d.udid===udid))||{};
          const _px=_dev.proxy?proxies.find(p=>String(p.id)===String(_dev.proxy)):null;
          const acct={
            nom:ctx.name||'',
            username:ctx.username||'',
            password:ctx.password||'',
            totp_key:ctx['2fa_secret']||'',
            phone:'',
            birthday:'',
            category:'Compte en attente',
            operator:'Automatisation',
            device_udid:udid||'',
            device_tag:_dev.tag||udid||'',
            proxy_id:_dev.proxy||'',
            proxy_name:_px?_px.name:'',
            proxy_country:_px?(_px.country||''):'',
          };
          addLog('  💾 Sauvegarde: @'+acct.username+(acct.totp_key?' · 2FA✓':'')+'  ['+udid+']','#3fb950');
          try{
            const KEY='phonelabs.created_accounts';
            const all=JSON.parse(localStorage.getItem(KEY)||'[]');
            const uname=(acct.username||'').trim().toLowerCase();
            const dup=uname&&all.some(a=>(a.username||'').trim().toLowerCase()===uname);
            if(dup){
              addLog('  ⚠️ Doublon détecté (@'+acct.username+') — non ajouté','#d29922');
            } else if(typeof AccountCreationStore!=='undefined'&&AccountCreationStore.add){
              // Utilise le store pour déclencher emit() (rafraîchit graphique + compteurs)
              // On force temporairement __FARM_USER__ pour contrôler l'opérateur
              const _prev=window.__FARM_USER__;
              window.__FARM_USER__={name:'Automatisation'};
              AccountCreationStore.add({...acct,category:'Compte en attente'});
              window.__FARM_USER__=_prev;
              addLog('  ✅ Ajouté dans Stockage de comptes (catégorie: Compte en attente)','#3fb950');
            } else {
              // Fallback sans store
              all.unshift({...acct,id:String(performance.now()).replace('.',''),saved_at:new Date().toISOString(),operator:'Automatisation'});
              localStorage.setItem(KEY,JSON.stringify(all.slice(0,5000)));
              addLog('  ✅ Sauvegardé (graphique: rafraîchis la page)','#3fb950');
            }
          }catch(e){addLog('  ❌ Sauvegarde: '+e.message,'#f85149');}
          _setNodeResult(node.id,{username:acct.username,password:acct.password,name:acct.nom,'2fa_secret':acct.totp_key});
          break;}
        case'farm_assign_account':{
          const _tag=(devicesRef.current.find(x=>x.udid===udid)||{}).tag||'';
          const _src=d.category||'Compte en attente';
          const S=window.AccountCreationStore;
          if(!S||!S.attributeOldest){addLog('  ⚠️ Stockage comptes indisponible','#d29922');break;}
          const r=S.attributeOldest(udid,_tag,_src);
          if(r&&r.already)addLog('  🔗 iPhone déjà lié à @'+((r.account&&r.account.username)||'?')+' — rien à faire','#484f58');
          else if(r&&r.ok)addLog('  🔗 Compte @'+((r.account&&r.account.username)||'?')+' attribué à cet iPhone','#3fb950');
          else addLog('  ⚠️ '+((r&&r.error)||'attribution impossible'),'#d29922');
          break;}
        case'farm_account_info':{
          const S=window.AccountCreationStore;
          const _target=ctx.cloud_profile_id||udid;   // profil cloud attribué en amont, sinon le device du flux
          const acc=(S&&S.getAttributed)?S.getAttributed(_target):null;
          if(acc){
            if(acc.username)ctx.username=acc.username;
            if(acc.nom)ctx.name=acc.nom;
            if(acc.password)ctx.password=acc.password;
            if(acc.totp_key)ctx['2fa_secret']=acc.totp_key;
            _setNodeResult(node.id,{username:acc.username,password:acc.password,name:acc.nom,'2fa_secret':acc.totp_key});
            addLog('  📥 Compte attribué chargé : @'+(acc.username||'?')+(acc.totp_key?' · 2FA✓':''),'#3fb950');
          }else addLog('  ⚠️ Aucun compte attribué à cet iPhone (mets « Attribuer un compte » avant)','#d29922');
          break;}
        case'farm_set_category':{
          const _cat=d.category||'Warm Up';
          const S=window.AccountCreationStore;
          const r=(S&&S.setAttributedCategory)?S.setAttributedCategory(udid,_cat):null;
          if(r&&r.ok)addLog('  🏷️ Catégorie du compte attribué → '+_cat,'#3fb950');
          else addLog('  ⚠️ '+((r&&r.error)||'aucun compte attribué'),'#d29922');
          break;}
        case'farm_set_plug':{
          const _plug=(d.plug||'Plug')==='Plug';
          const S=window.AccountCreationStore;
          const r=(S&&S.setAttributedPlug)?S.setAttributedPlug(udid,_plug):null;
          if(r&&r.ok)addLog('  🔌 Compte attribué marqué → '+(_plug?'✅ Plug':'⬜ Pas plug'),'#3fb950');
          else addLog('  ⚠️ '+((r&&r.error)||'aucun compte attribué'),'#d29922');
          break;}
        case'content_spoof':{
          if(!udid){addLog('  ⚠️ Aucun profil sélectionné','#d29922');break;}
          const _pk=String(d.pack||'').replace(/^—.*—$/,'').trim();
          if(!_pk){addLog('  ⚠️ Choisis un pack source dans le bloc « Contenu spoofé »','#d29922');break;}
          const _mode={'Tout':'all','Minimal':'min','Quota':'quota'}[d.mode||'Quota']||'quota';
          const _q=parseInt(d.quota||20)||20;
          addLog('  🧬 Spoof « '+_pk+' » → dossier du profil ('+(d.mode||'Quota')+(_mode==='quota'?' '+_q:'')+')…','#c9a9ff');
          const rr=B.sourceDistribute?await B.sourceDistribute(_pk,[udid],_mode,_q,null):{ok:false,error:'indisponible'};
          if(rr&&rr.ok)addLog('  ✅ '+rr.total_made+' fichier(s) spoofé(s) déposé(s) dans le dossier du profil','#3fb950');
          else addLog('  ⚠️ '+((rr&&rr.error)||'échec du spoof'),'#d29922');
          await _sleep(1200); break;}
        case'media_send_reel':
        case'media_send_photo':
        case'media_send_pfp':{
          if(!udid){addLog('  ⚠️ Aucun téléphone sélectionné','#d29922');break;}
          const _kind=node.type==='media_send_reel'?'video':(node.type==='media_send_pfp'?'profile':'photo');
          const _tag=((devicesRef.current.find(x=>x.udid===udid)||{}).tag||'').replace(/^☁️\s*/,'').trim();
          addLog('  📤 Envoi média ('+_kind+') depuis le dossier du profil…','#1dbab4');
          const r=window.Backend.mediaSendFromFolder?await window.Backend.mediaSendFromFolder(udid,_tag,_kind):{ok:false,error:'indisponible'};
          if(r&&r.ok)addLog('  ✅ Média « '+r.name+' » envoyé (dossier '+r.sub+')','#3fb950');
          else addLog('  ⚠️ '+((r&&r.error)||'envoi impossible'),'#d29922');
          await _sleep(1500); break;}
        // ── ☁️ Blocs Phone Cloud ──
        case'cloud_power_on':{
          if(!udid){addLog('  ⚠️ Aucun cloud phone sélectionné','#d29922');break;}
          addLog('  🔋 Allumage du cloud phone…','#3b82f6');
          try{ await window.Backend.cloudphonePower(udid,true); }catch(e){}
          // ATTEND que le phone soit allumé + booté + connecté AVANT de passer au bloc suivant
          {const _t0=Date.now(); let _ready=false, _err=null;
           while(Date.now()-_t0 < 150000){   // jusqu'à 2,5 min (allumage + boot Android)
             let r=null; try{ r=await window.Backend.cloudphoneConnect(udid); }catch(e){}
             if(r&&r.state==='ready'){_ready=true;break;}
             if(r&&r.state==='error'){_err=r.msg||'échec';break;}
             await _sleep(3000);
           }
           if(_ready)addLog('  ✅ Cloud phone allumé et prêt','#3fb950');
           else if(_err)addLog('  ❌ Allumage impossible : '+_err,'#f85149');
           else addLog('  ⚠️ Timeout d\'allumage (>2,5 min)','#d29922');}
          break;}
        case'cloud_power_off':{
          if(!udid){addLog('  ⚠️ Aucun cloud phone sélectionné','#d29922');break;}
          addLog('  ⏻ Extinction du cloud phone…','#3b82f6');
          try{ await window.Backend.cloudphonePower(udid,false); }catch(e){}
          break;}
        case'cloud_app_switch':{
          addLog('  ▤ App Switcher (aperçu des apps)','#3b82f6');
          await B.action(udid,'recents',{}); await _sleep(800); break;}
        case'cloud_back':{
          addLog('  ◀ Retour','#3b82f6');
          await B.action(udid,'back',{}); await _sleep(500); break;}
        case'cloud_home':{
          addLog('  ⬤ Accueil','#3b82f6');
          await B.action(udid,'home',{}); await _sleep(500); break;}
        case'cloud_create':{
          const grp=d.group||'IG';
          addLog('  ➕ Création d\'un profil cloud (facturé)…','#f0a020');
          const src=(devicesRef.current.find(x=>x._cloud)||{}).udid||udid;
          const r=window.Backend.cloudphoneDuplicate?await window.Backend.cloudphoneDuplicate(src,d.name||'',grp):{ok:false};
          if(r.ok){addLog('  ✅ Profil « '+(r.name||'')+' » créé dans « '+grp+' »','#3fb950');_setNodeResult(node.id,{profile:r.name,profile_id:r.id||''});}
          else addLog('  ❌ Création: '+((r.error||r.msg)||'échec'),'#f85149');
          break;}
        case'cloud_rename':{
          if(!udid){addLog('  ⚠️ Aucun cloud phone sélectionné','#d29922');break;}
          if(!d.name){addLog('  ⚠️ Nouveau nom vide','#d29922');break;}
          const r=window.Backend.cloudphoneRename?await window.Backend.cloudphoneRename(udid,d.name):{ok:false};
          if(r.ok)addLog('  ✏️ Profil renommé → '+d.name,'#3fb950');
          else addLog('  ❌ Renommage: '+((r.error||r.msg)||'échec'),'#f85149');
          break;}
        case'cloud_assign_account':{
          const pool=d.pool||'Compte en attente';
          const acctCat=d.category||'Compte en attente';
          const S=window.AccountCreationStore;
          if(!S||!S.attributeOldest){addLog('  ⚠️ Stockage comptes indisponible','#d29922');break;}
          const usedIds=new Set((S.list?S.list():[]).map(a=>String(a.attributed_udid||'')).filter(Boolean));
          let phones=[];try{phones=await window.Backend.cloudphones();}catch(e){}
          let prof=(phones||[]).find(p=>p.group===pool && !usedIds.has(String(p.id)));
          if(!prof){
            addLog('  ➕ Aucun profil libre dans « '+pool+' » → création (facturé)…','#f0a020');
            const src=(phones&&phones[0]||{}).id;
            const cr=window.Backend.cloudphoneDuplicate?await window.Backend.cloudphoneDuplicate(src,'',pool):{ok:false};
            if(!cr.ok){addLog('  ❌ Création profil: '+((cr.error||cr.msg)||'échec'),'#f85149');break;}
            await _sleep(1800);
            try{phones=await window.Backend.cloudphones();}catch(e){}
            prof=(phones||[]).find(p=>p.group===pool && !usedIds.has(String(p.id)))||(cr.id?{id:cr.id,name:cr.name,group:pool}:null);
          }
          if(!prof){addLog('  ⚠️ Impossible d\'obtenir un profil','#d29922');break;}
          const r=S.attributeOldest(String(prof.id),prof.name||('☁️ '+prof.id),acctCat);
          if(r&&r.ok){
            ctx.cloud_profile_id=String(prof.id);   // -> lu par le bloc « Info compte attribué »
            addLog('  🔗 Compte @'+((r.account&&r.account.username)||'?')+' → profil « '+(prof.name||prof.id)+' » (ID '+prof.id+')','#3fb950');
            _setNodeResult(node.id,{profile:prof.name,profile_id:String(prof.id),username:(r.account&&r.account.username)});
          } else if(r&&r.already) addLog('  🔗 Profil déjà lié à @'+((r.account&&r.account.username)||'?'),'#484f58');
          else addLog('  ⚠️ '+((r&&r.error)||'attribution impossible'),'#d29922');
          break;}
        case'logic_end':{
          addLog('🔴 Arrêt forcé','#f85149');
          // Stoppe le run ET le batch si actif
          runningRef.current=false;
          if(batchRunningRef.current){
            batchRunningRef.current=false;setBatchRunning(false);
            addLog('⛔ Batch stoppé par bloc Arrêt forcé','#f85149');
          }
          break;}
        case'logic_cycle_end':{
          addLog('🔁 Fin de cycle — '+(batchRunningRef.current?'cooldown + prochain run':'run terminé'),'#3fb950');
          // Marque la fin de cycle comme succès (runningRef reste true → stopRun(true) dans doRun)
          break;}
        default: await _sleep(1000);
      }
    } else {
      await _sleep(node.type==='logic_wait'?Math.max(500,parseFloat(d.s||2)*800):node.type.startsWith('logic_')?300:950);
    }
  }

  async function _traverse(nodeId,visited,udid,live,ctx){
    if(!runningRef.current)return;
    const node=nodesRef.current.find(n=>n.id===nodeId);
    if(!node)return;
    // logic_loop: géré par loopCountersRef, pas par visited (pour permettre le re-passage)
    if(node.type==='logic_loop'){
      const n=Math.max(1,Math.min(100,parseInt(node.d.n||3)));
      const outEdges=edgesRef.current.filter(e=>e.f===nodeId);
      const bodyEdge=outEdges.find(e=>e.port===0)||outEdges[0];
      const doneEdge=outEdges.find(e=>e.port===1)||outEdges[1];
      const total=loopCountersRef.current[nodeId]||0;
      // Sécurité absolue anti-boucle infinie
      if(total>=n*20)return;
      const blk=AUTO_BLOCKS[node.type];
      for(let i=0;i<n;i++){
        if(!runningRef.current)break;
        loopCountersRef.current[nodeId]=(loopCountersRef.current[nodeId]||0)+1;
        setActiveNodeId(nodeId);setActiveNodeType(node.type);
        setBadge(blk?blk.e+' '+blk.l:node.type);
        addLog(autoLogMsg(node),AUTO_LOG_C[node.type]||'#8b949e');
        addLog('  🔁 Itération '+(i+1)+'/'+n,'#bc8cff');
        try{await _execBlock(node,udid,live,ctx);}catch(err){addLog('⚠️ '+(err?.message||String(err)),'#f85149');}
        if(!runningRef.current)break;
        if(bodyEdge)await _traverse(bodyEdge.t,new Set(visited),udid,live,ctx);
      }
      if(runningRef.current){addLog('  ✅ Boucle terminée ('+n+' itérations)','#3fb950');if(doneEdge)await _traverse(doneEdge.t,visited,udid,live,ctx);}
      return;
    }
    if(visited.has(nodeId))return;
    visited.add(nodeId);
    // Anti-boucle infinie : max 300 passages par nœud par run
    ctx._visits=ctx._visits||{};
    ctx._visits[nodeId]=(ctx._visits[nodeId]||0)+1;
    if(ctx._visits[nodeId]>300){addLog('⚠️ Boucle infinie — nœud "'+node.type+'" atteint 300× — arrêt','#f85149');return;}
    // blocs de données purement visuels → skip silencieux
    if(node.type==='data_context'||node.type==='data_ip'||node.type==='annot_note'||node.type==='annot_draw'){}
    else {
    const blk=AUTO_BLOCKS[node.type];
    await _waitIfPaused();
    if(!runningRef.current)return;
    setActiveNodeId(node.id);setActiveNodeType(node.type);
    setBadge((blk?blk.e+' '+blk.l:node.type));
    addLog(autoLogMsg(node),AUTO_LOG_C[node.type]||'#8b949e');
    try{ await _execBlock(node,udid,live,ctx); }
    catch(err){ addLog('⚠️ '+(err&&err.message||String(err)),'#f85149'); }
    }
    if(!runningRef.current)return;
    const outEdges=edgesRef.current.filter(e=>e.f===nodeId);
    if(!outEdges.length)return;
    if(node.type==='logic_random'){
      // choisir 1 sortie au hasard
      const chosen=outEdges[Math.floor(Math.random()*outEdges.length)];
      addLog('  🎲 → sortie #'+(outEdges.indexOf(chosen)+1),'#f78166');
      await _traverse(chosen.t,visited,udid,live,ctx);
    } else if(node.type==='logic_split'){
      // toutes les sorties en séquence
      addLog('  ⑃ → '+outEdges.length+' branches','#bc8cff');
      for(const e of outEdges){
        if(!runningRef.current)break;
        await _traverse(e.t,new Set(visited),udid,live,ctx);
      }
    } else if(node.type==='wda_tap_router'){
      const labels=_parseLabels(node.d.labels);
      const routedPort=ctx._routerPort!=null?ctx._routerPort:labels.length; // default = port après les labels
      delete ctx._routerPort;
      const edge=outEdges.find(e=>e.port===routedPort)||(routedPort>=labels.length?outEdges.find(e=>e.port===labels.length):null);
      if(edge){
        const label=labels[routedPort]||'par défaut';
        addLog('  🔀 → '+label,'#8957e5');
        await _traverse(edge.t,visited,udid,live,ctx);
      } else {
        // Pas de port default connecté → on continue quand même sur le premier edge disponible
        const fallback=outEdges[outEdges.length-1];
        if(fallback){addLog('  ↪ Aucun match → fallback','#d29922');await _traverse(fallback.t,visited,udid,live,ctx);}
        else addLog('  ↪ Aucun bloc connecté — routage ignoré','#d29922');
      }
    } else if(AUTO_BLOCKS[node.type]&&AUTO_BLOCKS[node.type].branch){
      const branchTrue=ctx._branchResult!==false;
      const trueEdge=outEdges.find(e=>e.port===0)||outEdges[0];
      const falseEdge=outEdges.find(e=>e.port===1)||outEdges[1]||outEdges[0];
      const edge=branchTrue?trueEdge:falseEdge;
      // Toujours démarrer avec un visited vide pour les branches → permet les boucles
      // La protection anti-infini est assurée par ctx._visits (max 300/nœud)
      if(edge) await _traverse(edge.t,new Set(),udid,live,ctx);
    } else {
      await _traverse(outEdges[0].t,visited,udid,live,ctx);
    }
  }

  async function doRun(fromId){
    if(runningRef.current)return;
    if(!batchRunningRef.current&&!_acquireProxy())return; // hors batch : réserve le proxy
    loopCountersRef.current={};
    runningRef.current=true;setRunning(true);setBadge('En cours…');setLogs(prev=>[...prev,{msg:'── Run '+(new Date().toLocaleTimeString())+'  ──',color:'#388bfd',id:Date.now(),separator:true}]);
    const udid=deviceId;
    const live=!!(udid&&window.Backend&&window.Backend.action);
    // contexte partagé — se remplit au fur et à mesure de l'exécution
    // Pré-charge les infos du dernier gen_account sauvegardé pour que {{password}} etc. fonctionnent
    const ctx={phone:'',sms_code:'',username:'',password:'',name:'',birthday:'','2fa_secret':'','2fa_code':'',_heroNumId:''};
    // Restaure les données persistées de chaque bloc depuis nodeResults (localStorage)
    const _nr=nodeResultsRef.current;
    nodesRef.current.forEach(n=>{
      const r=_nr[n.id];if(!r)return;
      if(n.type==='gen_account')Object.assign(ctx,{username:r.username||ctx.username,password:r.password||ctx.password,name:r.name||ctx.name,birthday:r.birthday||ctx.birthday,'2fa_secret':r['2fa_secret']||ctx['2fa_secret']});
      if(n.type==='farm_account_info'){ // 2e priorité (gen_account l'emporte) : remplit seulement les champs vides
        if(!ctx.username&&r.username)ctx.username=r.username;
        if(!ctx.password&&r.password)ctx.password=r.password;
        if(!ctx.name&&r.name)ctx.name=r.name;
        if(!ctx['2fa_secret']&&r['2fa_secret'])ctx['2fa_secret']=r['2fa_secret'];
      }
      if(n.type==='farm_buy_number'&&r.numId&&!ctx._heroNumId){ctx._heroNumId=r.numId;ctx.phone=(r.phone||'').replace('+1','');}
      if((n.type==='ai_2fa'||n.type==='gen_account')&&r['2fa_secret'])ctx['2fa_secret']=r['2fa_secret'];
      if(n.type==='farm_wait_sms'&&r.code)ctx.sms_code=r.code;
    });
    addLog('▶ Lancement'+(fromId?' depuis bloc':'')+(live?'':' · simulation'),'#3fb950');
    if(!live)addLog('ℹ️ Sélectionne un iPhone pour exécuter réellement','#d29922');
    const start=fromId?nodesRef.current.find(n=>n.id===fromId):nodesRef.current.find(n=>n.type==='trigger_manual');
    if(!start){addLog('⚠️ Aucun bloc déclencheur / départ introuvable','#d29922');stopRun(false);return;}
    await _traverse(start.id,new Set(),udid,live,ctx);
    stopRun(runningRef.current);
  }
  async function doRunSingle(nodeId){
    if(runningRef.current)return;
    if(!_acquireProxy())return; // réserve le proxy avant d'exécuter un bloc seul
    loopCountersRef.current={};
    const node=nodesRef.current.find(n=>n.id===nodeId);
    if(!node)return;
    runningRef.current=true;setRunning(true);setBadge('En cours…');setLogs(prev=>[...prev,{msg:'── Run '+(new Date().toLocaleTimeString())+'  ──',color:'#388bfd',id:Date.now(),separator:true}]);
    const udid=deviceId;
    const live=!!(udid&&window.Backend&&window.Backend.action);
    const ctx={phone:'',sms_code:'',username:'',password:'',name:'',birthday:'','2fa_secret':'','2fa_code':'',_heroNumId:''};
    // Restaure les données persistées de chaque bloc depuis nodeResults (localStorage)
    const _nr=nodeResultsRef.current;
    nodesRef.current.forEach(n=>{
      const r=_nr[n.id];if(!r)return;
      if(n.type==='gen_account')Object.assign(ctx,{username:r.username||ctx.username,password:r.password||ctx.password,name:r.name||ctx.name,birthday:r.birthday||ctx.birthday,'2fa_secret':r['2fa_secret']||ctx['2fa_secret']});
      if(n.type==='farm_account_info'){ // 2e priorité (gen_account l'emporte) : remplit seulement les champs vides
        if(!ctx.username&&r.username)ctx.username=r.username;
        if(!ctx.password&&r.password)ctx.password=r.password;
        if(!ctx.name&&r.name)ctx.name=r.name;
        if(!ctx['2fa_secret']&&r['2fa_secret'])ctx['2fa_secret']=r['2fa_secret'];
      }
      if(n.type==='farm_buy_number'&&r.numId&&!ctx._heroNumId){ctx._heroNumId=r.numId;ctx.phone=(r.phone||'').replace('+1','');}
      if((n.type==='ai_2fa'||n.type==='gen_account')&&r['2fa_secret'])ctx['2fa_secret']=r['2fa_secret'];
      if(n.type==='farm_wait_sms'&&r.code)ctx.sms_code=r.code;
    });
    const blk=AUTO_BLOCKS[node.type];
    addLog('▶ Bloc seul : '+(blk?blk.e+' '+blk.l:node.type)+(live?'':' · simulation'),'#58a6ff');
    setActiveNodeId(node.id);setActiveNodeType(node.type);
    setBadge(blk?blk.e+' '+blk.l:node.type);
    addLog(autoLogMsg(node),AUTO_LOG_C[node.type]||'#8b949e');
    try{ await _execBlock(node,udid,live,ctx); }
    catch(err){ addLog('⚠️ '+(err&&err.message||String(err)),'#f85149'); }
    stopRun(true);
  }
  function stopRun(ok){
    lastRunOkRef.current=ok;
    if(runTimerRef.current)clearTimeout(runTimerRef.current);
    runningRef.current=false;pausedRef.current=false;setPaused(false);
    setRunning(false);setActiveNodeId(null);setActiveNodeType(null);
    setBadge(ok?'✓ Terminé !':'⛔ Arrêté');if(!ok)addLog('⛔ Arrêté','#f85149');
    // Libère le proxy sauf si un batch est encore en cours (il le garde entre les runs)
    if(!batchRunningRef.current)releaseProxy(_proxyFor(deviceId));
  }

  function _fmtDur(ms){const s=Math.round(ms/1000);return s<60?s+'s':Math.floor(s/60)+'m'+((s%60)?''+(s%60)+'s':'');}

  function playSound(type){
    if(!soundEnabledRef.current)return;
    if(window.SoundBoard)window.SoundBoard.play(type);
  }

  async function startBatch(){
    if(batchRunning||running)return;
    if(!_acquireProxy())return; // réserve le proxy pour toute la durée du batch
    batchRunningRef.current=true;setBatchRunning(true);
    const total=Math.max(1,parseInt(batchCount)||1);
    const cooldown=Math.max(0,parseInt(batchCooldown)||0);
    const startTs=Date.now();
    const runTimes=[];
    setBatchProgress({cur:0,total,runTimes:[],start:startTs});
    addLog('🔄 Batch démarré — '+total+' run(s), cooldown '+cooldown+'s','#8957e5');
    for(let i=0;i<total;i++){
      if(!batchRunningRef.current)break;
      const runStart=Date.now();
      setBatchProgress({cur:i+1,total,runTimes:[...runTimes],start:startTs});
      addLog('▶ Batch run '+(i+1)+'/'+total,'#8957e5');
      playSound('start');
      lastRunOkRef.current=null;
      await doRun(null);
      const ok=lastRunOkRef.current;
      const dur=Date.now()-runStart;
      runTimes.push({ok,dur});
      setBatchProgress({cur:i+1,total,runTimes:[...runTimes],start:startTs});
      playSound(ok!==false?'success':'error');
      if(i<total-1&&batchRunningRef.current){
        addLog('⏳ Cooldown '+cooldown+'s…','#d29922');
        for(let t=0;t<cooldown*10;t++){
          if(!batchRunningRef.current)break;
          await _sleep(100);
        }
      }
    }
    if(batchRunningRef.current){
      const totalMs=Date.now()-startTs;
      addLog('✅ Batch terminé — '+runTimes.length+' runs en '+_fmtDur(totalMs),'#3fb950');
      playSound('batch_done');
    }
    batchRunningRef.current=false;setBatchRunning(false);
    setBatchProgress(prev=>prev?{...prev,done:true}:null);
    releaseProxy(_proxyFor(deviceId)); // fin du batch → libère le proxy
  }

  function stopBatch(){
    batchRunningRef.current=false;setBatchRunning(false);
    if(running)stopRun(false);
    releaseProxy(_proxyFor(deviceId)); // arrêt manuel → libère le proxy
    addLog('⛔ Batch arrêté','#f85149');
  }

  function startSbResize(e){e.preventDefault();resizeRef.current={side:'sb',startX:e.clientX,initW:sbWidth};}
  function startRpResize(e){e.preventDefault();resizeRef.current={side:'rp',startX:e.clientX,initW:rpWidth};}

  const jsonStr=useMemo(()=>{
    const order=autoGetOrder(nodesRef.current,edgesRef.current);
    return JSON.stringify({name:flowName,device:deviceId||'iPhone #2',flow:order.map(n=>({type:n.type,...n.d}))},null,2);
  },[nodes,edges,flowName,deviceId]);

  return(
    <div style={{position:'fixed',inset:0,zIndex:200,display:'flex',flexDirection:'column',background:'#0d1117',fontFamily:'inherit'}}
      onClick={()=>ctxMenu&&setCtxMenu(null)}>
      {/* ── Topbar ── */}
      <div style={{display:'flex',alignItems:'center',gap:7,padding:'0 12px',background:'#161b22',borderBottom:'1px solid #21262d',height:44,flex:'none'}}>
        {onBack&&(<>
          <button onClick={()=>onBack()}
            style={{background:'none',border:'none',color:'#8b949e',cursor:'pointer',fontSize:12,padding:'4px 8px',borderRadius:6,display:'flex',alignItems:'center',gap:4,whiteSpace:'nowrap'}}
            onMouseEnter={e=>e.currentTarget.style.background='#21262d'}
            onMouseLeave={e=>e.currentTarget.style.background='none'}>
            ← Retour
          </button>
          <div style={{width:1,height:20,background:'#30363d',flex:'none'}}></div>
        </>)}
        <input value={flowName} onChange={e=>setFlowName(e.target.value)}
          style={{background:'none',border:'none',color:'#e6edf3',fontSize:13,fontWeight:700,outline:'none',padding:'4px 8px',borderRadius:6,minWidth:100}}
          onFocus={e=>e.currentTarget.style.background='#21262d'}
          onBlur={e=>e.currentTarget.style.background='none'} />
        <div style={{width:1,height:20,background:'#30363d',flex:'none'}}></div>
        <select value={deviceId} onChange={e=>{const v=e.target.value;if(v&&deviceLockedByOther(v)){setProxyBlock({deviceBusy:true,tried:_labelFor(v)});return;}setDeviceId(v);try{localStorage.setItem('pl.auto.dev',v);}catch(e){}}}
          style={{background:'#21262d',border:'1px solid #30363d',borderRadius:6,color:'#e6edf3',fontSize:11.5,padding:'4px 8px',cursor:'pointer',flex:'none'}}>
          <option value=''>📱 Sélectionner iPhone…</option>
          {devices.length>0
            ?devices.map(d=>{
                const lk=proxyLockedByOther(d.proxy);       // proxy occupé par une autre fenêtre ?
                const dlk=deviceLockedByOther(d.udid);       // iPhone déjà ouvert dans une autre fenêtre ?
                const proxyBusy=!!lk&&d.udid!==deviceId;
                const devBusy=!!dlk&&d.udid!==deviceId;
                const busy=proxyBusy||devBusy;
                return <option key={d.udid} value={d.udid} disabled={busy}>
                  📱 {d.tag||d.name}{devBusy?' 🔒 ouvert ailleurs':(proxyBusy?' 🔒 proxy occupé':'')}
                </option>;
              })
            :[<option key='x' value='iphonex'>📱 iPhone #2 — iphonex</option>]
          }
        </select>
        {/* Badge du proxy de l'iPhone sélectionné */}
        {deviceId&&(()=>{
          const sd=devices.find(d=>d.udid===deviceId);
          // Cloud phone : proxy MoreLogin (porté par le device), affiché direct.
          if(sd&&sd._cloud){
            const has=!!sd.proxyName;
            const flag=has?(AUTO_FLAG[sd.proxyCountry]||''):'';
            return(
              <div title={has?'Proxy MoreLogin du cloud phone (rotation IP dessus)':'Ce cloud phone n\'a pas de proxy dans MoreLogin'}
                style={{display:'flex',alignItems:'center',gap:5,padding:'4px 10px',borderRadius:6,flex:'none',whiteSpace:'nowrap',fontSize:11,fontWeight:700,
                  background:has?'rgba(59,130,246,.14)':'rgba(139,148,158,.1)',
                  border:'1px solid '+(has?'rgba(59,130,246,.4)':'#30363d'),
                  color:has?'#60a5fa':'#8b949e'}}>
                🔌 {flag?flag+' ':''}{has?sd.proxyName:'Aucun proxy'}
              </div>
            );
          }
          const pid=sd&&sd.proxy;
          const px=pid&&proxies.find(p=>String(p.id)===String(pid));
          const flag=px?(AUTO_FLAG[px.country||autoFlagFromHost(px.host)]||''):'';
          return(
            <div onClick={()=>px&&setProxyInfo(px)}
              title={px?'Voir les infos du proxy':'Aucun proxy attribué à cet iPhone'}
              style={{display:'flex',alignItems:'center',gap:5,padding:'4px 10px',borderRadius:6,flex:'none',whiteSpace:'nowrap',fontSize:11,fontWeight:700,cursor:px?'pointer':'default',
                background:px?'rgba(29,186,180,.12)':'rgba(139,148,158,.1)',
                border:'1px solid '+(px?'rgba(29,186,180,.35)':'#30363d'),
                color:px?'#1dbab4':'#8b949e'}}>
              🔌 {flag?flag+' ':''}{px?px.name:'Aucun proxy'}{px?' ▾':''}
            </div>
          );
        })()}
        {proxyInfo&&(()=>{
          const px=proxyInfo;
          const flag=AUTO_FLAG[px.country||autoFlagFromHost(px.host)]||'';
          const rows=[
            ['Serveur : Port (IP)', (px.host||'')+(px.port?':'+px.port:''), 'hp'],
            ['Utilisateur', px.username, 'user'],
            ['Mot de passe', px.password, 'pass'],
            ['URL de rotation IP', px.url, 'url'],
          ].filter(r=>r[1]);
          return(
            <div onClick={()=>setProxyInfo(null)}
              style={{position:'fixed',inset:0,background:'rgba(1,4,9,.7)',zIndex:99999,display:'flex',alignItems:'center',justifyContent:'center',padding:20}}>
              <div onClick={e=>e.stopPropagation()}
                style={{background:'#161b22',border:'1px solid #1dbab4',borderRadius:14,padding:20,width:'min(400px, 100%)',boxSizing:'border-box',maxHeight:'88vh',overflowX:'hidden',overflowY:'auto',boxShadow:'0 12px 48px rgba(0,0,0,.6)'}}>
                <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:4,fontSize:15,fontWeight:800,color:'#e6edf3'}}>
                  <span>🔌 {flag?flag+' ':''}{px.name}</span>
                </div>
                <div style={{fontSize:11.5,color:'#8b949e',marginBottom:14}}>Infos du proxy de cet iPhone — clique pour copier.</div>
                <div style={{display:'grid',gap:8}}>
                  {rows.map(([lbl,val,key])=>{
                    const isPass=key==='pass';
                    const shown=isPass?(showProxyPass?val:'•'.repeat(Math.min(14,String(val).length))):val;
                    return(
                    <div key={key} style={{display:'flex',alignItems:'center',gap:8,padding:'9px 11px',background:'#0d1117',border:'1px solid #21262d',borderRadius:9,minWidth:0}}>
                      <div style={{minWidth:0,flex:1,overflow:'hidden'}}>
                        <div style={{fontSize:10,color:'#484f58',fontWeight:700,textTransform:'uppercase',letterSpacing:'.04em'}}>{lbl}</div>
                        <div style={{fontSize:12.5,color:'#e6edf3',fontFamily:(key==='url'||isPass)?'monospace':'inherit',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',maxWidth:'100%'}}>{shown}</div>
                      </div>
                      {isPass&&<button onClick={()=>setShowProxyPass(v=>!v)} data-sfx="none" title={showProxyPass?'Masquer':'Afficher'}
                        style={{flex:'none',padding:'5px 8px',borderRadius:7,border:'1px solid #30363d',background:'#21262d',color:'#8b949e',fontSize:13,cursor:'pointer'}}>{showProxyPass?'🙈':'👁'}</button>}
                      <button onClick={()=>_copyProxy(val,key)} data-sfx="none"
                        style={{flex:'none',padding:'5px 11px',borderRadius:7,border:'1px solid #30363d',background:proxyCopied===key?'rgba(29,186,180,.15)':'#21262d',color:proxyCopied===key?'#1dbab4':'#8b949e',fontSize:11.5,fontWeight:700,cursor:'pointer'}}>
                        {proxyCopied===key?'Copié ✓':'Copier'}</button>
                    </div>
                    );
                  })}
                </div>
                <button onClick={()=>setProxyInfo(null)}
                  style={{marginTop:16,width:'100%',padding:'9px 0',background:'#21262d',border:'1px solid #30363d',borderRadius:9,color:'#e6edf3',fontWeight:700,fontSize:12.5,cursor:'pointer'}}>Fermer</button>
              </div>
            </div>
          );
        })()}
        {proxyBlock&&(
          <div onClick={()=>setProxyBlock(null)}
            style={{position:'fixed',inset:0,background:'rgba(1,4,9,.7)',zIndex:99999,display:'flex',alignItems:'center',justifyContent:'center',padding:20}}>
            <div onClick={e=>e.stopPropagation()}
              style={{background:'#161b22',border:'1px solid #f85149',borderRadius:14,padding:'26px 24px',maxWidth:410,textAlign:'center',boxShadow:'0 12px 48px rgba(0,0,0,.6)'}}>
              <div style={{fontSize:36,marginBottom:10}}>{proxyBlock.deviceBusy?'📱🔒':'🔌🔒'}</div>
              {proxyBlock.deviceBusy
                ?<>
                  <div style={{fontSize:15,fontWeight:800,color:'#e6edf3',marginBottom:10}}>iPhone déjà ouvert dans une autre fenêtre</div>
                  <div style={{fontSize:12.5,color:'#8b949e',lineHeight:1.55,marginBottom:20}}>
                    <b style={{color:'#e6edf3'}}>{proxyBlock.tried}</b> est déjà piloté dans une autre fenêtre. Pour éviter les confusions, un iPhone ne peut être ouvert que dans <b style={{color:'#e6edf3'}}>une seule fenêtre à la fois</b>.<br/>
                    Ferme l'autre fenêtre, ou choisis un autre iPhone.
                  </div>
                </>
                :<>
                  <div style={{fontSize:15,fontWeight:800,color:'#e6edf3',marginBottom:10}}>Proxy déjà actif dans une autre fenêtre</div>
                  <div style={{fontSize:12.5,color:'#8b949e',lineHeight:1.55,marginBottom:20}}>
                    Le proxy <b style={{color:'#1dbab4'}}>{String(proxyBlock.proxy).slice(0,18)}</b> est déjà utilisé par <b style={{color:'#e6edf3'}}>{proxyBlock.taken}</b> dans une autre fenêtre.<br/>
                    Impossible de lancer <b style={{color:'#e6edf3'}}>{proxyBlock.tried}</b> sur le même proxy en même temps (même IP → interférences). Choisis un iPhone avec un <b style={{color:'#e6edf3'}}>autre proxy</b>.
                  </div>
                </>}
              <button onClick={()=>setProxyBlock(null)}
                style={{padding:'9px 26px',background:'#21262d',border:'1px solid #30363d',borderRadius:9,color:'#e6edf3',fontWeight:700,fontSize:12.5,cursor:'pointer'}}>Compris</button>
            </div>
          </div>
        )}
        <div style={{width:1,height:20,background:'#30363d',flex:'none'}}></div>
        <button onClick={doUndo} title="Annuler (Ctrl+Z)"
          style={{padding:'5px 10px',background:'#21262d',border:'1px solid #30363d',borderRadius:7,color:'#e6edf3',fontWeight:700,fontSize:13,cursor:'pointer',flex:'none',opacity:canUndo?1:0.35}}>
          ↩
        </button>
        <button onClick={doRedo} title="Rétablir (Ctrl+Y)"
          style={{padding:'5px 10px',background:'#21262d',border:'1px solid #30363d',borderRadius:7,color:'#e6edf3',fontWeight:700,fontSize:13,cursor:'pointer',flex:'none',opacity:canRedo?1:0.35}}>
          ↪
        </button>
        <div style={{width:1,height:20,background:'#30363d',flex:'none'}}></div>
        {/* ── Bouton dessin + barre outils ── */}
        <button onClick={()=>{const next=!drawModeRef.current;drawModeRef.current=next;setDrawMode(next);setDrawEraser(false);}} title={drawMode?'Quitter le mode dessin (Esc)':'Mode dessin libre'}
          style={{padding:'5px 10px',background:drawMode?'#388bfd':'#21262d',border:'1px solid '+(drawMode?'#388bfd':'#30363d'),borderRadius:7,color:drawMode?'#fff':'#8b949e',fontWeight:700,fontSize:13,cursor:'pointer',flex:'none'}}>
          ✏️
        </button>
        {drawMode&&(<>
          {['#f78166','#ffa657','#e3b341','#3fb950','#79c0ff','#bc8cff','#e6edf3','#484f58'].map(c=>(
            <div key={c} onClick={()=>{setDrawColor(c);setDrawEraser(false);}}
              style={{width:16,height:16,borderRadius:'50%',background:c,cursor:'pointer',flex:'none',border:drawColor===c&&!drawEraser?'2px solid #fff':'2px solid transparent',boxSizing:'border-box'}} />
          ))}
          <div style={{display:'flex',gap:3,flex:'none'}}>
            {[2,4,8].map(s=>(
              <div key={s} onClick={()=>{setDrawSize(s);setDrawEraser(false);}}
                style={{width:16,height:16,borderRadius:4,background:drawSize===s&&!drawEraser?'#388bfd':'#21262d',border:'1px solid #30363d',cursor:'pointer',display:'flex',alignItems:'center',justifyContent:'center'}}>
                <div style={{width:s,height:s,borderRadius:'50%',background:'#e6edf3'}} />
              </div>
            ))}
          </div>
          <button onClick={()=>setDrawEraser(e=>!e)} title="Gomme"
            style={{padding:'3px 8px',background:drawEraser?'#6e7681':'#21262d',border:'1px solid #30363d',borderRadius:6,color:'#e6edf3',fontSize:12,cursor:'pointer',flex:'none'}}>
            🧹
          </button>
          <button onClick={clearDraw} title="Effacer tout le dessin"
            style={{padding:'3px 8px',background:'#21262d',border:'1px solid #30363d',borderRadius:6,color:'#f85149',fontSize:11,cursor:'pointer',flex:'none'}}>
            🗑 Effacer
          </button>
        </>)}
        <div style={{width:1,height:20,background:'#30363d',flex:'none'}}></div>
        <button onClick={()=>doRun(null)}
          style={{padding:'5px 13px',background:running?'#21262d':'#238636',border:'none',borderRadius:7,color:running?'#8b949e':'#fff',fontWeight:700,fontSize:12,cursor:running?'default':'pointer',flex:'none'}}>
          ▶ Lancer
        </button>
        {running&&(
          <button onClick={()=>{const next=!pausedRef.current;pausedRef.current=next;setPaused(next);addLog(next?'⏸ Pause':'▶ Reprise',next?'#d29922':'#3fb950');}}
            style={{padding:'5px 13px',background:paused?'#388bfd':'#d29922',border:'none',borderRadius:7,color:'#fff',fontWeight:700,fontSize:12,cursor:'pointer',flex:'none'}}>
            {paused?'▶ Reprendre':'⏸ Pause'}
          </button>
        )}
        <button onClick={()=>stopRun(false)}
          style={{padding:'5px 13px',background:running?'#da3633':'#21262d',border:'1px solid #30363d',borderRadius:7,color:running?'#fff':'#8b949e',fontWeight:700,fontSize:12,cursor:'pointer',flex:'none'}}>
          ■ Stop
        </button>
        <button onClick={()=>{
            const data=JSON.stringify({name:flowName,nodes:nodesRef.current,edges:edgesRef.current},null,2);
            const a=document.createElement('a');
            a.href='data:application/json;charset=utf-8,'+encodeURIComponent(data);
            a.download=(flowName||'flux').replace(/[^a-z0-9]/gi,'_').toLowerCase()+'.json';
            a.click();
          }}
          style={{padding:'5px 13px',background:'#21262d',border:'1px solid #30363d',borderRadius:7,color:'#bc8cff',fontWeight:700,fontSize:12,cursor:'pointer',flex:'none'}}>
          ↑ Exporter
        </button>
        <button onClick={()=>{
            const inp=document.createElement('input');
            inp.type='file';inp.accept='.json,application/json';
            inp.onchange=()=>{
              const f=inp.files[0];if(!f)return;
              const r=new FileReader();
              r.onload=(ev)=>{
                try{
                  const d=JSON.parse(ev.target.result);
                  if(!Array.isArray(d.nodes)||!Array.isArray(d.edges))throw new Error('Format invalide');
                  if(d.name)setFlowName(d.name);
                  nodesRef.current=d.nodes;edgesRef.current=d.edges;
                  setNodes([...d.nodes]);setEdges([...d.edges]);
                  if(svgRef.current)svgRef.current.innerHTML='';
                  setTimeout(()=>updateSVG(),80);
                }catch(e){alert('Fichier invalide : '+e.message);}
              };
              r.readAsText(f);
            };
            inp.click();
          }}
          style={{padding:'5px 13px',background:'#21262d',border:'1px solid #30363d',borderRadius:7,color:'#bc8cff',fontWeight:700,fontSize:12,cursor:'pointer',flex:'none'}}>
          ↓ Importer
        </button>
        <button onClick={clearCanvas}
          style={{padding:'5px 13px',background:'#21262d',border:'1px solid #30363d',borderRadius:7,color:'#f85149',fontWeight:700,fontSize:12,cursor:'pointer',flex:'none'}}>
          🗑 Vider
        </button>
        <span style={{fontSize:9.5,color:'#484f58',marginLeft:4,whiteSpace:'nowrap',flex:'none'}}>Molette = zoom · Clic droit = options</span>

        {/* ── Bouton Focus ── */}
        <button onClick={()=>{const n=!focusActiveRef.current;focusActiveRef.current=n;setFocusActive(n);}}
          title={focusActive?'Désactiver le suivi automatique':'Activer le suivi : le canvas se centre sur le bloc actif'}
          style={{marginLeft:8,padding:'4px 10px',background:focusActive?'rgba(56,139,253,.18)':'#0d1117',
            border:'1px solid '+(focusActive?'#388bfd':'#30363d'),borderRadius:6,
            color:focusActive?'#58a6ff':'#484f58',fontSize:11.5,fontWeight:focusActive?700:400,
            cursor:'pointer',display:'flex',alignItems:'center',gap:5,flex:'none',transition:'all .15s'}}>
          👁 <span style={{fontSize:10}}>Focus</span>
        </button>

        {/* ── Batch controls (topbar) ── */}
        <div style={{marginLeft:'auto',display:'flex',alignItems:'center',gap:5,flex:'none',paddingLeft:8}}>
          <div style={{width:1,height:20,background:'#30363d'}} />
          {!batchRunning?(
            <>
              <div style={{display:'flex',flexDirection:'column',gap:1}}>
                <div style={{fontSize:7.5,color:'#484f58',textAlign:'center',lineHeight:1}}>Runs</div>
                <input type="number" min="1" max="999" value={batchCount}
                  onChange={e=>setBatchCount(e.target.value)}
                  style={{width:42,background:'#0d1117',border:'1px solid #30363d',borderRadius:4,color:'#e6edf3',fontSize:11,padding:'2px 4px',textAlign:'center'}} />
              </div>
              <div style={{display:'flex',flexDirection:'column',gap:1}}>
                <div style={{fontSize:7.5,color:'#484f58',textAlign:'center',lineHeight:1}}>Cooldown(s)</div>
                <input type="number" min="0" max="3600" value={batchCooldown}
                  onChange={e=>setBatchCooldown(e.target.value)}
                  style={{width:46,background:'#0d1117',border:'1px solid #30363d',borderRadius:4,color:'#e6edf3',fontSize:11,padding:'2px 4px',textAlign:'center'}} />
              </div>
              <button onClick={()=>{soundEnabledRef.current=!soundEnabledRef.current;setSoundEnabled(v=>!v);}}
                style={{padding:'3px 6px',background:'#0d1117',border:'1px solid #30363d',borderRadius:4,color:soundEnabled?'#e6edf3':'#484f58',fontSize:12,cursor:'pointer',lineHeight:1}}>
                {soundEnabled?'🔊':'🔇'}
              </button>
              <button onClick={startBatch}
                style={{padding:'4px 11px',background:'#8957e5',border:'none',borderRadius:6,color:'#fff',fontWeight:700,fontSize:11.5,cursor:'pointer',whiteSpace:'nowrap'}}>
                🔄 ×{batchCount}
              </button>
            </>
          ):(
            <>
              {batchProgress&&(
                <div style={{fontSize:10,color:'#8957e5',fontWeight:700,whiteSpace:'nowrap'}}>
                  {batchProgress.cur}/{batchProgress.total}
                  {batchPaused&&<span style={{color:'#d29922'}}> ⏸</span>}
                </div>
              )}
              <button onClick={()=>{const n=!batchPausedRef.current;batchPausedRef.current=n;setBatchPaused(n);addLog(n?'⏸ Batch pause':'▶ Batch repris',n?'#d29922':'#3fb950');}}
                style={{padding:'4px 9px',background:batchPaused?'#388bfd':'#d29922',border:'none',borderRadius:6,color:'#fff',fontWeight:700,fontSize:11,cursor:'pointer'}}>
                {batchPaused?'▶':'⏸'}
              </button>
              <button onClick={stopBatch}
                style={{padding:'4px 9px',background:'#da3633',border:'none',borderRadius:6,color:'#fff',fontWeight:700,fontSize:11,cursor:'pointer'}}>
                ■
              </button>
            </>
          )}
          <div style={{width:1,height:20,background:'#30363d'}} />
          <span style={{fontSize:10.5,color:savedMsg?'#3fb950':'#8b949e',whiteSpace:'nowrap',transition:'color .3s'}}>
            {savedMsg||(nodes.length+' blocs · '+edges.length+' connexions')}
          </span>
        </div>
      </div>

      {/* ── Body ── */}
      <div style={{display:'flex',flex:1,overflow:'hidden'}}>
        {sbCollapsed
          ?<div onClick={toggleSb} title="Afficher les blocs" data-sfx="none"
             style={{width:26,flex:'none',background:'#161b22',borderRight:'1px solid #21262d',display:'flex',flexDirection:'column',alignItems:'center',paddingTop:8,cursor:'pointer',gap:10}}
             onMouseEnter={e=>e.currentTarget.style.background='#1c2230'}
             onMouseLeave={e=>e.currentTarget.style.background='#161b22'}>
             <span style={{fontSize:12,color:'#58a6ff'}}>▶</span>
             <span style={{fontSize:9,writingMode:'vertical-rl',letterSpacing:'.12em',textTransform:'uppercase',color:'#484f58',fontWeight:700}}>Blocs</span>
           </div>
          :<>
             <AutoSidebar onDragStart={onSbDragStart} width={sbWidth} onCollapse={toggleSb} groups={blockGroups} onGroupDragStart={onGroupDragStart} onDeleteGroup={onDeleteGroup} platform={flowPlat} />
             <ResizeHandle onMouseDown={startSbResize} />
           </>}

        {/* Canvas */}
        <div ref={canvasRef} style={{flex:1,position:'relative',overflow:'hidden',background:'#0d1117',cursor:drawMode?(drawEraser?'cell':'crosshair'):'grab'}}
          onMouseDown={onCanvasDown}
          onDragOver={e=>e.preventDefault()}
          onDrop={onCanvasDrop}>
          {drawMode&&(
            <div style={{position:'absolute',top:10,left:'50%',transform:'translateX(-50%)',zIndex:50,display:'flex',alignItems:'center',gap:8,padding:'6px 14px',background:'rgba(31,111,235,.92)',borderRadius:20,boxShadow:'0 2px 12px rgba(0,0,0,.5)',pointerEvents:'none',userSelect:'none'}}>
              <span style={{fontSize:12,color:'#fff',fontWeight:700,letterSpacing:'.04em'}}>✏️ MODE DESSIN — Esc pour quitter</span>
            </div>
          )}
          <div style={{position:'absolute',inset:-6000,backgroundImage:'radial-gradient(circle, #1e2430 1px, transparent 1px)',backgroundSize:'20px 20px',pointerEvents:'none'}}></div>
          <div ref={innerRef} style={{position:'absolute',left:0,top:0,transformOrigin:'0 0'}}>
            <canvas ref={drawCvRef} width={8000} height={8000}
              style={{position:'absolute',left:0,top:0,zIndex:drawMode?30:0,pointerEvents:drawMode?'all':'none',cursor:'inherit'}}
              onMouseDown={onDrawStart} onMouseMove={onDrawMove} onMouseUp={onDrawEnd} onMouseLeave={onDrawEnd} />
            <svg ref={svgRef} style={{position:'absolute',left:0,top:0,overflow:'visible',zIndex:1,pointerEvents:'none'}} width="8000" height="8000"></svg>
            <div style={{position:'relative',zIndex:2}}>
              {nodes.map(node=>(
                <AutoNode key={node.id} node={node}
                  isActive={activeNodeId===node.id}
                  onHeaderMouseDown={e=>onNodeHead(e,node)}
                  onPortOutDown={(e,port)=>onPortOut(e,node.id,port)}
                  onDelete={()=>deleteNode(node.id)}
                  onChange={(k,v)=>updateNodeData(node.id,k,v)}
                  onCtxMenu={(e,id)=>{e.preventDefault();e.stopPropagation();setCtxMenu({x:e.clientX,y:e.clientY,nodeId:id});}}
                  onStartCapture={cfg=>setCaptureTap(typeof cfg==='string'?{nodeId:cfg,fieldX:'x',fieldY:'y'}:cfg)}
                  isSelected={selectedIds.has(node.id)}
                  nodeResult={nodeResults[node.id]}
                  deviceId={deviceId} />
              ))}
            </div>
            {selRectState&&<div style={{position:'absolute',left:selRectState.x1,top:selRectState.y1,width:Math.max(1,selRectState.x2-selRectState.x1),height:Math.max(1,selRectState.y2-selRectState.y1),border:'1.5px solid #388bfd',background:'rgba(56,139,253,.09)',borderRadius:3,pointerEvents:'none',zIndex:200}} />}
          </div>
          {nodes.length===0&&(
            <div style={{position:'absolute',inset:0,display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',pointerEvents:'none',gap:10}}>
              <div style={{fontSize:38}}>⚡</div>
              <div style={{fontSize:14,color:'#8b949e',fontWeight:600}}>Glisse des blocs depuis la sidebar</div>
              <div style={{fontSize:11,color:'#484f58',textAlign:'center',lineHeight:1.7}}>Relie les ports · Molette pour zoomer · Clic droit sur un bloc pour le lancer</div>
            </div>
          )}
          {/* Zoom badge */}
          <div style={{position:'absolute',bottom:12,right:12,background:'#21262d',border:'1px solid #30363d',borderRadius:6,display:'flex',alignItems:'center',overflow:'hidden',fontSize:10.5,color:'#8b949e',fontFamily:'monospace'}}>
            <button onClick={()=>{const ns=Math.max(0.2,Math.round((scaleRef.current-0.1)*10)/10);const el=canvasRef.current;const rect=el.getBoundingClientRect();const cx=rect.width/2,cy=rect.height/2;panRef.current={x:cx-(cx-panRef.current.x)*(ns/scaleRef.current),y:cy-(cy-panRef.current.y)*(ns/scaleRef.current)};scaleRef.current=ns;applyT();updateSVG();setZoomPct(Math.round(ns*100));}}
              style={{background:'none',border:'none',borderRight:'1px solid #30363d',color:'#8b949e',cursor:'pointer',fontSize:13,padding:'2px 7px',lineHeight:1,fontFamily:'monospace'}}
              onMouseEnter={e=>e.currentTarget.style.color='#e6edf3'} onMouseLeave={e=>e.currentTarget.style.color='#8b949e'}>−</button>
            <span style={{padding:'3px 8px',minWidth:38,textAlign:'center'}}>{zoomPct}%</span>
            <button onClick={()=>{const ns=Math.min(3,Math.round((scaleRef.current+0.1)*10)/10);const el=canvasRef.current;const rect=el.getBoundingClientRect();const cx=rect.width/2,cy=rect.height/2;panRef.current={x:cx-(cx-panRef.current.x)*(ns/scaleRef.current),y:cy-(cy-panRef.current.y)*(ns/scaleRef.current)};scaleRef.current=ns;applyT();updateSVG();setZoomPct(Math.round(ns*100));}}
              style={{background:'none',border:'none',borderLeft:'1px solid #30363d',color:'#8b949e',cursor:'pointer',fontSize:13,padding:'2px 7px',lineHeight:1,fontFamily:'monospace'}}
              onMouseEnter={e=>e.currentTarget.style.color='#e6edf3'} onMouseLeave={e=>e.currentTarget.style.color='#8b949e'}>+</button>
          </div>
        </div>

        <ResizeHandle onMouseDown={startRpResize} />
        <AutoRightPanel badge={badge} logs={logs} running={running} activeType={activeNodeType} width={rpWidth} udid={deviceId}
          captureTap={captureTap}
          onClearLogs={()=>setLogs([])}
          onCaptureDone={(x,y)=>{
            if(captureTap){
              const nid=captureTap.nodeId||captureTap;
              if(captureTap.zoneIdx!=null){
                const nd=nodes.find(n=>n.id===nid);
                if(nd){const zs=_parseZones(nd.d.zones);if(zs[captureTap.zoneIdx]){zs[captureTap.zoneIdx].x=x;zs[captureTap.zoneIdx].y=y;updateNodeData(nid,'zones',JSON.stringify(zs));}}
              } else {
                const fx=captureTap.fieldX!==undefined?captureTap.fieldX:'x';
                const fy=captureTap.fieldY!==undefined?captureTap.fieldY:'y';
                if(fx)updateNodeData(nid,fx,String(x));
                if(fy)updateNodeData(nid,fy,String(y));
              }
              setCaptureTap(null);
            }
          }} />
      </div>

      {/* ── Pop-up nommage d'un groupe de blocs ── */}
      {groupNaming&&(
        <div onClick={()=>setGroupNaming(null)}
          style={{position:'fixed',inset:0,background:'rgba(1,4,9,.7)',zIndex:99999,display:'flex',alignItems:'center',justifyContent:'center',padding:20}}>
          <div onClick={e=>e.stopPropagation()}
            style={{background:'#161b22',border:'1px solid #8957e5',borderRadius:14,padding:'22px 22px',width:'min(380px, 100%)',boxSizing:'border-box',boxShadow:'0 12px 48px rgba(0,0,0,.6)'}}>
            <div style={{fontSize:15,fontWeight:800,color:'#e6edf3',marginBottom:4}}>📦 Enregistrer le groupe</div>
            <div style={{fontSize:12,color:'#8b949e',marginBottom:14}}>{(groupNaming.ids||[]).length} bloc(s) — donne un nom à ce groupe réutilisable.</div>
            <input autoFocus value={groupNameInput} data-sfx="none"
              onChange={e=>setGroupNameInput(e.target.value)}
              onKeyDown={e=>{if(e.key==='Enter')commitGroup();if(e.key==='Escape')setGroupNaming(null);}}
              placeholder="Ex. Fermer Instagram"
              style={{width:'100%',boxSizing:'border-box',background:'#0d1117',border:'1px solid #8957e5',borderRadius:8,color:'#e6edf3',fontSize:13,fontWeight:600,padding:'9px 11px',outline:'none'}} />
            <div style={{display:'flex',gap:8,justifyContent:'flex-end',marginTop:16}}>
              <button onClick={()=>setGroupNaming(null)} data-sfx="none"
                style={{padding:'8px 16px',background:'#21262d',border:'1px solid #30363d',borderRadius:8,color:'#8b949e',fontWeight:700,fontSize:12.5,cursor:'pointer'}}>Annuler</button>
              <button onClick={commitGroup} disabled={!groupNameInput.trim()} data-sfx="none"
                style={{padding:'8px 18px',background:groupNameInput.trim()?'#8957e5':'#21262d',border:'none',borderRadius:8,color:groupNameInput.trim()?'#fff':'#484f58',fontWeight:700,fontSize:12.5,cursor:groupNameInput.trim()?'pointer':'default'}}>Enregistrer</button>
            </div>
          </div>
        </div>
      )}

      {/* ── Context menu ── */}
      {ctxMenu&&(
        <div style={{position:'fixed',left:ctxMenu.x,top:ctxMenu.y,zIndex:500,background:'#161b22',border:'1px solid #30363d',borderRadius:8,padding:'4px 0',boxShadow:'0 8px 28px rgba(0,0,0,.7)',minWidth:200}}
          onMouseDown={e=>e.stopPropagation()}>
          {ctxMenu.edgeIdx!=null?(
            /* ─ menu lien ─ */
            <>
              <div style={{padding:'8px 14px 5px',fontSize:9,color:'#8b949e',fontWeight:700,textTransform:'uppercase',letterSpacing:'.07em',borderBottom:'1px solid #21262d',marginBottom:3}}>
                Connexion
              </div>
              <div style={{padding:'8px 14px',cursor:'pointer',fontSize:12,color:'#f85149',display:'flex',alignItems:'center',gap:8}}
                onMouseEnter={e=>e.currentTarget.style.background='#21262d'}
                onMouseLeave={e=>e.currentTarget.style.background='none'}
                onClick={()=>{
                  const ne=edgesRef.current.filter((_,i)=>i!==ctxMenu.edgeIdx);
                  edgesRef.current=ne;setEdges([...ne]);setCtxMenu(null);pushHistory(nodesRef.current,ne);
                }}>
                🗑 Supprimer le lien
              </div>
            </>
          ):(
            /* ─ menu nœud ─ */
            <>
              <div style={{padding:'8px 14px 5px',fontSize:9,color:'#8b949e',fontWeight:700,textTransform:'uppercase',letterSpacing:'.07em',borderBottom:'1px solid #21262d',marginBottom:3}}>
                {AUTO_BLOCKS[nodes.find(n=>n.id===ctxMenu.nodeId)?.type]?.e} {AUTO_BLOCKS[nodes.find(n=>n.id===ctxMenu.nodeId)?.type]?.l}
              </div>
              <div style={{padding:'8px 14px',cursor:'pointer',fontSize:12,color:'#3fb950',display:'flex',alignItems:'center',gap:8}}
                onMouseEnter={e=>e.currentTarget.style.background='#21262d'}
                onMouseLeave={e=>e.currentTarget.style.background='none'}
                onClick={()=>{doRun(ctxMenu.nodeId);setCtxMenu(null);}}>
                ▶ Lancer depuis ce bloc
              </div>
              <div style={{padding:'8px 14px',cursor:'pointer',fontSize:12,color:'#58a6ff',display:'flex',alignItems:'center',gap:8}}
                onMouseEnter={e=>e.currentTarget.style.background='#21262d'}
                onMouseLeave={e=>e.currentTarget.style.background='none'}
                onClick={()=>{doRunSingle(ctxMenu.nodeId);setCtxMenu(null);}}>
                ▶ Lancer ce bloc seul
              </div>
              <div style={{padding:'8px 14px',cursor:'pointer',fontSize:12,color:'#e3b341',display:'flex',alignItems:'center',gap:8}}
                onMouseEnter={e=>e.currentTarget.style.background='#21262d'}
                onMouseLeave={e=>e.currentTarget.style.background='none'}
                onClick={()=>{
                  const src=nodesRef.current.find(x=>x.id===ctxMenu.nodeId);
                  if(src){
                    const newId='an'+Date.now()+'_'+(Math.random()*99999|0);
                    const copy={...src,id:newId,x:src.x+260,y:src.y+20,d:{...src.d}};
                    const nn=[...nodesRef.current,copy];
                    nodesRef.current=nn;setNodes(nn);
                    pushHistory(nn,edgesRef.current);
                    setSelectedIds(new Set([newId]));
                  }
                  setCtxMenu(null);
                }}>
                ⧉ Dupliquer ce bloc
              </div>
              {selectedIds.size>1&&selectedIds.has(ctxMenu.nodeId)&&(
                <div style={{padding:'8px 14px',cursor:'pointer',fontSize:12,color:'#bc8cff',display:'flex',alignItems:'center',gap:8}}
                  onMouseEnter={e=>e.currentTarget.style.background='#21262d'}
                  onMouseLeave={e=>e.currentTarget.style.background='none'}
                  onClick={()=>{saveSelectionAsGroup();setCtxMenu(null);}}>
                  💾 Enregistrer le groupe ({selectedIds.size} blocs)
                </div>
              )}
              {selectedIds.size>1&&selectedIds.has(ctxMenu.nodeId)&&(
                <div style={{padding:'8px 14px',cursor:'pointer',fontSize:12,color:'#e3b341',display:'flex',alignItems:'center',gap:8}}
                  onMouseEnter={e=>e.currentTarget.style.background='#21262d'}
                  onMouseLeave={e=>e.currentTarget.style.background='none'}
                  onClick={()=>{
                    const ts=Date.now();
                    const idMap={};
                    Array.from(selectedIds).forEach((id,i)=>{idMap[id]='an'+(ts+i)+'_'+(Math.random()*99999|0);});
                    const copies=Array.from(selectedIds).map(id=>{
                      const src=nodesRef.current.find(x=>x.id===id);
                      if(!src)return null;
                      return{...src,id:idMap[id],x:src.x+260,y:src.y+20,d:{...src.d}};
                    }).filter(Boolean);
                    // Liaisons internes au groupe uniquement
                    const internalEdges=edgesRef.current
                      .filter(eg=>idMap[eg.f]&&idMap[eg.t])
                      .map(eg=>({...eg,id:'ae'+(ts+(Math.random()*9999|0)),f:idMap[eg.f],t:idMap[eg.t]}));
                    const nn=[...nodesRef.current,...copies];
                    const ne=[...edgesRef.current,...internalEdges];
                    nodesRef.current=nn;edgesRef.current=ne;
                    setNodes(nn);setEdges(ne);
                    pushHistory(nn,ne);
                    setSelectedIds(new Set(copies.map(c=>c.id)));
                    setCtxMenu(null);
                  }}>
                  ⧉ Dupliquer les {selectedIds.size} blocs sélectionnés
                </div>
              )}
              <div style={{padding:'8px 14px',cursor:'pointer',fontSize:12,color:'#e6edf3',display:'flex',alignItems:'center',gap:8}}
                onMouseEnter={e=>e.currentTarget.style.background='#21262d'}
                onMouseLeave={e=>e.currentTarget.style.background='none'}
                onClick={()=>{
                  const n=nodes.find(x=>x.id===ctxMenu.nodeId);
                  if(n){const d={...n.d};alert(JSON.stringify({type:n.type,...d},null,2));}
                  setCtxMenu(null);
                }}>
                🔍 Inspecter ce bloc
              </div>
              <div style={{padding:'8px 14px',cursor:'pointer',fontSize:12,color:'#f85149',display:'flex',alignItems:'center',gap:8,borderTop:'1px solid #21262d',marginTop:3}}
                onMouseEnter={e=>e.currentTarget.style.background='#21262d'}
                onMouseLeave={e=>e.currentTarget.style.background='none'}
                onClick={()=>{deleteNode(ctxMenu.nodeId);setCtxMenu(null);}}>
                🗑 Supprimer ce bloc
              </div>
            </>
          )}
        </div>
      )}

      {/* ── JSON modal ── */}
      {showJson&&(
        <div style={{position:'fixed',inset:0,background:'rgba(0,0,0,.75)',zIndex:600,display:'flex',alignItems:'center',justifyContent:'center'}}
          onClick={e=>{if(e.target===e.currentTarget)setShowJson(false);}}>
          <div style={{background:'#161b22',border:'1px solid #30363d',borderRadius:12,padding:20,width:560,maxHeight:'75vh',overflow:'auto'}}>
            <div style={{fontSize:13,fontWeight:700,color:'#bc8cff',marginBottom:12}}>{'{ }'} Actions JSON — prêt pour le backend</div>
            <pre style={{background:'#0d1117',border:'1px solid #30363d',borderRadius:8,padding:14,fontSize:11,color:'#e6edf3',overflow:'auto',lineHeight:1.7,whiteSpace:'pre-wrap',margin:0}}>{jsonStr}</pre>
            <button onClick={()=>setShowJson(false)}
              style={{marginTop:12,padding:'6px 18px',borderRadius:7,background:'#21262d',border:'1px solid #30363d',color:'#8b949e',cursor:'pointer',fontSize:12,fontWeight:700}}>
              Fermer
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

// ─── AutoFlowCard ─────────────────────────────────────────
function AutoFlowCard({flow,onEdit,onDelete,onDuplicate,onRename}){
  const [editing,setEditing]=React.useState(false);
  const [nm,setNm]=React.useState(flow.name||'');
  const saveName=()=>{if(onRename)onRename((nm.trim()||flow.name||'Sans nom'));setEditing(false);};
  const nc=(flow.nodes||[]).length,ec=(flow.edges||[]).length;
  const cats=[...new Set((flow.nodes||[]).map(n=>AUTO_BLOCKS[n.type]&&AUTO_BLOCKS[n.type].cat).filter(Boolean))];
  const date=flow.updatedAt||flow.createdAt;
  const ds=date?new Date(date).toLocaleDateString('fr-FR',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}):'';
  return(
    <div style={{background:'#161b22',border:'1px solid #30363d',borderRadius:10,padding:14,display:'flex',flexDirection:'column',gap:10,transition:'border-color .15s'}}
      onMouseEnter={e=>e.currentTarget.style.borderColor='#388bfd'}
      onMouseLeave={e=>e.currentTarget.style.borderColor='#30363d'}>
      <div style={{display:'flex',alignItems:'flex-start',gap:8}}>
        <div style={{flex:1,minWidth:0}}>
          {editing
            ?<input autoFocus value={nm} data-sfx="none"
               onChange={e=>setNm(e.target.value)}
               onKeyDown={e=>{if(e.key==='Enter')saveName();if(e.key==='Escape'){setNm(flow.name||'');setEditing(false);}}}
               onBlur={saveName}
               style={{width:'100%',fontSize:13,fontWeight:700,background:'#0d1117',border:'1px solid #388bfd',borderRadius:5,color:'#e6edf3',padding:'3px 6px',outline:'none',boxSizing:'border-box'}} />
            :<div onDoubleClick={()=>{setNm(flow.name||'');setEditing(true);}} title="Double-clic pour renommer"
               style={{fontSize:13,fontWeight:700,color:'#e6edf3',whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis',cursor:'text'}}>{flow.name||'Sans nom'}</div>}
          <div style={{fontSize:10,color:'#8b949e',marginTop:2}}>{nc} blocs · {ec} connexions{ds?' · '+ds:''}</div>
        </div>
        <div style={{display:'flex',gap:4,flex:'none'}}>
          <button onClick={()=>{setNm(flow.name||'');setEditing(true);}} title="Renommer" data-sfx="none" style={{background:'#21262d',border:'1px solid #30363d',borderRadius:5,color:'#8b949e',cursor:'pointer',padding:'3px 7px',fontSize:11}}>✎</button>
          <button onClick={onDuplicate} title="Dupliquer" style={{background:'#21262d',border:'1px solid #30363d',borderRadius:5,color:'#8b949e',cursor:'pointer',padding:'3px 7px',fontSize:11}}>⎘</button>
          <button onClick={onDelete} title="Supprimer" style={{background:'#21262d',border:'1px solid #30363d',borderRadius:5,color:'#f85149',cursor:'pointer',padding:'3px 7px',fontSize:11}}>✕</button>
        </div>
      </div>
      {cats.length>0&&(
        <div style={{display:'flex',flexWrap:'wrap',gap:4}}>
          {cats.map(c=>(
            <span key={c} style={{fontSize:9.5,padding:'2px 7px',borderRadius:4,background:'#21262d',border:'1px solid #30363d',color:'#8b949e'}}>
              {AUTO_CAT[c]?AUTO_CAT[c].icon+' '+AUTO_CAT[c].label:c}
            </span>
          ))}
        </div>
      )}
      <div style={{display:'flex',gap:6}}>
        <button onClick={onEdit}
          style={{flex:1,padding:'8px 0',background:'transparent',border:'1px solid #30363d',borderRadius:7,color:'#e6edf3',fontWeight:700,fontSize:12,cursor:'pointer',transition:'background .15s,border-color .15s'}}
          onMouseEnter={e=>{e.currentTarget.style.background='#21262d';e.currentTarget.style.borderColor='#388bfd';}}
          onMouseLeave={e=>{e.currentTarget.style.background='transparent';e.currentTarget.style.borderColor='#30363d';}}>
          ✏️ Éditer le flux
        </button>
        <button title="Ouvrir dans une fenêtre séparée" onClick={()=>{
          const url='http://127.0.0.1:8787/?popup=automation&flow='+encodeURIComponent(flow.id);
          if(window.electronGoogle&&window.electronGoogle.openAutomationPopup){
            window.electronGoogle.openAutomationPopup(flow.id);
          } else {
            window.open(url,'_blank','width=1280,height=860,menubar=no,toolbar=no');
          }
        }}
          style={{padding:'8px 10px',background:'transparent',border:'1px solid #30363d',borderRadius:7,color:'#8b949e',fontSize:13,cursor:'pointer',transition:'background .15s,border-color .15s'}}
          onMouseEnter={e=>{e.currentTarget.style.background='#21262d';e.currentTarget.style.borderColor='#388bfd';e.currentTarget.style.color='#58a6ff';}}
          onMouseLeave={e=>{e.currentTarget.style.background='transparent';e.currentTarget.style.borderColor='#30363d';e.currentTarget.style.color='#8b949e';}}>
          ⧉
        </button>
      </div>
    </div>
  );
}

// ─── AutomationList ───────────────────────────────────────
function AutomationList({onEdit,platform}){
  const plat=platform==='android'?'android':'ios';
  const [flows,setFlows]=React.useState(autoLoadFlows);
  const platOf=(f)=>(f.platform==='android'?'android':'ios');   // flux legacy = iOS
  const shown=flows.filter(f=>platOf(f)===plat);
  const isAndroid=plat==='android';
  function createFlow(){
    const id='flow_'+Date.now();
    const f={id,name:'Nouveau flux',platform:plat,nodes:[],edges:[],createdAt:new Date().toISOString()};
    const u=[...flows,f];autoSaveFlows(u);setFlows(u);onEdit(f);
  }
  function deleteFlow(id){const u=flows.filter(f=>f.id!==id);autoSaveFlows(u);setFlows(u);}
  function renameFlow(id,name){const u=flows.map(f=>f.id===id?{...f,name:name,updatedAt:new Date().toISOString()}:f);autoSaveFlows(u);setFlows(u);}
  function dupFlow(flow){
    const copy={...flow,id:'flow_'+Date.now(),name:flow.name+' (copie)',platform:platOf(flow),nodes:[...(flow.nodes||[])],edges:[...(flow.edges||[])]};
    const u=[...flows,copy];autoSaveFlows(u);setFlows(u);
  }
  function openFlow(flow){const s=autoLoadFlows().find(f=>f.id===flow.id)||flow;onEdit(s);}
  // Import d'un flux .json -> tagué avec la catégorie courante
  function _convertMoreLogin(j){
    // Détecte le format MoreLogin (editorVersion ou variables au niveau racine, pas d'edges)
    if(!j.editorVersion && !j.variables) return null;
    const mlNodes = j.nodes || [];
    // Mapping type MoreLogin → type PhoneLabs
    const TYPE_MAP = {
      start:         'trigger_manual',
      end:           'logic_end',
      waitTime:      'logic_wait',
      openApp:       'wda_launch',
      executeADB:    'adb_shell',
      executeJS:     'js_script',
      downloadMedia: 'download_file',
    };
    const val = (iv, key) => {
      const v = iv && iv[key];
      if(!v) return '';
      if(v.type === 'constant') return v.content != null ? String(v.content) : '';
      return '';
    };
    const COLS = 4, STEP_X = 260, STEP_Y = 160;
    const nodes = mlNodes.map((n, i) => {
      const type = TYPE_MAP[n.type] || 'adb_shell';
      const iv = n.data && n.data.inputsValues || {};
      let d = {};
      if(n.type === 'waitTime')      d = { s: String(Math.round((parseInt(val(iv,'waitTime'))||0)/1000)||2) };
      if(n.type === 'openApp')       d = { bundle: val(iv,'packageName') || 'com.instagram.android' };
      if(n.type === 'executeADB')    d = { cmd: val(iv,'command') || '' };
      if(n.type === 'executeJS')     d = { code: val(iv,'code') || '' };
      if(n.type === 'downloadMedia') d = { url: val(iv,'url') || '', dest: val(iv,'downloadTo') || 'gallery' };
      // Pré-remplir les valeurs par défaut des champs manquants
      const blockDef = AUTO_BLOCKS[type];
      if(blockDef && blockDef.f) blockDef.f.forEach(fld => { if(d[fld.k] === undefined) d[fld.k] = fld.v || ''; });
      return {
        id: n.id || ('n_'+i),
        type,
        x: (i % COLS) * STEP_X + 40,
        y: Math.floor(i / COLS) * STEP_Y + 40,
        d,
      };
    });
    // Connexions séquentielles — le builder utilise e.f (from) et e.t (to)
    const edges = nodes.slice(0,-1).map((n,i)=>({
      id:'e_'+i, f:n.id, t:nodes[i+1].id
    }));
    return { nodes, edges };
  }

  function importFlow(){
    const inp=document.createElement('input');inp.type='file';inp.accept='.json,application/json';
    inp.onchange=e=>{const file=e.target.files[0];if(!file)return;const rd=new FileReader();
      rd.onload=()=>{try{const j=JSON.parse(rd.result);
        // Tente la conversion MoreLogin si le format ne correspond pas au format PhoneLabs
        const converted = (!j.nodes || (j.nodes.length>0 && !j.nodes[0].type?.startsWith)) ? null : _convertMoreLogin(j);
        const ml = _convertMoreLogin(j);
        const base = ml ? { nodes: ml.nodes, edges: ml.edges, name: j.name || 'Flux importé (MoreLogin)' }
                        : { nodes: j.nodes||[], edges: j.edges||[], name: j.name||'Flux importé' };
        const f={...base, id:'flow_'+Date.now(), platform: plat };
        const u=[...flows,f];autoSaveFlows(u);setFlows(u);
        if(ml) alert('✅ Flux MoreLogin converti (' + ml.nodes.length + ' blocs) — visible dans l\'onglet actuel');
      }catch(err){alert('Fichier .json invalide : '+err.message);}};
      rd.readAsText(file);};
    inp.click();
  }
  return(
    <div style={{padding:24,maxWidth:1020,margin:'0 auto'}}>
      <div style={{display:'flex',alignItems:'center',marginBottom:22,gap:10}}>
        <div>
          <div style={{fontSize:18,fontWeight:700,color:'#e6edf3'}}>{isAndroid?'☁️ Automatisations · Android':'📱 Automatisations · iOS'}</div>
          <div style={{fontSize:12,color:'#8b949e',marginTop:3}}>{isAndroid?'Flux pour vos Phone Cloud (Android) · auto-sauvegardé':'Flux pour vos iPhones physiques · auto-sauvegardé'}</div>
        </div>
        <button onClick={importFlow} style={{marginLeft:'auto',padding:'8px 14px',background:'#21262d',border:'1px solid #30363d',borderRadius:8,color:'#e6edf3',fontWeight:600,fontSize:12.5,cursor:'pointer'}}>⬇ Importer</button>
        <button onClick={createFlow} style={{padding:'8px 18px',background:isAndroid?'#3b82f6':'#238636',border:'none',borderRadius:8,color:'#fff',fontWeight:700,fontSize:13,cursor:'pointer'}}>
          + Nouveau flux
        </button>
      </div>
      {shown.length===0?(
        <div style={{textAlign:'center',padding:'72px 0',color:'#8b949e',border:'1px dashed #30363d',borderRadius:12}}>
          <div style={{fontSize:38,marginBottom:14}}>{isAndroid?'☁️':'⚡'}</div>
          <div style={{fontSize:15,fontWeight:700,marginBottom:6,color:'#e6edf3'}}>Aucune automatisation {isAndroid?'Android':'iOS'}</div>
          <div style={{fontSize:12,marginBottom:18}}>Créez votre premier flux pour {isAndroid?'vos Phone Cloud (Android)':'vos iPhones physiques'}</div>
          <button onClick={createFlow} style={{padding:'8px 20px',background:'#21262d',border:'1px solid #30363d',borderRadius:8,color:'#e6edf3',fontWeight:600,fontSize:12,cursor:'pointer'}}>+ Créer un flux</button>
        </div>
      ):(
        <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fill, minmax(270px, 1fr))',gap:14}}>
          {shown.map(f=>(
            <AutoFlowCard key={f.id} flow={f}
              onEdit={()=>openFlow(f)}
              onDelete={()=>deleteFlow(f.id)}
              onDuplicate={()=>dupFlow(f)}
              onRename={(name)=>renameFlow(f.id,name)} />
          ))}
        </div>
      )}
    </div>
  );
}

// ─── Root ─────────────────────────────────────────────────
function AutomationBuilder({popupFlowId}){
  const {useState}=React;
  const [editing,setEditing]=useState(()=>{
    // Mode popup : charge directement le flux spécifié dans l'URL
    if(popupFlowId){
      return autoLoadFlows().find(f=>f.id===popupFlowId)||null;
    }
    try{
      const id=localStorage.getItem('pl.auto.openflow');
      if(id){localStorage.removeItem('pl.auto.openflow');return autoLoadFlows().find(f=>f.id===id)||null;}
    }catch(e){}
    return null;
  });
  const [mode,setMode]=useState(()=>{try{return localStorage.getItem('pl.auto.mode')==='campagnes'?'campagnes':'flux';}catch(e){return 'flux';}});
  const pick=(m)=>{setMode(m);try{localStorage.setItem('pl.auto.mode',m);}catch(e){}};
  // Catégorie de plateforme : iOS (iPhone physique) vs Android (Phone Cloud) — 2 mondes séparés
  const [plat,setPlat]=useState(()=>{try{return localStorage.getItem('pl.auto.plat')==='android'?'android':'ios';}catch(e){return 'ios';}});
  const pickPlat=(p)=>{setPlat(p);try{localStorage.setItem('pl.auto.plat',p);}catch(e){}};
  if(editing)return <AutomationEditor flow={editing} onBack={popupFlowId?null:()=>setEditing(null)} />;
  if(popupFlowId)return(
    <div style={{display:'flex',alignItems:'center',justifyContent:'center',height:'100vh',background:'#0d1117',color:'#8b949e',fontSize:14}}>
      Flux introuvable — ferme cette fenêtre.
    </div>
  );
  const tabBtn=(m,label)=>(
    <button onClick={()=>pick(m)} data-sfx="none"
      style={{padding:'6px 18px',borderRadius:8,fontSize:12.5,fontWeight:700,cursor:'pointer',
        border:'1px solid '+(mode===m?'#8957e5':'#30363d'),
        background:mode===m?'rgba(137,87,229,.15)':'#161b22',
        color:mode===m?'#bc8cff':'#8b949e'}}>{label}</button>
  );
  const platBtn=(pv,label)=>(
    <button onClick={()=>pickPlat(pv)} data-sfx="none"
      style={{padding:'8px 22px',borderRadius:10,fontSize:13,fontWeight:800,cursor:'pointer',
        border:'1px solid '+(plat===pv?(pv==='android'?'rgba(120,160,220,.55)':'rgba(0,224,138,.5)'):'#30363d'),
        background:plat===pv?(pv==='android'?'rgba(120,160,220,.15)':'rgba(0,224,138,.12)'):'#161b22',
        color:plat===pv?(pv==='android'?'#9cc4f0':'#00e08a'):'#8b949e'}}>{label}</button>
  );
  return(
    <div>
      {/* Catégorie de plateforme : 2 mondes séparés (flux non mélangés) */}
      <div style={{display:'flex',gap:10,justifyContent:'center',padding:'16px 0 0'}}>
        {platBtn('ios','📱 iPhone physique · iOS')}
        {platBtn('android','☁️ Phone Cloud · Android')}
      </div>
      <div style={{display:'flex',gap:8,justifyContent:'center',padding:'12px 0 0'}}>
        {tabBtn('flux','⚡ Flux')}
        {tabBtn('campagnes','🗓️ Campagnes')}
      </div>
      {mode==='campagnes'
        ? (window.CampaignBuilder?React.createElement(window.CampaignBuilder,{platform:plat}):<div style={{textAlign:'center',padding:40,color:'#8b949e'}}>Module campagnes non chargé.</div>)
        : <AutomationList onEdit={setEditing} platform={plat} />}
    </div>
  );
}

window.AutomationBuilder=AutomationBuilder;
window.AutoLivePhone=AutoLivePhone;   // réutilisé par le pilotage cloud (flux H.264)

// ─── PilotageAuto — page dédiée dans la nav ───────────────
function PilotageAuto({onGo}){
  const {useState,useRef,useEffect,useMemo}=React;
  const [flows,setFlows]=useState(autoLoadFlows);
  const [selectedId,setSelectedId]=useState(()=>{
    try{return localStorage.getItem('pl.piauto.flow')||'';}catch(e){return '';}
  });
  const [batchCount,setBatchCount]=useState(5);
  const [batchCooldown,setBatchCooldown]=useState(20);
  const [soundEnabled,setSoundEnabled]=useState(true);
  const soundEnabledRef=useRef(true);
  const [batchRunning,setBatchRunning]=useState(false);
  const batchRunningRef=useRef(false);
  const [batchPaused,setBatchPaused]=useState(false);
  const batchPausedRef=useRef(false);
  const [batchProgress,setBatchProgress]=useState(null);
  const [logs,setLogs]=useState([]);
  const runningRef=useRef(false);
  const pausedRef=useRef(false);
  const lastRunOkRef=useRef(null);
  const [showPreview,setShowPreview]=useState(false);
  const [previewKey,setPreviewKey]=useState(0);
  const [previewUdid,setPreviewUdid]=useState(()=>{try{return localStorage.getItem('pl.auto.dev')||'';}catch(e){return '';}});
  // Multi-device selection — Set of udids
  const [selectedDevices,setSelectedDevices]=useState(()=>{
    try{const s=JSON.parse(localStorage.getItem('pl.piauto.devices')||'[]');return new Set(s);}catch(e){return new Set();}
  });
  // Pop-up de blocage : deux iPhones ne peuvent pas partager le même proxy simultanément
  const [proxyPopup,setProxyPopup]=useState(null);

  // Même source que la page iPhones — contient tag, proxy, udid, etc.
  const farm=useLiveDevices(5000);
  const devices=(farm.list||[]);

  function saveDeviceSel(sel){
    setSelectedDevices(sel);
    try{localStorage.setItem('pl.piauto.devices',JSON.stringify([...sel]));}catch(e){}
  }
  function toggleDevice(udid){
    const next=new Set(selectedDevices);
    if(next.has(udid)){next.delete(udid);saveDeviceSel(next);return;}
    // Sélection : bloque si le proxy de cet iPhone est déjà pris (autre fenêtre OU iPhone déjà coché)
    const dev=devices.find(d=>d.udid===udid);
    if(dev&&dev.proxy){
      const other=proxyLockedByOther(dev.proxy);
      if(other){
        setProxyPopup({
          proxy:dev.proxy,
          taken:other.label||(other.udid?other.udid.slice(0,8):'une autre fenêtre'),
          tried:dev.tag||dev.name||dev.udid.slice(0,8),
        });
        return; // refuse : proxy actif dans une autre fenêtre
      }
      const clash=devices.find(d=>d.udid!==udid&&selectedDevices.has(d.udid)&&d.proxy===dev.proxy);
      if(clash){
        setProxyPopup({
          proxy:dev.proxy,
          taken:clash.tag||clash.name||clash.udid.slice(0,8),
          tried:dev.tag||dev.name||dev.udid.slice(0,8),
        });
        return; // refuse la sélection
      }
    }
    next.add(udid);saveDeviceSel(next);
  }

  // Conflits proxy : deux devices sélectionnés avec même proxy non-null
  const proxyConflicts=useMemo(()=>{
    const sel=devices.filter(d=>selectedDevices.has(d.udid)&&d.proxy);
    const groups={};
    sel.forEach(d=>{if(!groups[d.proxy])groups[d.proxy]=[];groups[d.proxy].push(d);});
    return Object.values(groups).filter(g=>g.length>1);
  },[devices,selectedDevices]);
  const loopCountersRef=useRef({});

  useEffect(()=>{setFlows(autoLoadFlows());},[]);

  function addLog(msg,color){
    setLogs(prev=>[...prev,{msg,color,id:Date.now()+Math.random()}]);
    setTimeout(()=>{const el=document.getElementById('pia-log-bottom');if(el)el.scrollIntoView({block:'nearest'});},30);
  }
  function _fmtDur(ms){const s=Math.round(ms/1000);return s<60?s+'s':Math.floor(s/60)+'m'+((s%60)?''+(s%60)+'s':'');}
  function fmtMs(ms){return _fmtDur(ms);}

  function playSound(type){
    if(!soundEnabledRef.current)return;
    if(window.SoundBoard)window.SoundBoard.play(type);
  }

  function _sleep(ms){return new Promise(r=>setTimeout(r,ms));}

  function startBatch(){
    if(!selectedId){addLog('❌ Sélectionne d\'abord un flux','#f85149');return;}
    // Stocke les params et ouvre l'éditeur — le vrai moteur tourne dans AutomationEditor
    try{
      localStorage.setItem('pl.auto.openflow', selectedId);
      localStorage.setItem('pl.auto.batchauto', JSON.stringify({
        count: Math.max(1,parseInt(batchCount)||1),
        cooldown: Math.max(0,parseInt(batchCooldown)||0)
      }));
    }catch(e){}
    if(onGo)onGo('automation');
  }


  function stopBatch(){
    batchRunningRef.current=false;batchPausedRef.current=false;
    setBatchRunning(false);setBatchPaused(false);
    addLog('⛔ Batch arrêté','#f85149');
  }

  function toggleBatchPause(){
    const next=!batchPausedRef.current;
    batchPausedRef.current=next;setBatchPaused(next);
    addLog(next?'⏸ Batch en pause':'▶ Batch repris',next?'#d29922':'#3fb950');
  }

  const flow=flows.find(f=>f.id===selectedId);
  const prog=batchProgress;

  return(
    <div style={{padding:24,maxWidth:980,margin:'0 auto',display:'flex',flexDirection:'column',gap:18}}>
      <div style={{display:'flex',alignItems:'flex-start',gap:12,flexWrap:'wrap'}}>
        <div style={{flex:1}}>
          <div style={{fontSize:18,fontWeight:700,color:'#e6edf3',marginBottom:4}}>🔄 Pilotage auto</div>
          <div style={{fontSize:12,color:'#8b949e'}}>Lance un flux plusieurs fois en séquentiel avec cooldown entre chaque run</div>
        </div>
        <button onClick={()=>setShowPreview(p=>!p)}
          style={{padding:'7px 16px',background:showPreview?'#388bfd':'#21262d',border:'1px solid '+(showPreview?'#388bfd':'#30363d'),
            borderRadius:8,color:showPreview?'#fff':'#8b949e',fontWeight:700,fontSize:12,cursor:'pointer',flex:'none',display:'flex',alignItems:'center',gap:6}}>
          📱 {showPreview?'Masquer aperçu':'Aperçu iPhone'}
        </button>
      </div>

      {/* ── Layout : colonne gauche (contrôles) + droite (aperçu) ── */}
      <div style={{display:'flex',gap:18,alignItems:'flex-start'}}>
      <div style={{flex:1,display:'flex',flexDirection:'column',gap:18}}>

      {/* Sélecteur flux */}
      <div style={{background:'#161b22',border:'1px solid #30363d',borderRadius:10,padding:16,display:'flex',flexDirection:'column',gap:10}}>
        <div style={{fontSize:11,color:'#8b949e',fontWeight:700,textTransform:'uppercase',letterSpacing:'.05em'}}>Flux à lancer</div>
        {flows.length===0?(
          <div style={{color:'#484f58',fontSize:12}}>Aucun flux — crée-en un dans Automatisation</div>
        ):(
          <div style={{display:'flex',flexDirection:'column',gap:6}}>
            {flows.map(f=>(
              <div key={f.id} onClick={()=>{setSelectedId(f.id);try{localStorage.setItem('pl.piauto.flow',f.id);}catch(e){}}}
                style={{padding:'9px 12px',borderRadius:7,border:'1px solid '+(selectedId===f.id?'#8957e5':'#30363d'),
                  background:selectedId===f.id?'rgba(137,87,229,.12)':'#0d1117',cursor:'pointer',
                  display:'flex',alignItems:'center',gap:10,transition:'border-color .15s'}}>
                <div style={{width:8,height:8,borderRadius:'50%',background:selectedId===f.id?'#8957e5':'#484f58',flex:'none'}} />
                <div>
                  <div style={{fontSize:13,color:'#e6edf3',fontWeight:selectedId===f.id?700:400}}>{f.name||'Sans nom'}</div>
                  <div style={{fontSize:10,color:'#484f58'}}>{(f.nodes||[]).length} blocs · {(f.edges||[]).length} connexions</div>
                </div>
              </div>
            ))}
          </div>
        )}
      </div>

      {/* ── iPhones cibles ── */}
      <div style={{background:'#161b22',border:'1px solid #30363d',borderRadius:10,padding:16,display:'flex',flexDirection:'column',gap:10}}>
        <div style={{display:'flex',alignItems:'center',justifyContent:'space-between'}}>
          <div style={{fontSize:11,color:'#8b949e',fontWeight:700,textTransform:'uppercase',letterSpacing:'.05em'}}>📱 iPhones cibles</div>
          <div style={{display:'flex',gap:6}}>
            <button onClick={()=>{
              // Sélectionne tout mais 1 seul iPhone par proxy (les doublons de proxy sont ignorés)
              const seen=new Set();const pick=new Set();let skipped=0;
              devices.forEach(d=>{
                if(d.proxy){if(seen.has(d.proxy)){skipped++;return;}seen.add(d.proxy);}
                pick.add(d.udid);
              });
              saveDeviceSel(pick);
              if(skipped)setProxyPopup({allSkipped:skipped});
            }}
              style={{fontSize:10,padding:'3px 8px',background:'#21262d',border:'1px solid #30363d',borderRadius:5,color:'#8b949e',cursor:'pointer'}}>Tout</button>
            <button onClick={()=>saveDeviceSel(new Set())}
              style={{fontSize:10,padding:'3px 8px',background:'#21262d',border:'1px solid #30363d',borderRadius:5,color:'#8b949e',cursor:'pointer'}}>Aucun</button>
          </div>
        </div>

        {devices.length===0?(
          <div style={{color:'#484f58',fontSize:12}}>Aucun iPhone connecté</div>
        ):(
          <div style={{display:'flex',flexDirection:'column',gap:4}}>
            {devices.map(d=>{
              const sel=selectedDevices.has(d.udid);
              const conflict=proxyConflicts.some(g=>g.find(x=>x.udid===d.udid));
              return(
                <div key={d.udid} onClick={()=>!batchRunning&&toggleDevice(d.udid)}
                  style={{display:'flex',alignItems:'center',gap:10,padding:'8px 10px',borderRadius:7,
                    border:'1px solid '+(sel?(conflict?'#f85149':'#3fb950'):'#21262d'),
                    background:sel?(conflict?'rgba(248,81,73,.08)':'rgba(63,185,80,.08)'):'#0d1117',
                    cursor:batchRunning?'default':'pointer',transition:'all .15s'}}>
                  <div style={{width:14,height:14,borderRadius:3,border:'2px solid '+(sel?'#3fb950':'#484f58'),
                    background:sel?'#3fb950':'transparent',flex:'none',display:'flex',alignItems:'center',justifyContent:'center'}}>
                    {sel&&<span style={{color:'#0d1117',fontSize:10,lineHeight:1,fontWeight:900}}>✓</span>}
                  </div>
                  <div style={{flex:1,minWidth:0}}>
                    <div style={{fontSize:12,color:d.tag?'#e6edf3':'#6e7681',fontWeight:sel?700:400,whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>
                      {d.tag||'iPhone sans nom'}
                    </div>
                    <div style={{fontSize:9.5,color:'#484f58',display:'flex',gap:6,alignItems:'center'}}>
                      <span className="mono">{d.udid.slice(0,12)}…</span>
                      {d.proxy&&<span style={{color:conflict?'#f85149':'#1dbab4'}}>🔌{conflict?' ⚠️ conflit proxy':''}</span>}
                    </div>
                  </div>
                  {/* état run en cours sur ce device */}
                  {batchRunning&&sel&&(
                    <span style={{width:7,height:7,borderRadius:'50%',background:'#3fb950',flex:'none',animation:'blink 1s infinite'}} />
                  )}
                </div>
              );
            })}
          </div>
        )}

        {/* Warning conflits proxy */}
        {proxyConflicts.length>0&&(
          <div style={{background:'rgba(248,81,73,.1)',border:'1px solid rgba(248,81,73,.3)',borderRadius:7,padding:'8px 10px',fontSize:11,color:'#f85149',display:'flex',gap:8,alignItems:'flex-start'}}>
            <span style={{flex:'none'}}>⚠️</span>
            <div>
              <div style={{fontWeight:700,marginBottom:2}}>Conflit proxy détecté</div>
              {proxyConflicts.map((g,i)=>(
                <div key={i} style={{fontSize:10,color:'#ffa657'}}>
                  Proxy {g[0].proxy.slice(0,8)} : {g.map(d=>d.tag||d.name||d.udid.slice(0,8)).join(', ')} — ne peuvent pas tourner simultanément
                </div>
              ))}
            </div>
          </div>
        )}

        {/* Pop-up blocage proxy partagé */}
        {proxyPopup&&(
          <div onClick={()=>setProxyPopup(null)}
            style={{position:'fixed',inset:0,background:'rgba(1,4,9,.7)',zIndex:9999,display:'flex',alignItems:'center',justifyContent:'center',padding:20}}>
            <div onClick={e=>e.stopPropagation()}
              style={{background:'#161b22',border:'1px solid #f85149',borderRadius:14,padding:'26px 24px',maxWidth:400,textAlign:'center',boxShadow:'0 12px 48px rgba(0,0,0,.6)'}}>
              <div style={{fontSize:36,marginBottom:10}}>🔌⚠️</div>
              {proxyPopup.allSkipped?(
                <>
                  <div style={{fontSize:15,fontWeight:800,color:'#e6edf3',marginBottom:10}}>Proxys en double ignorés</div>
                  <div style={{fontSize:12.5,color:'#8b949e',lineHeight:1.55,marginBottom:20}}>
                    <b style={{color:'#e6edf3'}}>{proxyPopup.allSkipped}</b> iPhone(s) partageant un proxy déjà utilisé n'ont pas été sélectionnés, pour éviter les interférences. Un seul iPhone par proxy peut tourner à la fois.
                  </div>
                </>
              ):(
                <>
                  <div style={{fontSize:15,fontWeight:800,color:'#e6edf3',marginBottom:10}}>Proxy déjà utilisé</div>
                  <div style={{fontSize:12.5,color:'#8b949e',lineHeight:1.55,marginBottom:20}}>
                    L'iPhone <b style={{color:'#e6edf3'}}>{proxyPopup.taken}</b> utilise déjà le proxy <b style={{color:'#1dbab4'}}>{String(proxyPopup.proxy).slice(0,18)}</b>.<br/>
                    Tu ne peux pas lancer <b style={{color:'#e6edf3'}}>{proxyPopup.tried}</b> sur le <b>même proxy</b> en même temps — choisis un iPhone avec un <b style={{color:'#e6edf3'}}>autre proxy</b> (proxy 2, proxy 3…) pour éviter les interférences.
                  </div>
                </>
              )}
              <button onClick={()=>setProxyPopup(null)}
                style={{padding:'9px 26px',background:'#21262d',border:'1px solid #30363d',borderRadius:9,color:'#e6edf3',fontWeight:700,fontSize:12.5,cursor:'pointer'}}>Compris</button>
            </div>
          </div>
        )}

        {selectedDevices.size>0&&(
          <div style={{fontSize:10,color:'#8b949e',textAlign:'center'}}>
            {selectedDevices.size} iPhone{selectedDevices.size>1?'s':''} sélectionné{selectedDevices.size>1?'s':''}
            {selectedDevices.size>1?' → runs en parallèle':''}
          </div>
        )}
      </div>

      {/* Config */}
      <div style={{background:'#161b22',border:'1px solid #30363d',borderRadius:10,padding:16,display:'flex',gap:12,flexWrap:'wrap',alignItems:'flex-end'}}>
        <div style={{flex:1,minWidth:100}}>
          <div style={{fontSize:11,color:'#8b949e',marginBottom:6,fontWeight:700}}>Nombre de runs</div>
          <input type="number" min="1" max="999" value={batchCount} onChange={e=>setBatchCount(e.target.value)}
            style={{width:'100%',background:'#0d1117',border:'1px solid #30363d',borderRadius:7,color:'#e6edf3',fontSize:14,padding:'7px 10px',boxSizing:'border-box'}} />
        </div>
        <div style={{flex:1,minWidth:100}}>
          <div style={{fontSize:11,color:'#8b949e',marginBottom:6,fontWeight:700}}>Cooldown (secondes)</div>
          <input type="number" min="0" max="3600" value={batchCooldown} onChange={e=>setBatchCooldown(e.target.value)}
            style={{width:'100%',background:'#0d1117',border:'1px solid #30363d',borderRadius:7,color:'#e6edf3',fontSize:14,padding:'7px 10px',boxSizing:'border-box'}} />
        </div>
        <button onClick={()=>{const n=!soundEnabledRef.current;soundEnabledRef.current=n;setSoundEnabled(n);}}
          title={soundEnabled?'Couper le son':'Activer le son'}
          style={{padding:'7px 14px',background:'#0d1117',border:'1px solid #30363d',borderRadius:7,color:soundEnabled?'#e6edf3':'#484f58',fontSize:18,cursor:'pointer'}}>
          {soundEnabled?'🔊':'🔇'}
        </button>
      </div>

      {/* Barre de progression */}
      {prog&&(
        <div style={{background:'#161b22',border:'1px solid #30363d',borderRadius:10,padding:16,display:'flex',flexDirection:'column',gap:10}}>
          <div style={{display:'flex',justifyContent:'space-between',alignItems:'center'}}>
            <span style={{fontSize:14,fontWeight:700,color:'#8957e5'}}>Run {prog.cur}/{prog.total}</span>
            <span style={{fontSize:13,color:'#8b949e'}}>{prog.start?_fmtDur(Date.now()-prog.start):''} écoulé</span>
          </div>
          <div style={{height:6,borderRadius:3,background:'#21262d',overflow:'hidden'}}>
            <div style={{height:'100%',width:((prog.cur/prog.total)*100)+'%',background:batchRunning?'#8957e5':'#3fb950',transition:'width .4s'}} />
          </div>
          {prog.runTimes&&prog.runTimes.length>0&&(
            <div style={{display:'flex',flexWrap:'wrap',gap:5}}>
              {prog.runTimes.map((r,i)=>(
                <div key={i} title={'Run '+(i+1)+': '+(r.ok?'✅':'❌')+' '+_fmtDur(r.dur)}
                  style={{width:14,height:14,borderRadius:'50%',background:r.ok?'#3fb950':'#f85149',cursor:'default'}} />
              ))}
            </div>
          )}
        </div>
      )}

      {/* Bouton lancer/stop */}
      <div style={{display:'flex',gap:10,flexDirection:'column'}}>
        <div style={{display:'flex',gap:10}}>
          {!batchRunning?(
            <button onClick={startBatch}
              disabled={!selectedId||selectedDevices.size===0||proxyConflicts.length>0}
              title={proxyConflicts.length>0?'Résous les conflits proxy avant de lancer':selectedDevices.size===0?'Sélectionne au moins un iPhone':''}
              style={{flex:1,padding:'11px 0',
                background:(!selectedId||selectedDevices.size===0||proxyConflicts.length>0)?'#21262d':'#8957e5',
                border:proxyConflicts.length>0?'1px solid #f85149':'none',borderRadius:8,
                color:(!selectedId||selectedDevices.size===0||proxyConflicts.length>0)?'#484f58':'#fff',
                fontWeight:700,fontSize:13,cursor:(!selectedId||selectedDevices.size===0||proxyConflicts.length>0)?'default':'pointer'}}>
              {proxyConflicts.length>0?'⚠️ Conflit proxy':
                selectedDevices.size===0?'Sélectionne un iPhone':
                '▶ Lancer ×'+batchCount+(selectedDevices.size>1?' sur '+selectedDevices.size+' iPhones':'')+(flow?' — '+flow.name:'')}
            </button>
          ):(
            <>
              <button onClick={toggleBatchPause}
                style={{flex:1,padding:'11px 0',background:batchPaused?'#388bfd':'#d29922',border:'none',borderRadius:8,color:'#fff',fontWeight:700,fontSize:14,cursor:'pointer'}}>
                {batchPaused?'▶ Reprendre':'⏸ Pause'}
              </button>
              <button onClick={stopBatch}
                style={{flex:1,padding:'11px 0',background:'#da3633',border:'none',borderRadius:8,color:'#fff',fontWeight:700,fontSize:14,cursor:'pointer'}}>
                ■ Stop
              </button>
            </>
          )}
        </div>
        {selectedId&&(
          <button onClick={()=>{
              try{localStorage.setItem('pl.auto.openflow',selectedId);}catch(e){}
              if(onGo)onGo('automation');
            }}
            style={{width:'100%',padding:'8px 0',background:'#21262d',border:'1px solid #30363d',borderRadius:8,
              color:'#8b949e',fontWeight:600,fontSize:12,cursor:'pointer',display:'flex',alignItems:'center',justifyContent:'center',gap:6}}
            onMouseEnter={e=>{e.currentTarget.style.borderColor='#8957e5';e.currentTarget.style.color='#e6edf3';}}
            onMouseLeave={e=>{e.currentTarget.style.borderColor='#30363d';e.currentTarget.style.color='#8b949e';}}>
            🔍 Voir le flux{batchRunning?' (en cours)':''}
          </button>
        )}
      </div>

      {/* Log */}
      <div style={{background:'#161b22',border:'1px solid #30363d',borderRadius:10,display:'flex',flexDirection:'column',minHeight:160}}>
        <div style={{display:'flex',alignItems:'center',justifyContent:'space-between',padding:'8px 12px 4px',borderBottom:'1px solid #21262d',flex:'none'}}>
          <div style={{fontSize:10,color:'#8b949e',fontWeight:700,textTransform:'uppercase',letterSpacing:'.05em'}}>📋 Log</div>
          <button onClick={()=>setLogs([])} style={{fontSize:10,color:'#484f58',background:'none',border:'none',cursor:'pointer'}}>Effacer</button>
        </div>
        <div style={{flex:1,overflowY:'auto',padding:'6px 12px 10px',maxHeight:280,display:'flex',flexDirection:'column',gap:0}}>
          {logs.length===0
            ?<div style={{fontSize:11,color:'#484f58',fontFamily:'monospace',marginTop:6}}>— vide —</div>
            :logs.map(l=><div key={l.id} style={{fontSize:10,fontFamily:'monospace',color:l.color,padding:'1px 0',lineHeight:1.6,wordBreak:'break-all'}}>{l.msg}</div>)
          }
          <div id="pia-log-bottom" />
        </div>
      </div>
      </div>{/* fin colonne gauche */}

      {/* ── Colonne droite : aperçu iPhone ── */}
      {showPreview&&(
        <div style={{flex:'none',width:280,display:'flex',flexDirection:'column',gap:8,position:'sticky',top:0}}>
          <div style={{background:'#161b22',border:'1px solid #30363d',borderRadius:10,overflow:'hidden'}}>
            <div style={{display:'flex',alignItems:'center',justifyContent:'space-between',padding:'7px 10px',borderBottom:'1px solid #21262d'}}>
              <span style={{fontSize:9,color:'#8b949e',fontWeight:700,textTransform:'uppercase',letterSpacing:'.05em'}}>📱 iPhone Live</span>
              <div style={{display:'flex',gap:6,alignItems:'center'}}>
                <select value={previewUdid} onChange={e=>{setPreviewUdid(e.target.value);setPreviewKey(k=>k+1);try{localStorage.setItem('pl.auto.dev',e.target.value);}catch(e){}}}
                  style={{background:'#0d1117',border:'1px solid #30363d',borderRadius:5,color:'#e6edf3',fontSize:9.5,padding:'2px 5px',maxWidth:120}}>
                  <option value=''>Sélectionner…</option>
                  {devices.length>0
                    ?devices.map(d=><option key={d.udid} value={d.udid}>{d.tag||d.name||d.udid.slice(0,8)}</option>)
                    :[<option key='x' value='iphonex'>iPhone #2</option>]
                  }
                </select>
                <button onClick={()=>setPreviewKey(k=>k+1)} title="Rafraîchir"
                  style={{background:'none',border:'none',color:'#484f58',cursor:'pointer',fontSize:12,padding:0,lineHeight:1}}
                  onMouseEnter={e=>e.currentTarget.style.color='#58a6ff'}
                  onMouseLeave={e=>e.currentTarget.style.color='#484f58'}>↻</button>
              </div>
            </div>
            <div style={{background:'#0d1117'}}>
              <AutoLivePhone key={previewKey} udid={previewUdid} width={280} />
            </div>
          </div>
        </div>
      )}
      </div>{/* fin layout flex */}
    </div>
  );
}

window.PilotageAuto=PilotageAuto;
