/* ============================================================
   FLOWSHIELD — Blue Team (CIS Benchmark hardening)
   ============================================================ */
function BenchCard({ b, onClick, active }) {
  const ref = useRefL(null);
  const onMove = (e) => { const el=ref.current; if(!el)return; const r=el.getBoundingClientRect(); el.style.setProperty('--mx',((e.clientX-r.left)/r.width)*100+'%'); el.style.setProperty('--my',((e.clientY-r.top)/r.height)*100+'%'); };
  return (
    <div ref={ref} onMouseMove={onMove} onClick={onClick} className="tool-card theme-blue" style={{ flexDirection:'column', borderColor: active?'var(--blue)':'var(--line)', boxShadow: active?'0 0 0 1px var(--blue), 0 14px 40px -22px var(--blue-glow)':'none' }}>
      <span className="bracket tl" /><span className="bracket tr" /><span className="bracket bl" /><span className="bracket br" />
      <div style={{ display:'flex', gap:13, alignItems:'center', width:'100%' }}>
        <div className="tool-ico"><Icon name={b.icon} size={24} /></div>
        <div style={{ flex:1, minWidth:0 }}>
          <div className="tool-title" style={{ marginBottom:2, fontSize:14 }}>{b.name}</div>
          <div className="mono" style={{ fontSize:10.5, color:'var(--txt-mute)', whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{b.target}</div>
        </div>
        <Gauge value={b.score} size={56} thick={6} color={b.score>=80?'var(--ok)':b.score>=70?'var(--amber)':'var(--red)'} sub="" />
      </div>
      <div style={{ display:'flex', gap:6, marginTop:16, width:'100%' }}>
        <span className="badge sev-ok" style={{ flex:1, justifyContent:'center', padding:'4px' }}>✓ {b.pass}</span>
        <span className="badge sev-crit" style={{ flex:1, justifyContent:'center', padding:'4px' }}>✕ {b.fail}</span>
        <span className="badge sev-info" style={{ flex:1, justifyContent:'center', padding:'4px' }}>N/A {b.na}</span>
        <span className="badge" style={{ flex:'0 0 auto', justifyContent:'center', padding:'4px 8px', color:'var(--blue)', borderColor:'color-mix(in srgb,var(--blue) 35%,transparent)', background:'var(--blue-soft)' }}>{b.level}</span>
      </div>
    </div>
  );
}

function ControlRow({ c, onRemediate, remediated }) {
  const [open, setOpen] = useStateL(false);
  const status = remediated ? 'pass' : c.status;
  return (
    <div className="panel" style={{ marginBottom:8, borderColor: status==='fail'?'color-mix(in srgb,var(--red) 30%,var(--line))':'var(--line)', overflow:'hidden' }}>
      <div onClick={()=>c.remediation && setOpen(o=>!o)} style={{ display:'flex', alignItems:'center', gap:14, padding:'13px 16px', cursor: c.remediation?'pointer':'default' }}>
        <div style={{ width:30, height:30, borderRadius:7, display:'grid', placeItems:'center', flexShrink:0, color: status==='pass'?'var(--ok)':'var(--red)', background: status==='pass'?'color-mix(in srgb,var(--ok) 12%,transparent)':'var(--red-soft)', border:'1px solid '+(status==='pass'?'color-mix(in srgb,var(--ok) 30%,transparent)':'color-mix(in srgb,var(--red) 30%,transparent)') }}>
          <Icon name={status==='pass'?'check':'x'} size={16} />
        </div>
        <span className="mono" style={{ fontSize:11.5, color:'var(--blue)', flexShrink:0, width:46 }}>{c.id}</span>
        <div style={{ flex:1, minWidth:0 }}>
          <div style={{ fontSize:13, color:'var(--txt-hi)', fontWeight:500 }}>{c.title}</div>
          <div className="mono" style={{ fontSize:10.5, color:'var(--txt-mute)', marginTop:2 }}>{c.cat} · {c.level}</div>
        </div>
        <Sev level={c.sev} />
        {c.auto && <span className="badge sev-ok" style={{ padding:'2px 8px' }} title="Auto-remediable">AUTO</span>}
        {c.remediation && <Icon name="chevrond" size={16} style={{ color:'var(--txt-mute)', transform: open?'rotate(180deg)':'none', transition:'transform .2s' }} />}
      </div>
      {open && c.remediation && (
        <div className="enter" style={{ padding:'0 16px 16px 16px', borderTop:'1px solid var(--line)' }}>
          <div className="mono upper" style={{ fontSize:10, color:'var(--txt-mute)', margin:'14px 0 8px' }}>Remediación sugerida</div>
          <div className="terminal" style={{ fontSize:11.5, padding:'12px 14px' }}>
            <span className="t-dim">$ </span><span className="t-acc">{c.remediation}</span>
          </div>
          {status==='fail' && (
            <div style={{ display:'flex', gap:10, marginTop:12 }}>
              <Btn variant="solid" size="sm" icon="bolt" className="theme-blue" onClick={()=>onRemediate(c.id)}>Aplicar remediación</Btn>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

function btScoreColor(v) { return v>=80?'var(--ok)':v>=70?'var(--amber)':'var(--red)'; }
function btProfLevel(title) {
  const t = (title||'').toLowerCase();
  const m = t.match(/level\s*([12])/) || t.match(/\bl([12])\b/);
  if (m) return 'L'+m[1];
  if (t.includes('ens')) return 'ENS';
  if (t.includes('stig')) return 'STIG';
  if (t.includes('pci')) return 'PCI';
  return 'CIS';
}

/* A real failed/error rule (from a stored scan) with its remediation. */
function RemediationRow({ f }) {
  const [open, setOpen] = useStateL(false);
  const sev = window.normSev(f.severity);
  return (
    <div className="panel" style={{ marginBottom:8, borderColor:'color-mix(in srgb,var(--red) 30%,var(--line))', overflow:'hidden' }}>
      <div onClick={()=>setOpen(o=>!o)} style={{ display:'flex', alignItems:'center', gap:14, padding:'13px 16px', cursor:'pointer' }}>
        <div style={{ width:30, height:30, borderRadius:7, display:'grid', placeItems:'center', flexShrink:0, color:'var(--red)', background:'var(--red-soft)', border:'1px solid color-mix(in srgb,var(--red) 30%,transparent)' }}>
          <Icon name="x" size={16} />
        </div>
        <div style={{ flex:1, minWidth:0 }}>
          <div style={{ fontSize:13, color:'var(--txt-hi)', fontWeight:500 }}>{f.title}</div>
          <div className="mono" style={{ fontSize:10.5, color:'var(--txt-mute)', marginTop:2, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{f.rule_id}{f.result==='error'?' · error':''}</div>
        </div>
        <Sev level={sev} />
        <Icon name="chevrond" size={16} style={{ color:'var(--txt-mute)', flexShrink:0, transform: open?'rotate(180deg)':'none', transition:'transform .2s' }} />
      </div>
      {open && (
        <div className="enter" style={{ padding:'0 16px 16px 16px', borderTop:'1px solid var(--line)' }}>
          <div className="mono upper" style={{ fontSize:10, color:'var(--txt-mute)', margin:'14px 0 8px' }}>Remediación</div>
          <div className="terminal" style={{ fontSize:11.5, padding:'12px 14px', whiteSpace:'pre-wrap', lineHeight:1.55 }}>
            {f.remediation
              ? <span className="t-acc">{f.remediation}</span>
              : <span className="t-dim">Sin script de remediación en el contenido SCAP. Abre el informe HTML para el detalle completo.</span>}
          </div>
        </div>
      )}
    </div>
  );
}

const SEV_BLOCKS = [['critical','Críticos'],['high','Altos'],['medium','Medios'],['low','Bajos'],['info','Informativos']];

/* Vista de histórico completo: todos los escaneos por sistema, con borrado. */
function DriftPanel({ data }) {
  if (data === 'loading') return <div className="mono" style={{ fontSize:11, color:'var(--txt-mute)', padding:'10px 18px' }}>Comparando escaneos…</div>;
  if (data && data.__error) return <div className="mono" style={{ fontSize:11, color:'var(--red)', padding:'10px 18px' }}>{data.__error}</div>;
  if (!data) return null;
  const d = data.score_delta;
  const dColor = d > 0 ? 'var(--ok)' : d < 0 ? 'var(--red)' : 'var(--txt-dim)';
  const Sec = ({ title, rows, color }) => !rows.length ? null : (
    <div style={{ marginTop:8 }}>
      <div className="mono" style={{ fontSize:10, color, marginBottom:4 }}>{title} · {rows.length}</div>
      <div style={{ display:'grid', gap:3 }}>
        {rows.slice(0,12).map(r => (
          <div key={r.rule_id} style={{ display:'flex', gap:8, alignItems:'center', fontSize:11.5 }}>
            <Sev level={window.normSev(r.severity)} />
            <span style={{ color:'var(--txt)', flex:1, minWidth:0, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{r.title}</span>
          </div>
        ))}
        {rows.length > 12 && <div className="mono" style={{ fontSize:10, color:'var(--txt-mute)' }}>+{rows.length-12} más…</div>}
      </div>
    </div>
  );
  return (
    <div style={{ padding:'12px 18px', borderTop:'1px solid var(--line)', background:'var(--bg-void)' }}>
      <div style={{ display:'flex', alignItems:'center', gap:12, flexWrap:'wrap', marginBottom:6 }}>
        <span className="eyebrow" style={{ margin:0 }}>Drift vs escaneo anterior</span>
        <span className="mono" style={{ fontSize:12, color:'var(--txt-hi)' }}>{data.score_before}% → {data.score_after}%</span>
        <span className="badge" style={{ padding:'1px 8px', color:dColor, borderColor:'currentColor' }}>{d>0?'+':''}{d} pts</span>
        <span className="badge sev-ok" style={{ padding:'1px 8px' }}>resueltas {data.counts.fixed}</span>
        <span className="badge sev-crit" style={{ padding:'1px 8px' }}>nuevas {data.counts.regressed}</span>
        <span className="badge sev-info" style={{ padding:'1px 8px' }}>persisten {data.counts.still_failing}</span>
      </div>
      <Sec title="REGLAS RESUELTAS" rows={data.fixed} color="var(--ok)" />
      <Sec title="NUEVAS / REGRESIÓN" rows={data.regressed} color="var(--red)" />
      <Sec title="SIGUEN FALLANDO" rows={data.still_failing} color="var(--txt-dim)" />
    </div>
  );
}

function HistoryView({ scans, onBack, onDelete }) {
  const [open, setOpen] = useStateL(null);
  const [cmp, setCmp] = useStateL(null);   // { id, data } drift for a scan row
  const doCompare = async (current, base) => {
    if (cmp && cmp.id === current.scan_id) { setCmp(null); return; }
    setCmp({ id: current.scan_id, data: 'loading' });
    try {
      const data = await window.fsApi('/api/blueteam/scans/compare?base=' + encodeURIComponent(base.scan_id) + '&current=' + encodeURIComponent(current.scan_id), { method:'GET' });
      setCmp({ id: current.scan_id, data });
    } catch (e) {
      setCmp({ id: current.scan_id, data: { __error: e.message || 'No se pudo comparar.' } });
    }
  };
  const groups = {};
  (scans||[]).forEach(s => {
    const key = (s.datastream||'?') + ' · ' + (s.target||'?');
    if (!groups[key]) groups[key] = { key, os:s.os||s.datastream, datastream:s.datastream, target:s.target, scans:[] };
    groups[key].scans.push(s);
  });
  const systems = Object.values(groups);
  return (
    <div className="enter theme-blue">
      <PageHeader title="Blue Team · Histórico" crumb="BLUE TEAM / HISTÓRICO"
        sub="Todos los escaneos OpenSCAP registrados, agrupados por sistema. Borra los que ya no necesites para mantener limpio el panel."
        right={<Btn variant="ghost" onClick={onBack}>← Volver al panel</Btn>} />
      {!systems.length ? (
        <Empty icon="report" title="Sin escaneos en el histórico" sub="Lanza un escaneo con «Nuevo scan» y aquí quedará registrado." />
      ) : (
        <div style={{ display:'grid', gap:10 }}>
          {systems.map(sys => {
            const latest = sys.scans[0]; const sc=(latest.summary&&latest.summary.score)||0;
            const isOpen = open===sys.key;
            return (
              <div key={sys.key} className="panel" style={{ overflow:'hidden', borderColor: isOpen?'var(--blue)':'var(--line)' }}>
                <div onClick={()=>setOpen(isOpen?null:sys.key)} style={{ display:'flex', alignItems:'center', gap:14, padding:'14px 18px', cursor:'pointer' }}>
                  <div className="tool-ico"><Icon name="server" size={20} /></div>
                  <div style={{ flex:1, minWidth:0 }}>
                    <div style={{ fontSize:14, color:'var(--txt-hi)', fontWeight:600 }}>{sys.os}</div>
                    <div className="mono" style={{ fontSize:10.5, color:'var(--txt-mute)', whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{sys.target} · {sys.datastream}</div>
                  </div>
                  <span className="badge" style={{ flexShrink:0, padding:'3px 9px', color:'var(--txt-dim)' }}>{sys.scans.length} escaneo{sys.scans.length!==1?'s':''}</span>
                  <Gauge value={sc} size={48} thick={5} color={btScoreColor(sc)} sub="" />
                  <Icon name="chevrond" size={16} style={{ color:'var(--txt-mute)', flexShrink:0, transform:isOpen?'rotate(180deg)':'none', transition:'transform .2s' }} />
                </div>
                {isOpen && (
                  <div className="enter">
                    {sys.scans.map((s, i) => { const su=s.summary||{}; const ssc=su.score||0;
                      const prev = sys.scans[i+1];   // older scan (list is newest-first)
                      const canCompare = prev && s.findings_available && prev.findings_available;
                      const showDrift = cmp && cmp.id === s.scan_id;
                      return (
                      <div key={s.scan_id}>
                      <div style={{ display:'flex', alignItems:'center', gap:12, padding:'12px 18px', borderTop:'1px solid var(--line)', flexWrap:'wrap' }}>
                        <div style={{ flex:1, minWidth:180 }}>
                          <div style={{ fontSize:12.5, color:'var(--txt-hi)' }}>{fmtScanDate(s.ts)}
                            {s.tailored && <span className="badge sev-info" style={{ marginLeft:8, padding:'1px 7px' }}>tailored</span>}
                            {su.remediated && <span className="badge sev-ok" style={{ marginLeft:6, padding:'1px 7px' }}>remediado</span>}
                          </div>
                          <div className="mono" style={{ fontSize:10.5, color:'var(--txt-mute)', marginTop:2, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{s.profile_title||s.profile}</div>
                        </div>
                        <div style={{ display:'flex', gap:6 }}>
                          <span className="badge sev-ok" style={{ padding:'2px 7px' }}>✓ {su.pass||0}</span>
                          <span className="badge sev-crit" style={{ padding:'2px 7px' }}>✕ {su.fail||0}</span>
                          <span className="badge sev-info" style={{ padding:'2px 7px' }}>N/A {su.notapplicable||0}</span>
                        </div>
                        <div className="disp" style={{ fontSize:15, fontWeight:700, width:48, textAlign:'right', color:btScoreColor(ssc) }}>{ssc}%</div>
                        {canCompare && <Btn variant="ghost" size="sm" icon="reload" onClick={()=>doCompare(s, prev)} title="Comparar con el escaneo anterior (drift)">{showDrift?'Ocultar':'Comparar'}</Btn>}
                        <Btn variant="ghost" size="sm" icon="report" onClick={()=>s.report_available && window.open(s.report_url,'_blank')} style={{ opacity:s.report_available?1:0.4 }} title={s.report_available?'Abrir informe HTML':'Informe no disponible'}>Reporte</Btn>
                        <Btn variant="ghost" size="sm" icon="x" onClick={()=>onDelete(s.scan_id)} title="Eliminar escaneo">Borrar</Btn>
                      </div>
                      {showDrift && <DriftPanel data={cmp.data} />}
                      </div>
                    );})}
                  </div>
                )}
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

function BlueTeam() {
  const [mode, setMode] = useStateL('panel');
  const [scans, setScans] = useStateL(null);        // null = cargando
  const [selKey, setSelKey] = useStateL('');
  const [findings, setFindings] = useStateL(null);  // findings reales del último scan del sistema seleccionado
  const [findingsBusy, setFindingsBusy] = useStateL(false);
  const [filter, setFilter] = useStateL('all');     // filtro (sistemas reales)
  const [fixed, setFixed] = useStateL([]);          // controles remediados (sistemas de ejemplo)
  const [dfilter, setDfilter] = useStateL('all');   // filtro (sistemas de ejemplo)

  // Cargar el historial real (panel e histórico); se refresca al volver del scan.
  useEffectL(() => {
    if (mode === 'scan') return;
    let alive = true;
    window.fsApi('/api/blueteam/scans', { method:'GET' })
      .then(d => { if(alive) setScans(d.scans || []); })
      .catch(()=>{ if(alive) setScans([]); });
    return () => { alive = false; };
  }, [mode]);

  // Sistemas REALES: agrupar el historial por sistema (DataStream + objetivo).
  const groups = {};
  (scans||[]).forEach(s => {
    const key = (s.datastream||'?') + ' · ' + (s.target||'?');
    if (!groups[key]) groups[key] = { key, os:s.os||s.datastream, datastream:s.datastream, target:s.target, scans:[] };
    groups[key].scans.push(s);
  });
  const realSystems = Object.values(groups).slice(0, 8).map(sys => {
    const latest = sys.scans[0]; const su = latest.summary || {};
    return { ...sys, _demo:false, latest, icon:'server', name:sys.os,
      target:(sys.target||'')+' · '+(sys.datastream||''),
      score:su.score||0, pass:su.pass||0, fail:su.fail||0, na:su.notapplicable||0,
      level:btProfLevel(latest.profile_title||latest.profile) };
  });
  // Sistemas de EJEMPLO (variedad: Ubuntu/Windows/Cisco…) desde el catálogo demo.
  const demoSystems = (window.CIS_BENCHMARKS||[]).map(b => ({ ...b, _demo:true, key:'demo:'+b.id }));
  const systems = [...realSystems, ...demoSystems].slice(0, 9);   // máx 9 tarjetas → cuadrícula 3×3
  const sel = systems.find(s=>s.key===selKey) || systems[0] || null;

  // Borrar un escaneo del histórico (usado desde el dashboard y desde el histórico).
  const delScan = async (sid) => {
    if (!sid || !window.confirm('¿Eliminar este escaneo del histórico? También se borrará su informe.')) return;
    try {
      await window.fsApi('/api/blueteam/scans/'+encodeURIComponent(sid), { method:'DELETE' });
      setScans(xs => (xs||[]).filter(s => s.scan_id !== sid));
    } catch(e) { /* el escaneo desaparece igualmente al refrescar */ }
  };

  // Al cambiar de tarjeta, resetear el estado de remediación de ejemplo.
  useEffectL(() => { setFixed([]); setDfilter('all'); }, [sel && sel.key]);

  // Sistemas reales: cargar (bajo demanda) las reglas fallidas del último escaneo.
  const selScanId = (sel && !sel._demo && sel.latest) ? sel.latest.scan_id : '';
  useEffectL(() => {
    setFindings(null); setFilter('all');
    if (!sel || sel._demo || !sel.latest) return;
    if (!sel.latest.findings_available) { setFindings([]); return; }
    let alive = true; setFindingsBusy(true);
    window.fsApi('/api/blueteam/scans/'+encodeURIComponent(selScanId)+'/findings', { method:'GET' })
      .then(d => { if(alive) setFindings(d.findings || []); })
      .catch(()=>{ if(alive) setFindings([]); })
      .finally(()=>{ if(alive) setFindingsBusy(false); });
    return () => { alive = false; };
  }, [sel && sel.key, selScanId]);

  if (mode === 'advisor') {
    return (
      <div className="enter theme-blue">
        <button onClick={() => setMode('panel')} style={{ display:'inline-flex', alignItems:'center', gap:7, background:'none', border:'none', color:'var(--txt-dim)', cursor:'pointer', fontSize:12.5, marginBottom:18, fontFamily:'var(--font-mono)' }}>
          ← volver al panel
        </button>
        <AiAdvisor faction="blueteam" />
      </div>
    );
  }
  if (mode === 'scan') return <ScapWorkbench bench={sel||{}} onBack={()=>setMode('panel')} />;
  if (mode === 'history') return <HistoryView scans={scans||[]} onBack={()=>setMode('panel')} onDelete={delScan} />;

  // Posture global (datos reales).
  const totalScans = (scans||[]).length;
  const avgScore = realSystems.length ? Math.round(realSystems.reduce((a,s)=>a+(s.score||0),0)/realSystems.length) : 0;
  const totalFail = realSystems.reduce((a,s)=>a+(s.fail||0),0);

  // ----- helpers de detalle por tipo de sistema -------------------------
  const remediate = (id) => setFixed(f => [...f, id]);
  const demoControls = (window.CIS_CONTROLS||[]).filter(c => {
    const st = fixed.includes(c.id) ? 'pass' : c.status;
    if (dfilter==='fail') return st==='fail';
    if (dfilter==='pass') return st==='pass';
    return true;
  });
  const demoAutoFail = (window.CIS_CONTROLS||[]).filter(c=>c.auto&&c.status==='fail'&&!fixed.includes(c.id));
  const demoFailCount = (window.CIS_CONTROLS||[]).filter(c => c.status==='fail' && !fixed.includes(c.id)).length;
  const demoEff = sel && sel._demo ? Math.min(99, (sel.score||0) + fixed.length*2) : 0;
  // Agrupar controles demo por bloque/servicio (categoría).
  const demoBlocks = {};
  demoControls.forEach(c => { (demoBlocks[c.cat] = demoBlocks[c.cat] || []).push(c); });

  // Sistemas reales: reglas fallidas agrupadas por severidad (bloques).
  const fl = findings || [];

  return (
    <div className="enter theme-blue">
      <PageHeader title="Blue Team · Hardening" crumb="BLUE TEAM / CIS BENCHMARK"
        sub="Endurecimiento evaluado con OpenSCAP contra los CIS Benchmarks. Tus escaneos reales y sistemas de ejemplo, con historial y remediación por bloques."
        right={<>
        <button className="btn btn-ghost btn-sm" onClick={() => setMode('advisor')} style={{ display:'inline-flex', alignItems:'center', gap:6 }}>
          <Icon name="robot" size={14} /> Asesor IA
        </button>
        <Btn variant="ghost" icon="clock" onClick={()=>setMode('history')}>Histórico</Btn><Btn variant="primary" icon="bolt" className="theme-blue" onClick={()=>setMode('scan')} glitch>Nuevo scan</Btn></>} />

      {scans === null ? (
        <div className="panel" style={{ padding:'18px 22px', color:'var(--txt-mute)', fontSize:13 }}>Cargando escaneos…</div>
      ) : (<>
        {/* posture summary (reales) */}
        <div style={{ display:'grid', gridTemplateColumns:'repeat(4,1fr)', gap:16, marginBottom:30 }}>
          <Stat label="Sistemas reales" value={realSystems.length} icon="server" accent="var(--blue)" sub={totalScans+' escaneos'} />
          <Stat label="Cumplimiento medio" value={(realSystems.length?avgScore:0)+'%'} icon="shield" accent={btScoreColor(avgScore)} />
          <Stat label="No conformes (último)" value={totalFail} icon="x" accent="var(--red)" />
          <Stat label="Escaneos totales" value={totalScans} icon="clock" accent="var(--ok)" />
        </div>

        {/* tarjetas: reales + ejemplos */}
        <div className="eyebrow">Sistemas y benchmarks · {systems.filter(s=>!s._demo).length} reales · {systems.filter(s=>s._demo).length} de ejemplo</div>
        <div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill,minmax(330px,1fr))', gap:14, marginBottom:32 }}>
          {systems.map(b => <BenchCard key={b.key||b.id} b={b} active={sel && sel.key===b.key} onClick={()=>setSelKey(b.key)} />)}
        </div>

        {/* detalle del sistema seleccionado */}
        {sel && (
        <div className="panel" style={{ padding:0, overflow:'hidden' }}>
          <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', flexWrap:'wrap', gap:14, padding:'18px 22px', borderBottom:'1px solid var(--line)', background:'var(--bg-panel-2)' }}>
            <div style={{ display:'flex', gap:14, alignItems:'center' }}>
              <div className="tool-ico"><Icon name={sel.icon||'server'} size={22} /></div>
              <div>
                <div className="disp" style={{ fontSize:17, fontWeight:700, color:'var(--txt-hi)', display:'flex', alignItems:'center', gap:9 }}>
                  {sel.name}
                  {sel._demo && <span className="badge sev-info" style={{ padding:'1px 8px', fontSize:9.5 }}>EJEMPLO</span>}
                </div>
                <div className="mono" style={{ fontSize:11, color:'var(--txt-mute)', marginTop:2 }}>
                  {sel._demo ? `${sel.target} · ${sel.total} controles · perfil ${sel.level}`
                             : `${sel.target} · ${sel.scans.length} escaneo${sel.scans.length!==1?'s':''}`}
                </div>
              </div>
            </div>
            <div style={{ display:'flex', alignItems:'center', gap:18 }}>
              <div style={{ textAlign:'right' }}>
                <div className="disp" style={{ fontSize:24, fontWeight:700, color:btScoreColor(sel._demo?demoEff:sel.score) }}>{sel._demo?demoEff:sel.score}%</div>
                <div className="mono" style={{ fontSize:9.5, color:'var(--txt-mute)' }}>{sel._demo?'tras remediación':'último · '+fmtScanDate(sel.latest.ts)}</div>
              </div>
              {sel._demo ? (
                <div style={{ display:'flex', gap:5 }}>
                  {[['all','Todos'],['fail','Fallidos'],['pass','OK']].map(([k,l])=>(
                    <button key={k} onClick={()=>setDfilter(k)} className="seg" style={{ padding:'7px 13px', background: dfilter===k?'var(--blue-soft)':'var(--bg-void)', borderColor: dfilter===k?'var(--blue)':'var(--line-bright)', color: dfilter===k?'var(--blue)':'var(--txt-dim)' }}>{l}</button>
                  ))}
                </div>
              ) : (
                <Btn variant="ghost" size="sm" icon="report"
                     onClick={()=>sel.latest.report_available && window.open(sel.latest.report_url,'_blank')}
                     style={{ opacity: sel.latest.report_available?1:0.4 }}
                     title={sel.latest.report_available?'Abrir informe HTML del último escaneo':'Informe no disponible (re-lanza el escaneo)'}>Último informe</Btn>
              )}
            </div>
          </div>

          {sel._demo ? (
            /* ---- sistema de EJEMPLO: controles por bloque + remediación interactiva ---- */
            <div style={{ padding:'16px 22px' }}>
              {demoFailCount>0 && dfilter!=='pass' && (
                <div style={{ display:'flex', alignItems:'center', gap:12, padding:'12px 14px', marginBottom:14, background:'var(--blue-soft)', border:'1px solid color-mix(in srgb,var(--blue) 30%,transparent)', borderRadius:8 }}>
                  <Icon name="bolt" size={18} style={{ color:'var(--blue)' }} />
                  <span style={{ flex:1, fontSize:13, color:'var(--txt)' }}><strong style={{ color:'var(--txt-hi)' }}>{demoAutoFail.length} controles</strong> se pueden remediar automáticamente sin reinicio.</span>
                  <Btn variant="solid" size="sm" icon="bolt" className="theme-blue" onClick={()=>setFixed(f=>[...new Set([...f, ...demoAutoFail.map(c=>c.id)])])}>Remediar todo</Btn>
                </div>
              )}
              {Object.keys(demoBlocks).length ? Object.keys(demoBlocks).map(cat => (
                <div key={cat} style={{ marginBottom:14 }}>
                  <div className="mono upper" style={{ fontSize:10, color:'var(--txt-mute)', margin:'4px 2px 8px' }}>{cat} · {demoBlocks[cat].length}</div>
                  {demoBlocks[cat].map(c => <ControlRow key={c.id} c={c} remediated={fixed.includes(c.id)} onRemediate={remediate} />)}
                </div>
              )) : <Empty icon="check" title="Sin controles en este filtro" sub="Todos los controles cumplen el criterio seleccionado." />}
            </div>
          ) : (<>
            {/* ---- sistema REAL: historial + remediación por bloques de severidad ---- */}
            <div style={{ borderBottom:'1px solid var(--line)' }}>
              <div className="mono upper" style={{ fontSize:10, color:'var(--txt-mute)', padding:'12px 22px 2px' }}>Historial</div>
              {sel.scans.map(s => { const su=s.summary||{}; const ssc=su.score||0; return (
                <div key={s.scan_id} style={{ display:'flex', alignItems:'center', gap:12, padding:'10px 22px', flexWrap:'wrap' }}>
                  <div style={{ flex:1, minWidth:180 }}>
                    <div style={{ fontSize:12.5, color:'var(--txt-hi)' }}>{fmtScanDate(s.ts)}
                      {s.tailored && <span className="badge sev-info" style={{ marginLeft:8, padding:'1px 7px' }}>tailored</span>}
                      {su.remediated && <span className="badge sev-ok" style={{ marginLeft:6, padding:'1px 7px' }}>remediado</span>}
                    </div>
                    <div className="mono" style={{ fontSize:10.5, color:'var(--txt-mute)', marginTop:2, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{s.profile_title||s.profile}</div>
                  </div>
                  <div style={{ display:'flex', gap:6 }}>
                    <span className="badge sev-ok" style={{ padding:'2px 7px' }}>✓ {su.pass||0}</span>
                    <span className="badge sev-crit" style={{ padding:'2px 7px' }}>✕ {su.fail||0}</span>
                    <span className="badge sev-info" style={{ padding:'2px 7px' }}>N/A {su.notapplicable||0}</span>
                  </div>
                  <div className="disp" style={{ fontSize:15, fontWeight:700, width:48, textAlign:'right', color:btScoreColor(ssc) }}>{ssc}%</div>
                  <Btn variant="ghost" size="sm" icon="report" onClick={()=>s.report_available && window.open(s.report_url,'_blank')} style={{ opacity:s.report_available?1:0.4 }} title={s.report_available?'Abrir informe HTML':'Informe no disponible (re-lanza el escaneo)'}>Reporte</Btn>
                  <Btn variant="ghost" size="sm" icon="x" onClick={()=>delScan(s.scan_id)} title="Eliminar escaneo">Borrar</Btn>
                </div>
              );})}
            </div>

            <div className="mono upper" style={{ fontSize:10, color:'var(--txt-mute)', padding:'14px 22px 4px' }}>Remediación · último escaneo (por bloques de severidad)</div>
            <div style={{ padding:'4px 22px 18px' }}>
              {(findingsBusy || findings===null) ? (
                <div style={{ color:'var(--txt-mute)', fontSize:13, padding:'8px 0' }}>Cargando remediación…</div>
              ) : !sel.latest.findings_available ? (
                <div style={{ display:'flex', alignItems:'center', gap:12, padding:'12px 14px', background:'var(--bg-void)', border:'1px solid var(--line)', borderRadius:8, fontSize:12.5, color:'var(--txt-dim)' }}>
                  <Icon name="report" size={18} style={{ color:'var(--txt-mute)' }} />
                  <span style={{ flex:1 }}>Este escaneo se hizo antes de que se guardara el detalle por regla. Abre el <strong style={{ color:'var(--txt-hi)' }}>informe HTML</strong> para ver la remediación, o vuelve a lanzarlo para tenerla aquí.</span>
                </div>
              ) : !fl.length ? (
                <Empty icon="check" title="Sin reglas no conformes" sub="El último escaneo no encontró reglas incumplidas." />
              ) : (
                SEV_BLOCKS.map(([sv,label]) => {
                  const items = fl.filter(f => window.normSev(f.severity) === window.normSev(sv));
                  if (!items.length) return null;
                  return (
                    <div key={sv} style={{ marginBottom:14 }}>
                      <div className="mono upper" style={{ fontSize:10, color:'var(--txt-mute)', margin:'4px 2px 8px', display:'flex', alignItems:'center', gap:8 }}>
                        <Sev level={window.normSev(sv)} /> {label} · {items.length}
                      </div>
                      {items.map((f,i) => <RemediationRow key={(f.rule_id||'')+i} f={f} />)}
                    </div>
                  );
                })
              )}
            </div>
          </>)}
        </div>
        )}
      </>)}

      <style>{`.seg{font-family:var(--font-display);font-weight:600;font-size:12px;border:1px solid;border-radius:7px;cursor:pointer;transition:all .18s}`}</style>
    </div>
  );
}

function AuditStream() {
  const [lines, setLines] = useStateL([]);
  const ref = useRefL(null);
  useEffectL(() => {
    const seq = [
      { t:'> cis-cat-engine · cargando benchmark', c:'acc' },
      { t:'[*] comprobando sistema de ficheros … OK', c:'' },
      { t:'[*] comprobando servicios … 2 desviaciones', c:'warn' },
      { t:'[*] comprobando configuración SSH …', c:'' },
      { t:'[!] 5.2.4 PermitRootLogin = yes (FAIL)', c:'err' },
      { t:'[*] comprobando firewall … FAIL', c:'err' },
      { t:'[*] comprobando auditoría (auditd) … OK', c:'ok' },
      { t:'> evaluación completada · 170 desviaciones', c:'ok' },
    ];
    let i=0; const tick=()=>{ const cur=seq[i]; setLines(l=>[...l,cur]); i++; if(i<seq.length) setTimeout(tick,240); };
    tick();
  }, []);
  useEffectL(()=>{ if(ref.current) ref.current.scrollTop=ref.current.scrollHeight; });
  return <div ref={ref} style={{ height:'100%', overflow:'auto' }}>{lines.map((l,i)=>l&&<div key={i} className={'t-'+(l.c||'')}>{l.t}</div>)}<div className="cursor-blink" /></div>;
}

/* ============================================================
   Historial de escaneos reales — agrupado por sistema (DataStream
   + objetivo). Cada entrada abre su informe HTML. Se monta al volver
   al panel, así que refleja el último escaneo lanzado.
   ============================================================ */
function fmtScanDate(iso) {
  try { return new Date(iso).toLocaleString('es-ES', { dateStyle:'medium', timeStyle:'short' }); }
  catch { return iso || '—'; }
}

function ScanHistory() {
  const [scans, setScans] = useStateL(null);   // null = cargando
  const [open, setOpen] = useStateL(null);      // clave del sistema desplegado

  useEffectL(() => {
    let alive = true;
    window.fsApi('/api/blueteam/scans', { method:'GET' })
      .then(d => { if(alive) setScans(d.scans || []); })
      .catch(()=>{ if(alive) setScans([]); });
    return () => { alive = false; };
  }, []);

  const scoreColor = (v) => v>=80?'var(--ok)':v>=70?'var(--amber)':'var(--red)';

  if (scans === null) {
    return (<>
      <div className="eyebrow" style={{ marginTop:32 }}>Historial de escaneos</div>
      <div className="panel" style={{ padding:'18px 22px', color:'var(--txt-mute)', fontSize:13 }}>Cargando historial…</div>
    </>);
  }

  // Agrupar por sistema: DataStream + objetivo escaneado.
  const groups = {};
  scans.forEach(s => {
    const key = (s.datastream||'?') + ' · ' + (s.target||'?');
    if (!groups[key]) groups[key] = { key, os:s.os||s.datastream, datastream:s.datastream, target:s.target, scans:[] };
    groups[key].scans.push(s);
  });
  const systems = Object.values(groups);

  return (
    <>
      <div className="eyebrow" style={{ marginTop:32 }}>Historial de escaneos</div>
      {!systems.length ? (
        <Empty icon="report" title="Aún no hay escaneos reales" sub="Lanza un escaneo con «Nuevo scan» y aquí aparecerá su historial por sistema, con acceso al informe." />
      ) : (
        <div style={{ display:'grid', gap:10 }}>
          {systems.map(sys => {
            const latest = sys.scans[0];
            const sc = (latest.summary && latest.summary.score) || 0;
            const isOpen = open === sys.key;
            return (
              <div key={sys.key} className="panel" style={{ overflow:'hidden', borderColor: isOpen?'var(--blue)':'var(--line)' }}>
                <div onClick={()=>setOpen(isOpen?null:sys.key)} style={{ display:'flex', alignItems:'center', gap:14, padding:'14px 18px', cursor:'pointer' }}>
                  <div className="tool-ico"><Icon name="server" size={20} /></div>
                  <div style={{ flex:1, minWidth:0 }}>
                    <div style={{ fontSize:14, color:'var(--txt-hi)', fontWeight:600 }}>{sys.os}</div>
                    <div className="mono" style={{ fontSize:10.5, color:'var(--txt-mute)', whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{sys.target} · {sys.datastream}</div>
                  </div>
                  <span className="badge" style={{ flexShrink:0, padding:'3px 9px', color:'var(--txt-dim)' }}>{sys.scans.length} escaneo{sys.scans.length!==1?'s':''}</span>
                  <Gauge value={sc} size={48} thick={5} color={scoreColor(sc)} sub="" />
                  <Icon name="chevrond" size={16} style={{ color:'var(--txt-mute)', flexShrink:0, transform:isOpen?'rotate(180deg)':'none', transition:'transform .2s' }} />
                </div>
                {isOpen && (
                  <div className="enter">
                    {sys.scans.map(s => {
                      const su = s.summary || {};
                      const ssc = su.score || 0;
                      return (
                        <div key={s.scan_id} style={{ display:'flex', alignItems:'center', gap:12, padding:'12px 18px', borderTop:'1px solid var(--line)', flexWrap:'wrap' }}>
                          <div style={{ flex:1, minWidth:180 }}>
                            <div style={{ fontSize:12.5, color:'var(--txt-hi)' }}>
                              {fmtScanDate(s.ts)}
                              {s.tailored && <span className="badge sev-info" style={{ marginLeft:8, padding:'1px 7px' }}>tailored</span>}
                              {su.remediated && <span className="badge sev-ok" style={{ marginLeft:6, padding:'1px 7px' }}>remediado</span>}
                            </div>
                            <div className="mono" style={{ fontSize:10.5, color:'var(--txt-mute)', marginTop:2, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{s.profile_title || s.profile}</div>
                          </div>
                          <div style={{ display:'flex', gap:6 }}>
                            <span className="badge sev-ok" style={{ padding:'2px 7px' }}>✓ {su.pass||0}</span>
                            <span className="badge sev-crit" style={{ padding:'2px 7px' }}>✕ {su.fail||0}</span>
                            <span className="badge sev-info" style={{ padding:'2px 7px' }}>N/A {su.notapplicable||0}</span>
                          </div>
                          <div className="disp" style={{ fontSize:15, fontWeight:700, width:48, textAlign:'right', color:scoreColor(ssc) }}>{ssc}%</div>
                          <Btn variant="ghost" size="sm" icon="report" onClick={()=>s.report_available && window.open(s.report_url,'_blank')} style={{ opacity:s.report_available?1:0.4 }} title={s.report_available?'Abrir informe HTML':'Informe no disponible (re-lanza el escaneo)'}>Reporte</Btn>
                        </div>
                      );
                    })}
                  </div>
                )}
              </div>
            );
          })}
        </div>
      )}
    </>
  );
}

/* ============================================================
   CIS Benchmark — escaneo de hardening dinámico
   ============================================================ */
const SCAP_PROFILES = [
  'Perfil común para sistemas de propósito general (74 reglas)',
  'CIS Benchmark — Nivel 1 · Servidor (186 reglas)',
  'CIS Benchmark — Nivel 2 · Servidor (262 reglas)',
  'CIS Benchmark — Nivel 1 · Estación de trabajo (142 reglas)',
  'ENS · Categoría ALTA (211 reglas)',
  'PCI-DSS v4.0 (96 reglas)',
  'STIG · DISA (318 reglas)',
];
const SCAP_CONTENT = [
  'ssg-ubuntu2204-ds.xml — Guía de configuración segura de Ubuntu 22.04',
  'ssg-rhel9-ds.xml — Guía de configuración segura de RHEL 9',
  'ssg-windows2022-ds.xml — Guía de configuración segura de Windows Server 2022',
  'ssg-debian12-ds.xml — Guía de configuración segura de Debian 12',
];
const SCAP_RULES = [
  { id:'r1', cat:'Software', sev:'high', t:'gpgcheck habilitado en la configuración principal de paquetes', d:'Garantiza que el gestor de paquetes verifica las firmas GPG de todos los paquetes instalados.', rem:'echo "gpgcheck=1" >> /etc/dnf/dnf.conf' },
  { id:'r2', cat:'Software', sev:'high', t:'gpgcheck habilitado en todos los repositorios', d:'Cada repositorio debe forzar la verificación de firmas.', rem:'sed -i "s/gpgcheck=0/gpgcheck=1/g" /etc/yum.repos.d/*.repo' },
  { id:'r3', cat:'Software', sev:'low', t:'Deshabilitar prelinking', d:'El prelinking interfiere con AIDE y la verificación de integridad.', rem:'apt purge prelink -y' },
  { id:'r4', cat:'Integridad', sev:'med', t:'Construir y probar la base de datos AIDE', d:'AIDE debe estar instalado e inicializado para detectar cambios no autorizados.', rem:'aideinit && cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db' },
  { id:'r5', cat:'Integridad', sev:'med', t:'Verificar y corregir permisos de ficheros con el gestor de paquetes', d:'Los permisos deben coincidir con los definidos por los paquetes.', rem:'rpm -Va --noconfig | awk \'$1 ~ /..5/\'' },
  { id:'r6', cat:'Kernel', sev:'high', t:'Deshabilitar soporte de kernel para USB vía bootloader', d:'Impide el montaje de dispositivos de almacenamiento USB no autorizados.', rem:'GRUB_CMDLINE_LINUX="nousb"' },
  { id:'r7', cat:'Permisos', sev:'med', t:'Permisos restrictivos en librerías compartidas', d:'/lib, /lib64 y /usr/lib deben tener permisos 0755 o más restrictivos.', rem:'find /lib /usr/lib -perm /022 -exec chmod go-w {} +' },
  { id:'r8', cat:'Permisos', sev:'med', t:'Propiedad root en librerías compartidas', d:'Las librerías del sistema deben pertenecer a root.', rem:'chown root:root /lib /usr/lib -R' },
  { id:'r9', cat:'Permisos', sev:'med', t:'Permisos restrictivos en ejecutables del sistema', d:'Los binarios en /bin, /sbin, /usr/bin deben ser no-escribibles por otros.', rem:'find /bin /sbin /usr/bin -perm /022 -exec chmod go-w {} +' },
  { id:'r10', cat:'Cuentas', sev:'crit', t:'Login directo de root no permitido (SSH)', d:'PermitRootLogin debe estar a "no" para forzar el uso de cuentas nominales.', rem:'sed -i "s/^#*PermitRootLogin.*/PermitRootLogin no/" /etc/ssh/sshd_config' },
  { id:'r11', cat:'Cuentas', sev:'high', t:'Root login restringido en consolas virtuales', d:'/etc/securetty no debe permitir login de root en TTYs.', rem:': > /etc/securetty' },
  { id:'r12', cat:'Cuentas', sev:'crit', t:'Sólo root tiene UID 0', d:'Ninguna otra cuenta debe tener UID 0.', rem:'awk -F: \'($3==0){print}\' /etc/passwd' },
  { id:'r13', cat:'Cuentas', sev:'crit', t:'Imposible iniciar sesión con contraseña vacía', d:'Ninguna cuenta debe tener una contraseña vacía.', rem:'passwd -l <usuario>' },
  { id:'r14', cat:'Cuentas', sev:'high', t:'Hashes de contraseña en /etc/shadow', d:'Los hashes no deben almacenarse en /etc/passwd.', rem:'pwconv' },
  { id:'r15', cat:'Contraseñas', sev:'med', t:'Longitud mínima de contraseña ≥ 14', d:'Política de complejidad mediante pam_pwquality.', rem:'minlen = 14 en /etc/security/pwquality.conf' },
  { id:'r16', cat:'Contraseñas', sev:'low', t:'Edad mínima de contraseña ≥ 1 día', d:'Evita cambios de contraseña en cascada.', rem:'PASS_MIN_DAYS 1 en /etc/login.defs' },
  { id:'r17', cat:'Contraseñas', sev:'low', t:'Edad máxima de contraseña ≤ 365 días', d:'Fuerza la rotación periódica de credenciales.', rem:'PASS_MAX_DAYS 365 en /etc/login.defs' },
  { id:'r18', cat:'Auditoría', sev:'med', t:'Notificación de último acceso al iniciar sesión', d:'Muestra el último acceso para detectar usos no autorizados.', rem:'session required pam_lastlog.so showfailed' },
  { id:'r19', cat:'Red', sev:'crit', t:'Verificar que el firewall está habilitado', d:'El cortafuegos del sistema debe estar activo con política deny por defecto.', rem:'ufw enable && ufw default deny incoming' },
  { id:'r20', cat:'Red', sev:'high', t:'Deshabilitar reenvío de paquetes IP', d:'net.ipv4.ip_forward debe estar a 0 salvo en routers.', rem:'sysctl -w net.ipv4.ip_forward=0' },
];

function ScapRule({ r, checked, onToggle }) {
  const [open, setOpen] = useStateL(false);
  return (
    <div style={{ borderBottom:'1px solid var(--line)' }}>
      <div style={{ display:'flex', alignItems:'center', gap:11, padding:'9px 14px', cursor:'pointer' }} onClick={()=>setOpen(o=>!o)}>
        <Icon name="chevron" size={13} style={{ color:'var(--txt-mute)', transform: open?'rotate(90deg)':'none', transition:'transform .18s', flexShrink:0 }} />
        <input type="checkbox" checked={checked} onChange={onToggle} onClick={e=>e.stopPropagation()} className="scap-chk" />
        <span style={{ flex:1, fontSize:13, color: checked?'var(--blue)':'var(--txt)', minWidth:0 }}>{r.t}</span>
        <Sev level={r.sev} />
      </div>
      {open && (
        <div className="enter" style={{ padding:'0 14px 14px 48px' }}>
          <div style={{ fontSize:12.5, color:'var(--txt-dim)', lineHeight:1.6, marginBottom:10 }}>{r.d}</div>
          <div className="mono upper" style={{ fontSize:9.5, color:'var(--txt-mute)', marginBottom:6 }}>Categoría · {r.cat} · Remediación</div>
          <div className="terminal" style={{ fontSize:11.5, padding:'10px 12px' }}><span className="t-dim">$ </span><span className="t-acc">{r.rem}</span></div>
        </div>
      )}
    </div>
  );
}

function ScapWorkbench({ bench, onBack }) {
  // Live metadata from the backend (real DataStreams / profiles / rules).
  const [datastreams, setDatastreams] = useStateL([]);
  const [content, setContent] = useStateL('');          // datastream id
  const [profiles, setProfiles] = useStateL([]);
  const [profile, setProfile] = useStateL('');          // profile id
  const [ruleList, setRuleList] = useStateL([]);        // rules of the profile
  const [loadingMeta, setLoadingMeta] = useStateL(false);
  const [target, setTarget] = useStateL('local');
  const [host, setHost] = useStateL('');
  const [port, setPort] = useStateL('22');
  const [authMethod, setAuthMethod] = useStateL('password'); // password | key
  const [password, setPassword] = useStateL('');
  const [sshKey, setSshKey] = useStateL('');
  const [sel, setSel] = useStateL([]);
  const [opts, setOpts] = useStateL({ fetch:false, remediate:false });
  const [phase, setPhase] = useStateL('idle'); // idle | running | done | error
  const [prog, setProg] = useStateL(0);
  const [results, setResults] = useStateL(null);
  const [reporting, setReporting] = useStateL(false);
  const [err, setErr] = useStateL('');
  const [q, setQ] = useStateL('');
  const [templates, setTemplates] = useStateL([]);     // saved tailoring templates
  const [tplId, setTplId] = useStateL('');             // active template id ('' = none)
  const [metaBusy, setMetaBusy] = useStateL('');        // '' | 'upload' | 'save'
  const fileRef = useRefL(null);
  const tplFileRef = useRefL(null);                     // hidden input for ingesting template files
  const pendingSelRef = useRefL(null);                  // template rules awaiting a profile reload
  const pendingProfileRef = useRefL(null);              // profile to force-pick after a DataStream switch
  const pendingTplIdRef = useRefL(null);                // server template id to mark active after rules reload

  // 1) Load available DataStreams once. Retries a few times so a cold-start
  //    race (first /api hit while the backend is still warming up) doesn't
  //    leave the picker stuck on "Cargando contenido…" with no recovery.
  useEffectL(() => {
    let alive = true;
    const load = (tries) => {
      window.fsApi('/api/blueteam/datastreams', { method:'GET' })
        .then(d => { if(!alive) return; const list = d.datastreams || []; setDatastreams(list);
          const def = list.find(x=>x.os==='debian12') || list[0];
          if (def) setContent(def.id); })
        .catch(()=>{ if(alive && tries>0) setTimeout(()=>load(tries-1), 1500); });
    };
    load(4);
    return () => { alive = false; };
  }, []);

  // 2) When the DataStream changes, load its profiles.
  useEffectL(() => {
    if (!content) return;
    let alive = true; setLoadingMeta(true); setProfiles([]); setProfile(''); setRuleList([]); setSel([]);
    window.fsApi('/api/blueteam/datastreams/'+encodeURIComponent(content)+'/profiles', { method:'GET' })
      .then(d => { if(!alive) return; const ps = d.profiles || []; setProfiles(ps);
        const want = pendingProfileRef.current; pendingProfileRef.current = null;
        const def = (want && ps.find(p=>p.id===want)) ? ps.find(p=>p.id===want)
          : (ps.find(p=>/_standard$/i.test(p.id)) || ps.find(p=>/high/i.test(p.id)) || ps[0]);
        if (def) setProfile(def.id); })
      .catch(()=>{}).finally(()=>{ if(alive) setLoadingMeta(false); });
    return () => { alive = false; };
  }, [content]);

  // 3) When the profile changes, load the rules it selects.
  useEffectL(() => {
    if (!content || !profile) return;
    let alive = true;
    window.fsApi('/api/blueteam/datastreams/'+encodeURIComponent(content)+'/rules?profile='+encodeURIComponent(profile), { method:'GET' })
      .then(d => { if(!alive) return;
        const rs = (d.rules||[]).map(r => ({ id:r.id, t:r.title, cat:r.category||'SCAP', sev:window.normSev(r.severity), d:r.description||'', rem:r.remediation||'' }));
        setRuleList(rs);
        if (pendingSelRef.current) {                 // applying a saved/ingested template
          setSel(pendingSelRef.current); pendingSelRef.current = null;
          if (pendingTplIdRef.current) { setTplId(pendingTplIdRef.current); pendingTplIdRef.current = null; }
        } else {                                     // plain profile (re)load
          setTplId(''); setSel(rs.map(r=>r.id));
        }
      })
      .catch(()=>{});
    return () => { alive = false; };
  }, [content, profile]);

  // 3b) Saved tailoring templates available for this DataStream.
  useEffectL(() => {
    if (!content) { setTemplates([]); return; }
    let alive = true;
    window.fsApi('/api/blueteam/templates?datastream='+encodeURIComponent(content), { method:'GET' })
      .then(d => { if(alive) setTemplates(d.templates||[]); })
      .catch(()=>{ if(alive) setTemplates([]); });
    return () => { alive = false; };
  }, [content]);

  const toggle = (id) => { setTplId(''); setSel(s => s.includes(id) ? s.filter(x=>x!==id) : [...s, id]); };
  const allSel = ruleList.length>0 && sel.length === ruleList.length;
  const rules = q ? ruleList.filter(r => (r.t+r.cat).toLowerCase().includes(q.toLowerCase())) : ruleList;

  // Apply a saved template: select its profile + exact rule subset.
  const applyTemplateObj = (t) => {
    if (!t) { setTplId(''); return; }
    setTplId(t.id);
    const want = t.selected_rules || [];
    if (t.base_profile && t.base_profile !== profile) {
      pendingSelRef.current = want;      // effect 3 will apply it after the rules reload
      setProfile(t.base_profile);
    } else {
      setSel(want);
    }
  };
  const applyTemplate = (id) => applyTemplateObj(templates.find(x=>x.id===id));

  const onCustomChange = (v) => {
    if (v === '__new__') { saveTemplate(); return; }   // keep current selection in the dropdown
    if (!v) { setTplId(''); setSel(ruleList.map(r=>r.id)); return; }
    applyTemplate(v);
  };

  // Upload a custom SCAP DataStream (new OS) and select it.
  const uploadXml = async (file) => {
    if (!file) return;
    setMetaBusy('upload'); setErr('');
    try {
      const fd = new FormData(); fd.append('file', file);
      const entry = await window.fsApiForm('/api/blueteam/datastreams/upload', fd);
      const list = await window.fsApi('/api/blueteam/datastreams', { method:'GET' });
      setDatastreams(list.datastreams || []);
      if (entry && entry.id) setContent(entry.id);
    } catch(e) {
      setErr(e && e.message ? e.message : 'No se pudo subir el contenido SCAP.'); setPhase('error');
    } finally { setMetaBusy(''); if (fileRef.current) fileRef.current.value=''; }
  };

  // Export the current rule selection to a portable template FILE. Uses the
  // browser's native "Save As" window (File System Access API) to choose the
  // filename — no in-app modal/prompt. Falls back to a download on browsers
  // without the API. The saved file can later be re-imported with "Ingestar".
  const saveTemplate = async () => {
    if (!content || !profile) return;
    if (!sel.length) { setErr('Selecciona al menos una regla para guardar la plantilla.'); setPhase('error'); return; }
    const ds = datastreams.find(d=>d.id===content);
    const suggested = ('flowshield-' + (ds ? (ds.os||ds.id) : content) + '-' + sel.length + 'reglas.json')
      .toLowerCase().replace(/[^a-z0-9.-]+/g,'-');
    const makeDoc = (name) => JSON.stringify({
      kind:'flowshield-scap-template', version:1, name,
      datastream:content, base_profile:profile,
      selected_rules:[...sel], count:sel.length,
      saved_at:new Date().toISOString(),
    }, null, 2);
    setMetaBusy('save'); setErr('');
    try {
      if (window.showSaveFilePicker) {
        const handle = await window.showSaveFilePicker({
          suggestedName: suggested,
          types: [{ description:'Plantilla FlowShield', accept:{ 'application/json':['.json'] } }],
        });
        const name = handle.name.replace(/\.json$/i,'');
        const w = await handle.createWritable();
        await w.write(makeDoc(name)); await w.close();
      } else {
        const name = suggested.replace(/\.json$/i,'');
        const blob = new Blob([makeDoc(name)], { type:'application/json' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a'); a.href = url; a.download = suggested;
        document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
      }
    } catch(e) {
      if (!(e && e.name === 'AbortError')) {   // ignore: user closed the save dialog
        setErr('No se pudo guardar la plantilla: ' + (e && e.message ? e.message : e)); setPhase('error');
      }
    } finally { setMetaBusy(''); }
  };

  // Ingest a previously-saved template file: register it server-side (so it
  // gets a real tailoring profile) and select it, switching DataStream/profile
  // if the template targets a different one than the current view.
  const ingestTemplate = async (file) => {
    if (!file) return;
    setMetaBusy('ingest'); setErr('');
    try {
      const doc = JSON.parse(await file.text());
      const dsId = doc.datastream, bp = doc.base_profile;
      const rules = Array.isArray(doc.selected_rules) ? doc.selected_rules : [];
      if (!dsId || !bp || !rules.length) throw new Error('el fichero no es una plantilla válida de FlowShield.');
      if (!datastreams.find(d=>d.id===dsId)) throw new Error('el DataStream de la plantilla («'+dsId+'») no está disponible aquí. Súbelo antes con «Subir XML».');
      const name = (doc.name || file.name.replace(/\.json$/i,'') || 'Plantilla importada').toString().slice(0,80);
      const tpl = await window.fsApi('/api/blueteam/templates', { method:'POST',
        body:{ name, datastream:dsId, base_profile:bp, selected_rules:rules } });
      if (dsId === content) {
        const d = await window.fsApi('/api/blueteam/templates?datastream='+encodeURIComponent(dsId), { method:'GET' });
        setTemplates(d.templates || []);
        applyTemplateObj({ id:tpl.id, base_profile:bp, selected_rules:rules });
      } else {
        pendingProfileRef.current = bp;       // effect 2 will pick this profile after the DS switch
        pendingSelRef.current = rules;        // effect 3 will apply this rule subset
        pendingTplIdRef.current = tpl.id;     // …and mark the template active
        setContent(dsId);
      }
    } catch(e) {
      setErr('No se pudo ingestar la plantilla: ' + (e && e.message ? e.message : e)); setPhase('error');
    } finally { setMetaBusy(''); if (tplFileRef.current) tplFileRef.current.value=''; }
  };

  const scan = async () => {
    if (!content || !profile || phase==='running') return;
    if (target==='remote' && !host.trim()) { setErr('Indica el host remoto (usuario@host).'); setPhase('error'); return; }
    setPhase('running'); setProg(6); setResults(null); setErr('');
    const timer = setInterval(()=> setProg(p => Math.min(p + Math.random()*5, 92)), 450);
    try {
      // Tailoring: a saved template wins; otherwise send an ad-hoc subset only
      // when the user actually narrowed the profile's rule set.
      const subset = sel.length > 0 && sel.length < ruleList.length;
      const body = {
        datastream: content, profile, target,
        host: target==='remote' ? host.trim() : null,
        port: parseInt(port,10) || 22,
        username: null,
        password: (target==='remote' && authMethod==='password') ? password : null,
        ssh_key: (target==='remote' && authMethod==='key') ? sshKey : null,
        fetch_remote_resources: !!opts.fetch,
        remediate: !!opts.remediate,
        template_id: tplId || null,
        selected_rules: (!tplId && subset) ? sel : null,
      };
      const r = await window.fsApi('/api/blueteam/scan', { method:'POST', body });
      const s = r.summary || {};
      const total = s.selected_total || ((s.pass||0)+(s.fail||0)+(s.notapplicable||0)+(s.error||0));
      setResults({ pass:s.pass||0, fail:s.fail||0, notapp:s.notapplicable||0, error:s.error||0,
        score:s.score||0, total, scan_id:r.scan_id, report_url:r.report_url, arf_url:r.arf_url,
        findings:r.findings||[], profile_title:r.profile_title, target:r.target });
      setProg(100); setPhase('done');
    } catch(e) {
      setErr(e && e.message ? e.message : 'Error ejecutando el escaneo.'); setPhase('error'); setProg(0);
    } finally { clearInterval(timer); }
  };

  const downloadReport = async () => {
    if (!results || reporting) return;
    setReporting(true);
    try {
      await window.fsDownloadPdf('/api/report/blueteam', results);
    } catch (e) {
      setErr('No se pudo generar el informe: ' + (e && e.message ? e.message : e));
    } finally {
      setReporting(false);
    }
  };

  return (
    <div className="enter theme-blue">
      <PageHeader title="Nuevo scan · CIS Benchmark" crumb="BLUE TEAM / HARDENING / NUEVO SCAN" />
      <button onClick={onBack} style={{ display:'inline-flex', alignItems:'center', gap:7, background:'none', border:'none', color:'var(--txt-dim)', cursor:'pointer', fontSize:12.5, marginBottom:18, fontFamily:'var(--font-mono)' }}>← volver al panel</button>

      <div className="panel" style={{ overflow:'hidden' }}>
        {/* config grid */}
        <div style={{ padding:'20px 22px', display:'grid', gridTemplateColumns:'130px 1fr', gap:'15px 16px', alignItems:'center', borderBottom:'1px solid var(--line)' }}>
          <Lbl>Contenido</Lbl>
          <div style={{ display:'flex', alignItems:'center', gap:12 }}>
            <select className="scap-sel" value={content} onChange={e=>setContent(e.target.value)} style={{ flex:1 }}>
              {!datastreams.length && <option value="">Cargando contenido…</option>}
              {datastreams.map(d=><option key={d.id} value={d.id}>{d.label} · {d.id}</option>)}
            </select>
            <input ref={fileRef} type="file" accept=".xml" style={{ display:'none' }} onChange={e=>uploadXml(e.target.files && e.target.files[0])} />
            <Btn variant="ghost" size="sm" icon="plus" onClick={()=>fileRef.current && fileRef.current.click()} style={{ flexShrink:0 }}>{metaBusy==='upload'?'Subiendo…':'Subir XML'}</Btn>
            <span className="disp" style={{ fontSize:15, fontWeight:700, color:'var(--txt-hi)', whiteSpace:'nowrap' }}>{(datastreams.find(d=>d.id===content)||{}).label || (bench && bench.name) || 'Selecciona un DataStream'}</span>
          </div>

          <Lbl>Personalización</Lbl>
          <div style={{ display:'flex', alignItems:'center', gap:10 }}>
            <select className="scap-sel" value={tplId} onChange={e=>onCustomChange(e.target.value)} disabled={metaBusy==='save'||metaBusy==='ingest'} style={{ flex:1 }}>
              <option value="">Ninguna seleccionada</option>
              {templates.map(t=><option key={t.id} value={t.id}>{t.name} · {t.selected} reglas</option>)}
              <option value="__new__">{metaBusy==='save'?'Guardando plantilla…':'Guardar selección como plantilla (archivo)…'}</option>
            </select>
            <input ref={tplFileRef} type="file" accept=".json,application/json" style={{ display:'none' }} onChange={e=>ingestTemplate(e.target.files && e.target.files[0])} />
            <Btn variant="ghost" size="sm" icon="plus" onClick={()=>tplFileRef.current && tplFileRef.current.click()} style={{ flexShrink:0 }}>{metaBusy==='ingest'?'Ingestando…':'Ingestar plantilla'}</Btn>
          </div>

          <Lbl>Perfil</Lbl>
          <div style={{ display:'flex', gap:10 }}>
            <select className="scap-sel" value={profile} onChange={e=>setProfile(e.target.value)} style={{ flex:1 }}>
              {!profiles.length && <option value="">{loadingMeta?'Cargando perfiles…':'—'}</option>}
              {profiles.map(p=><option key={p.id} value={p.id}>{p.title}</option>)}
            </select>
          </div>

          <Lbl>Objetivo</Lbl>
          <div style={{ display:'flex', gap:22, flexWrap:'wrap' }}>
            <Radio on={target==='local'} onClick={()=>setTarget('local')} label="Máquina local" />
            <Radio on={target==='remote'} onClick={()=>setTarget('remote')} label="Máquina remota (vía SSH)" />
          </div>

          <Lbl dim={target!=='remote'}>Usuario y host</Lbl>
          <div style={{ display:'flex', gap:10, opacity: target==='remote'?1:0.4, pointerEvents: target==='remote'?'auto':'none' }}>
            <div className="field" style={{ flex:1 }}>
              <Icon name="server" size={15} style={{ position:'absolute', left:12, top:12, color:'var(--blue)' }} />
              <input className="input input-prefix" value={host} onChange={e=>setHost(e.target.value)} placeholder="usuario@host  ·  p.ej. root@10.0.4.21" />
            </div>
            <div style={{ width:90 }}>
              <input className="input" value={port} onChange={e=>setPort(e.target.value)} placeholder="Puerto" style={{ textAlign:'center' }} />
            </div>
          </div>

          {target==='remote' && <>
            <Lbl>Autenticación</Lbl>
            <div style={{ display:'flex', gap:22, flexWrap:'wrap', alignItems:'center' }}>
              <Radio on={authMethod==='password'} onClick={()=>setAuthMethod('password')} label="Contraseña" />
              <Radio on={authMethod==='key'} onClick={()=>setAuthMethod('key')} label="Clave SSH" />
            </div>

            <Lbl>{authMethod==='password'?'Contraseña':'Clave privada'}</Lbl>
            {authMethod==='password'
              ? <input type="password" className="input" value={password} onChange={e=>setPassword(e.target.value)} placeholder="Contraseña SSH del objetivo" autoComplete="new-password" />
              : <textarea className="input" value={sshKey} onChange={e=>setSshKey(e.target.value)} placeholder="-----BEGIN OPENSSH PRIVATE KEY-----  ·  pega aquí la clave privada" style={{ minHeight:90, fontFamily:'var(--font-mono)', fontSize:11.5, resize:'vertical', width:'100%' }} />}
          </>}
        </div>

        {/* rules header */}
        <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', gap:12, padding:'13px 22px', background:'var(--bg-panel-2)', borderBottom:'1px solid var(--line)', flexWrap:'wrap' }}>
          <div style={{ display:'flex', alignItems:'center', gap:14 }}>
            <span className="eyebrow" style={{ margin:0 }}>Reglas</span>
            <span className="mono" style={{ fontSize:11, color:'var(--txt-dim)' }}>{sel.length} de {ruleList.length} en el perfil</span>
          </div>
          <div style={{ display:'flex', gap:8, alignItems:'center' }}>
            <div className="field">
              <Icon name="search" size={14} style={{ position:'absolute', left:11, top:9, color:'var(--txt-mute)' }} />
              <input className="input input-prefix" value={q} onChange={e=>setQ(e.target.value)} placeholder="Filtrar reglas…" style={{ padding:'7px 12px 7px 34px', fontSize:12, width:200 }} />
            </div>
            <Btn variant="ghost" size="sm" onClick={()=>setSel(allSel?[]:ruleList.map(r=>r.id))}>{allSel?'Deseleccionar todo':'Seleccionar todo'}</Btn>
          </div>
        </div>

        {/* rules list */}
        <div className="hidden-scroll" style={{ maxHeight:340, overflowY:'auto' }}>
          {rules.map(r => <ScapRule key={r.id} r={r} checked={sel.includes(r.id)} onToggle={()=>toggle(r.id)} />)}
          {!rules.length && <div style={{ padding:30, textAlign:'center', color:'var(--txt-mute)', fontSize:13 }}>{loadingMeta?'Cargando reglas del perfil…':(ruleList.length?'Sin reglas que coincidan.':'Selecciona contenido y perfil para cargar las reglas.')}</div>}
        </div>

        {/* footer / progress + scan */}
        <div style={{ borderTop:'1px solid var(--line)', background:'var(--bg-panel-2)' }}>
          <div style={{ padding:'8px 22px' }}>
            <div className="bar" style={{ height:8 }}><span style={{ width:prog+'%' }} /></div>
            <div className="mono" style={{ fontSize:10.5, color:'var(--txt-mute)', marginTop:6 }}>
              {phase==='idle' && `${prog}% (${ruleList.length} reglas en el perfil)`}
              {phase==='running' && `${prog}% · evaluando ${target==='remote'?(host||'host remoto'):'contenedor local'} …`}
              {phase==='error' && <span style={{ color:'var(--red)' }}>✕ {err}</span>}
              {phase==='done' && results && <span style={{ color:'var(--txt)' }}>100% · <span style={{ color:'var(--ok)' }}>{results.pass} pass</span> · <span style={{ color:'var(--red)' }}>{results.fail} fail</span> · <span style={{ color:'var(--txt-mute)' }}>{results.notapp} N/A</span> · cumplimiento {results.score}%</span>}
            </div>
          </div>
          <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', gap:16, padding:'12px 22px', flexWrap:'wrap' }}>
            <div style={{ display:'flex', gap:20, flexWrap:'wrap' }}>
              <Check on={opts.fetch} onClick={()=>setOpts(o=>({...o,fetch:!o.fetch}))} label="Obtener recursos remotos (OVAL/CVE)" />
              <Check on={opts.remediate} onClick={()=>setOpts(o=>({...o,remediate:!o.remediate}))} label="Remediar automáticamente" danger />
            </div>
            <Btn variant="solid" size="lg" className="theme-blue" icon={phase==='running'?null:'bolt'} onClick={scan} glitch style={{ opacity: (profile && phase!=='running')?1:0.5 }}>
              {phase==='running' ? 'ESCANEANDO…' : phase==='done' ? 'RE-ESCANEAR' : 'SCAN'}
            </Btn>
          </div>
        </div>
      </div>

      {phase==='done' && results && (
        <div className="panel enter" style={{ padding:20, marginTop:18 }}>
          <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:16, flexWrap:'wrap', gap:12 }}>
            <div className="eyebrow" style={{ margin:0 }}>Resultado del escaneo · {results.target || (target==='remote'?host:'local')}</div>
            <div style={{ display:'flex', gap:10 }}>
              <Btn variant="solid" size="sm" icon="report" className="theme-blue" onClick={downloadReport}>{reporting ? 'Generando…' : 'Informe PDF'}</Btn>
              <Btn variant="ghost" size="sm" icon="report" onClick={()=>window.open(results.report_url,'_blank')}>Ver informe HTML</Btn>
              <Btn variant="ghost" size="sm" icon="download" onClick={()=>window.open(results.arf_url,'_blank')}>Descargar ARF</Btn>
            </div>
          </div>
          <div style={{ display:'flex', gap:24, alignItems:'center', flexWrap:'wrap' }}>
            <Gauge value={results.score} size={120} color={results.score>=80?'var(--ok)':results.score>=70?'var(--amber)':'var(--red)'} sub="CUMPLIMIENTO" />
            <div style={{ flex:1, minWidth:220, display:'grid', gap:10 }}>
              <ResBar label="Aprobadas" v={results.pass} total={results.total||1} color="var(--ok)" />
              <ResBar label="Fallidas" v={results.fail} total={results.total||1} color="var(--red)" />
              <ResBar label="No aplicables" v={results.notapp} total={results.total||1} color="var(--txt-mute)" />
            </div>
            {opts.remediate && (
              <div style={{ alignSelf:'stretch', display:'flex', flexDirection:'column', justifyContent:'center', gap:8, padding:'14px 18px', background:'var(--blue-soft)', border:'1px solid color-mix(in srgb,var(--blue) 30%,transparent)', borderRadius:10, maxWidth:220 }}>
                <Icon name="bolt" size={20} style={{ color:'var(--blue)' }} />
                <div style={{ fontSize:12.5, color:'var(--txt-hi)', fontWeight:600 }}>Remediación aplicada</div>
                <div style={{ fontSize:11.5, color:'var(--txt-dim)' }}>{results.fail} reglas corregidas automáticamente en el objetivo.</div>
              </div>
            )}
          </div>
        </div>
      )}

      <style>{`
        .scap-sel{font-family:var(--font-mono);font-size:12.5px;color:var(--txt-hi);background:var(--bg-void);border:1px solid var(--line-bright);border-radius:6px;padding:9px 12px;outline:none;cursor:pointer;transition:border-color .2s}
        .scap-sel:focus,.scap-sel:hover{border-color:var(--blue)}
        .scap-chk{appearance:none;width:16px;height:16px;border:1.5px solid var(--line-bright);border-radius:4px;background:var(--bg-void);cursor:pointer;flex-shrink:0;position:relative;transition:all .15s}
        .scap-chk:checked{background:var(--blue);border-color:var(--blue);box-shadow:0 0 8px var(--blue-glow)}
        .scap-chk:checked::after{content:'';position:absolute;left:5px;top:1.5px;width:4px;height:8px;border:solid #04110D;border-width:0 2px 2px 0;transform:rotate(45deg)}
      `}</style>
    </div>
  );
}

function Lbl({ children, dim }) {
  return <div className="mono upper" style={{ fontSize:10.5, color: dim?'var(--txt-mute)':'var(--txt-dim)', letterSpacing:'.1em' }}>{children}</div>;
}
function Radio({ on, onClick, label }) {
  return (
    <button onClick={onClick} style={{ display:'inline-flex', alignItems:'center', gap:9, background:'none', border:'none', cursor:'pointer', padding:0 }}>
      <span style={{ width:17, height:17, borderRadius:99, border:'1.5px solid '+(on?'var(--blue)':'var(--line-bright)'), display:'grid', placeItems:'center', transition:'all .15s' }}>
        {on && <span style={{ width:8, height:8, borderRadius:99, background:'var(--blue)', boxShadow:'0 0 8px var(--blue-glow)' }} />}
      </span>
      <span style={{ fontSize:13, color: on?'var(--txt-hi)':'var(--txt-dim)' }}>{label}</span>
    </button>
  );
}
function Check({ on, onClick, label, danger }) {
  const c = danger ? 'var(--red)' : 'var(--blue)';
  return (
    <button onClick={onClick} style={{ display:'inline-flex', alignItems:'center', gap:9, background:'none', border:'none', cursor:'pointer', padding:0 }}>
      <span style={{ width:17, height:17, borderRadius:5, border:'1.5px solid '+(on?c:'var(--line-bright)'), background: on?c:'var(--bg-void)', display:'grid', placeItems:'center', transition:'all .15s', boxShadow: on?`0 0 8px ${danger?'var(--red-glow)':'var(--blue-glow)'}`:'none' }}>
        {on && <Icon name="check" size={11} style={{ color:'#04110D' }} />}
      </span>
      <span style={{ fontSize:12.5, color: on?'var(--txt-hi)':'var(--txt-dim)' }}>{label}</span>
    </button>
  );
}
function ResBar({ label, v, total, color }) {
  return (
    <div>
      <div style={{ display:'flex', justifyContent:'space-between', marginBottom:6 }}><span style={{ fontSize:12.5, color:'var(--txt)' }}>{label}</span><span className="mono" style={{ fontSize:12, color, fontWeight:600 }}>{v}</span></div>
      <div className="bar"><span style={{ width:(v/total*100)+'%', background:color, boxShadow:`0 0 8px ${color}` }} /></div>
    </div>
  );
}

Object.assign(window, { BlueTeam, ScapWorkbench });
