// Admin.jsx — backend dashboard. Same SPA, ?view=admin gated on user.is_admin.
// Design from the original webapp/Admin.jsx (the one we liked), wiring uses
// window.API (defined in api.jsx) so all data is live from canon_lm_server.
//
// One file holds: AdminShell + System / Database / Users / Threads / Datasets /
// Training views + the training log-tail subpanel.

function AdminShell({ user, initial, onExitToChat, onSignOut }) {
  const PAGES = [
    { id: 'engine',   label: 'Engine',    hint: 'M3 LLM start / stop' },
    { id: 'system',   label: 'System',    hint: 'CPU · RAM · GPU · disk' },
    { id: 'database', label: 'Database',  hint: 'users · threads · messages' },
    { id: 'users',    label: 'Users',     hint: 'manage accounts' },
    { id: 'invites',  label: 'Invites',   hint: 'issue / revoke codes' },
    { id: 'threads',  label: 'Threads',   hint: 'all conversations' },
    { id: 'datasets', label: 'Datasets',  hint: 'upload corpora' },
    { id: 'training', label: 'Training',  hint: 'launch + monitor jobs' },
  ];
  const [page, setPage] = React.useState(initial || 'system');
  const [confirmSignOut, setConfirmSignOut] = React.useState(false);

  React.useEffect(() => {
    const u = new URL(location.href);
    u.searchParams.set('view', 'admin');
    u.searchParams.set('sec', page);
    history.replaceState({}, '', u);
  }, [page]);

  return (
    <>
      <div className="ad-shell">
        <aside className="ad-rail">
          <div className="ad-rail-brand">
            <img src="../../assets/zigu-logo-gold.png" alt="ZIGU"/>
            <div>
              <div className="wm">ZIGU ADMIN</div>
              <div className="ver">canon_lm_server</div>
            </div>
          </div>

          <div className="ad-rail-section">Operations</div>
          <div className="ad-nav">
            {PAGES.map(p => (
              <button
                key={p.id}
                className={'ad-nav-item' + (page === p.id ? ' active' : '')}
                onClick={() => setPage(p.id)}
                title={p.hint}
              >
                <span className="lbl">{p.label}</span>
                <span className="hnt">{p.hint}</span>
              </button>
            ))}
          </div>

          <div className="ad-rail-foot">
            <button className="ad-link" onClick={onExitToChat} title="Return to chat">← Chat</button>
            <button className="ad-link" onClick={() => setConfirmSignOut(true)} title="Sign out">Sign out</button>
          </div>
        </aside>

        <main className="ad-main">
          <header className="ad-header">
            <div className="ad-crumbs">
              <span className="ad-crumb-root">Admin</span>
              <span className="ad-crumb-sep">/</span>
              <span className="ad-crumb-leaf">{PAGES.find(p => p.id === page).label}</span>
            </div>
            <div className="ad-userchip">
              <span className="ad-userchip-name">{user.display_name || user.email}</span>
              <span className="ad-tag ad-tag-admin">admin</span>
            </div>
          </header>

          <div className="ad-content">
            {page === 'engine'   && <AdminEngineView/>}
            {page === 'system'   && <AdminSystemView/>}
            {page === 'database' && <AdminDatabaseView/>}
            {page === 'users'    && <AdminUsersView currentUserId={user.id}/>}
            {page === 'invites'  && <AdminInvitesView/>}
            {page === 'threads'  && <AdminThreadsView/>}
            {page === 'datasets' && <AdminDatasetsView/>}
            {page === 'training' && <AdminTrainingView/>}
          </div>
        </main>
      </div>
      {confirmSignOut && <ConfirmModal eb="Admin" title="Sign out of admin?" desc="You'll be returned to the login screen." confirmText="Sign out" onClose={() => setConfirmSignOut(false)} onConfirm={onSignOut}/>}
    </>
  );
}


// ---------- shared helpers ----------

function fmtBytes(b) {
  if (b == null) return '—';
  if (b < 1024) return b + ' B';
  const units = ['KB', 'MB', 'GB', 'TB'];
  let v = b / 1024;
  let i = 0;
  while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
  return v.toFixed(v >= 100 ? 0 : 1) + ' ' + units[i];
}

function fmtDuration(sec) {
  if (sec == null || isNaN(sec)) return '—';
  const s = Math.floor(sec % 60);
  const m = Math.floor((sec / 60) % 60);
  const h = Math.floor((sec / 3600) % 24);
  const d = Math.floor(sec / 86400);
  if (d > 0) return d + 'd ' + h + 'h ' + m + 'm';
  if (h > 0) return h + 'h ' + m + 'm ' + s + 's';
  if (m > 0) return m + 'm ' + s + 's';
  return s + 's';
}

function fmtPercent(p) { return p == null ? '—' : p.toFixed(1) + '%'; }

function fmtDate(epoch) {
  if (!epoch) return '—';
  const d = new Date(epoch * 1000);
  const pad = n => String(n).padStart(2, '0');
  return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes());
}

function MetricCard({ label, value, sub }) {
  return (
    <div className="ad-card ad-metric">
      <div className="ad-metric-label">{label}</div>
      <div className="ad-metric-value">{value}</div>
      {sub && <div className="ad-metric-sub">{sub}</div>}
    </div>
  );
}

function ProgressBar({ percent }) {
  const p = Math.max(0, Math.min(100, percent || 0));
  const color = p > 90 ? 'var(--zg-danger)' : p > 75 ? 'var(--zg-warning)' : 'var(--accent)';
  return (
    <div className="ad-progress">
      <div className="ad-progress-fill" style={{ width: p + '%', background: color }}/>
    </div>
  );
}

// 401 → re-login. Re-used by every view.
function on401(e) {
  if (e && e.status === 401) { window.API.clear(); location.reload(); }
}


// ---------- Engine view (M3 LLM start/stop) ----------

function AdminEngineView() {
  const [status, setStatus] = React.useState(null);
  const [err, setErr] = React.useState(null);
  const [busy, setBusy] = React.useState(null); // 'start' | 'stop' | null
  const [tick, setTick] = React.useState(0);
  const [auto, setAuto] = React.useState(true);
  const [lastFetch, setLast] = React.useState(null);

  const fetchStatus = React.useCallback(() => {
    return window.API.json('/admin/engine/status')
      .then(s => { setStatus(s); setErr(null); setLast(Date.now()); })
      .catch(e => { on401(e); setErr(e.message); });
  }, []);

  React.useEffect(() => { fetchStatus(); }, [tick, fetchStatus]);

  React.useEffect(() => {
    if (!auto) return;
    const id = setInterval(() => setTick(t => t + 1), 5000);
    return () => clearInterval(id);
  }, [auto]);

  const start = async () => {
    if (busy) return;
    setBusy('start');
    try {
      const r = await window.API.json('/admin/engine/start', { method: 'POST' });
      setStatus(r.status); setLast(Date.now());
      if (!r.ok) alert('Start did not confirm · ' + r.detail);
    } catch (e) { on401(e); alert(e.message); }
    finally { setBusy(null); }
  };

  const stop = async () => {
    if (busy) return;
    if (!window.confirm('Stop the M3 LLM engine? Active chats will get errors until restart.')) return;
    setBusy('stop');
    try {
      const r = await window.API.json('/admin/engine/stop', { method: 'POST' });
      setStatus(r.status); setLast(Date.now());
      if (!r.ok) alert('Stop did not confirm · ' + r.detail);
    } catch (e) { on401(e); alert(e.message); }
    finally { setBusy(null); }
  };

  if (err && !status) return <div className="ad-error">{err}</div>;
  if (!status) return <div className="ad-loading">Loading engine status …</div>;

  const lastAgo = lastFetch ? Math.floor((Date.now() - lastFetch) / 1000) + 's ago' : '—';
  const badgeColor = status.running ? 'var(--zg-success)' : 'var(--zg-danger)';
  const badgeBg    = status.running ? 'rgba(111,158,92,0.12)' : 'rgba(201,92,74,0.12)';

  return (
    <div className="ad-engine">
      <div className="ad-row-actions">
        <span className="ad-mono ad-dim">last check · {lastAgo}</span>
        <label className="ad-toggle">
          <input type="checkbox" checked={auto} onChange={e => setAuto(e.target.checked)}/>
          <span>auto-refresh 5s</span>
        </label>
        <button className="ad-btn-mini" onClick={() => setTick(t => t + 1)} disabled={busy != null}>Refresh</button>
      </div>

      <div className="ad-card">
        <div className="ad-metric-label" style={{marginBottom: 12}}>M3 zigu_engine_rs</div>

        <div style={{display:'flex', alignItems:'center', gap:14, marginBottom:18}}>
          <span className="ad-status-badge" style={{ color: badgeColor, background: badgeBg, fontSize: 13, padding: '5px 12px' }}>
            {status.running && <span className="ad-status-dot" style={{ background: badgeColor }}/>}
            {status.running ? 'running' : 'stopped'}
          </span>
          {status.version && <span className="ad-mono ad-dim">v{status.version}</span>}
        </div>

        <div className="ad-info-grid">
          <div><span className="k">관리 엔진 URL</span><span className="v">{status.url}</span></div>
          <div><span className="k">SSH 대상</span><span className="v">{status.ssh_target}</span></div>
          {status.chat_upstream && (
            <div><span className="k">채팅 라우팅</span><span className="v">{status.chat_upstream} <span style={{color: status.chat_track === 'stable' ? 'var(--zg-success)' : 'var(--accent)', fontFamily:'var(--font-mono)', fontSize:10, marginLeft:6, padding:'1px 5px', border:'1px solid currentColor', borderRadius:3}}>{status.chat_track === 'stable' ? 'STABLE' : 'LATEST'}</span></span></div>
          )}
          {status.stable_url && (
            <div><span className="k">Stable sidecar</span><span className="v">{status.stable_url} <span style={{color: status.stable_running ? 'var(--zg-success)' : 'var(--zg-danger)', marginLeft:6, fontSize:11}}>{status.stable_running ? '● 가동' : '○ 정지'}</span></span></div>
          )}
        </div>

        {status.error && (
          <div style={{
            marginTop: 14, fontSize: 11.5, color: 'var(--zg-danger)',
            fontFamily: 'var(--font-mono)', wordBreak: 'break-all',
            border: '1px solid rgba(201,92,74,0.4)', background: 'rgba(201,92,74,0.06)',
            borderRadius: 'var(--r-2)', padding: '10px 12px',
          }}>{status.error.slice(0, 240)}</div>
        )}

        <div style={{display:'flex', gap:10, marginTop: 20}}>
          <button className="ad-btn-primary" onClick={start} disabled={busy != null || status.running}>
            {busy === 'start' ? 'Starting …' : 'Start engine'}
          </button>
          <button className="ad-btn-danger" onClick={stop} disabled={busy != null || !status.running}>
            {busy === 'stop' ? 'Stopping …' : 'Stop engine'}
          </button>
        </div>
      </div>

      <div className="ad-section">
        <h3>About</h3>
        <ul className="ad-mono ad-dim" style={{fontSize:11.5, lineHeight:1.7, paddingLeft:18, listStyle:'square'}}>
          <li>Rust binary on M3 reached over Tailscale.</li>
          <li>Stub mode unless rebuilt with --features model. Stub returns "day4-stub" tokens for any prompt.</li>
          <li>Start spawns nohup ... &amp; disown over SSH; stop sends SIGTERM to the engine PID.</li>
          <li>Chat in the right rail routes through this engine. Stopping it surfaces an error in the next message.</li>
        </ul>
      </div>
    </div>
  );
}


// ---------- System view ----------

function AdminSystemView() {
  const [data, setData] = React.useState(null);
  const [err, setErr] = React.useState(null);
  const [tick, setTick] = React.useState(0);
  const [autoRefresh, setAutoRefresh] = React.useState(true);

  React.useEffect(() => {
    let cancelled = false;
    window.API.adminSystem()
      .then(j => { if (!cancelled) { setData(j); setErr(null); } })
      .catch(e => { if (cancelled) return; on401(e); setErr(e.message); });
    return () => { cancelled = true; };
  }, [tick]);

  React.useEffect(() => {
    if (!autoRefresh) return;
    const id = setInterval(() => setTick(t => t + 1), 3000);
    return () => clearInterval(id);
  }, [autoRefresh]);

  if (err && !data) return <div className="ad-error">{err}</div>;
  if (!data) return <div className="ad-loading">Loading host metrics …</div>;

  const { system, cpu, memory, disks, gpu } = data;

  return (
    <div className="ad-system">
      <div className="ad-row-actions">
        <span className="ad-mono ad-dim">refresh every 3 s</span>
        <label className="ad-toggle">
          <input type="checkbox" checked={autoRefresh} onChange={e => setAutoRefresh(e.target.checked)}/>
          <span>auto</span>
        </label>
        <button className="ad-btn-mini" onClick={() => setTick(t => t + 1)}>Refresh</button>
      </div>

      <div className="ad-section">
        <h3>Host</h3>
        <div className="ad-info-grid">
          <div><span className="k">Hostname</span><span className="v">{system.hostname}</span></div>
          <div><span className="k">Platform</span><span className="v">{system.platform}</span></div>
          <div><span className="k">Python</span><span className="v">{system.python}</span></div>
          <div><span className="k">Server uptime</span><span className="v">{fmtDuration(system.server_uptime_sec)}</span></div>
        </div>
      </div>

      <div className="ad-section">
        <h3>CPU <span className="ad-h3-sub">{cpu.count_physical} physical · {cpu.count_logical} logical</span></h3>
        <div className="ad-bar-row">
          <div className="ad-bar-label">Total</div>
          <ProgressBar percent={cpu.percent}/>
          <div className="ad-bar-value">{fmtPercent(cpu.percent)}</div>
        </div>
        {cpu.per_cpu && cpu.per_cpu.length > 0 && (
          <div className="ad-percpu">
            {cpu.per_cpu.map((p, i) => (
              <div key={i} className="ad-percpu-cell">
                <div className="ad-percpu-lbl">cpu{i}</div>
                <ProgressBar percent={p}/>
                <div className="ad-percpu-val ad-mono">{fmtPercent(p)}</div>
              </div>
            ))}
          </div>
        )}
      </div>

      <div className="ad-section">
        <h3>Memory</h3>
        <div className="ad-bar-row">
          <div className="ad-bar-label">RAM</div>
          <ProgressBar percent={memory.percent}/>
          <div className="ad-bar-value">{fmtPercent(memory.percent)}</div>
        </div>
        <div className="ad-info-grid" style={{marginTop: 12}}>
          <div><span className="k">Total</span><span className="v">{fmtBytes(memory.total_bytes)}</span></div>
          <div><span className="k">Used</span><span className="v">{fmtBytes(memory.used_bytes)}</span></div>
          <div><span className="k">Available</span><span className="v">{fmtBytes(memory.available_bytes)}</span></div>
        </div>
      </div>

      <div className="ad-section">
        <h3>GPU</h3>
        {gpu.available ? (
          <>
            <div className="ad-info-grid">
              <div><span className="k">Device</span><span className="v">{gpu.name}</span></div>
              <div><span className="k">Count</span><span className="v">{gpu.device_count}</span></div>
            </div>
            <div className="ad-bar-row" style={{marginTop: 12}}>
              <div className="ad-bar-label">Allocated</div>
              <ProgressBar percent={gpu.memory_total_bytes ? (gpu.memory_allocated_bytes / gpu.memory_total_bytes) * 100 : 0}/>
              <div className="ad-bar-value">{fmtBytes(gpu.memory_allocated_bytes)} / {fmtBytes(gpu.memory_total_bytes)}</div>
            </div>
            <div className="ad-bar-row">
              <div className="ad-bar-label">Reserved</div>
              <ProgressBar percent={gpu.memory_total_bytes ? (gpu.memory_reserved_bytes / gpu.memory_total_bytes) * 100 : 0}/>
              <div className="ad-bar-value">{fmtBytes(gpu.memory_reserved_bytes)}</div>
            </div>
            {gpu.utilization_percent != null && (
              <div className="ad-bar-row">
                <div className="ad-bar-label">Utilization</div>
                <ProgressBar percent={gpu.utilization_percent}/>
                <div className="ad-bar-value">{fmtPercent(gpu.utilization_percent)}</div>
              </div>
            )}
          </>
        ) : (
          <div className="ad-dim">No CUDA device detected. Training will run on CPU.</div>
        )}
      </div>

      <div className="ad-section">
        <h3>Disks</h3>
        {disks.map(d => (
          <div key={d.path} className="ad-disk">
            <div className="ad-disk-head">
              <span className="ad-disk-label">{d.label}</span>
              <span className="ad-mono ad-dim ad-disk-path">{d.path}</span>
            </div>
            <div className="ad-bar-row">
              <div className="ad-bar-label">usage</div>
              <ProgressBar percent={d.percent}/>
              <div className="ad-bar-value">{fmtBytes(d.used_bytes)} / {fmtBytes(d.total_bytes)} · {fmtPercent(d.percent)}</div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}


// ---------- Database view ----------

function AdminDatabaseView() {
  const [data, setData] = React.useState(null);
  const [err, setErr] = React.useState(null);
  const [tick, setTick] = React.useState(0);

  React.useEffect(() => {
    let cancelled = false;
    window.API.adminDb()
      .then(j => { if (!cancelled) { setData(j); setErr(null); } })
      .catch(e => { if (cancelled) return; on401(e); setErr(e.message); });
    return () => { cancelled = true; };
  }, [tick]);

  if (err && !data) return <div className="ad-error">{err}</div>;
  if (!data) return <div className="ad-loading">Loading DB stats …</div>;

  return (
    <div className="ad-db">
      <div className="ad-row-actions">
        <span className="ad-mono ad-dim">sqlite</span>
        <button className="ad-btn-mini" onClick={() => setTick(t => t + 1)}>Refresh</button>
      </div>

      <div className="ad-metric-grid">
        <MetricCard label="Users"    value={data.users_count}    sub={data.admin_count + ' admin'}/>
        <MetricCard label="Threads"  value={data.threads_count}/>
        <MetricCard label="Messages" value={data.messages_count}/>
        <MetricCard label="DB size"  value={fmtBytes(data.db_size_bytes)} sub={data.db_size_bytes.toLocaleString() + ' bytes'}/>
      </div>

      <div className="ad-section">
        <h3>Storage location</h3>
        <div className="ad-mono ad-dim" style={{wordBreak:'break-all'}}>{data.db_path}</div>
      </div>
    </div>
  );
}


// ---------- Users view ----------

function AdminUsersView({ currentUserId }) {
  const [data, setData] = React.useState(null);
  const [err, setErr] = React.useState(null);
  const [q, setQ] = React.useState('');
  const [tick, setTick] = React.useState(0);
  const [busyId, setBusyId] = React.useState(null);
  const [confirmDel, setConfirmDel] = React.useState(null);
  const [addOpen, setAddOpen] = React.useState(false);
  const [resetOut, setResetOut] = React.useState(null); // {email, temporary_password}

  React.useEffect(() => {
    let cancelled = false;
    window.API.adminListUsers(q)
      .then(j => { if (!cancelled) { setData(j); setErr(null); } })
      .catch(e => { if (cancelled) return; on401(e); setErr(e.message); });
    return () => { cancelled = true; };
  }, [tick, q]);

  const refresh = () => setTick(t => t + 1);

  const toggleAdmin = async (u) => {
    if (u.id === currentUserId && u.is_admin) {
      if (!window.confirm('자신의 admin 권한을 해제하시겠어요? 이 세션에서 관리 기능을 잃습니다.')) return;
    }
    setBusyId(u.id);
    try { await window.API.adminToggleUserAdmin(u.id, !u.is_admin); refresh(); }
    catch (e) { on401(e); alert(e.message); }
    finally { setBusyId(null); }
  };

  const toggleActive = async (u) => {
    setBusyId(u.id);
    try { await window.API.adminUpdateUser(u.id, { is_active: !u.is_active }); refresh(); }
    catch (e) { on401(e); alert(e.message); }
    finally { setBusyId(null); }
  };

  const resetPw = async (u) => {
    if (!window.confirm(`'${u.email}' 의 비밀번호를 임시 비번으로 초기화하시겠어요?\n새 비번은 한 번만 표시되며 본인에게 직접 전달해야 합니다.`)) return;
    setBusyId(u.id);
    try {
      const r = await window.API.adminResetUserPassword(u.id);
      setResetOut(r);
    } catch (e) { on401(e); alert(e.message); }
    finally { setBusyId(null); }
  };

  const remove = async (u) => {
    setBusyId(u.id);
    try { await window.API.adminDeleteUser(u.id); refresh(); }
    catch (e) { on401(e); alert(e.message); }
    finally { setBusyId(null); setConfirmDel(null); }
  };

  return (
    <div className="ad-users">
      <div className="ad-row-actions">
        <input className="ad-search" type="text" placeholder="search email or name …" value={q} onChange={e => setQ(e.target.value)}/>
        <button className="ad-btn-mini" onClick={refresh}>Refresh</button>
        <button className="ad-btn-mini" onClick={() => setAddOpen(true)} style={{background:'var(--accent,var(--fg-2))',color:'var(--bg-canvas)'}}>+ 사용자 추가</button>
        <span className="ad-mono ad-dim" style={{marginLeft:'auto'}}>{data ? data.length + ' users' : ''}</span>
      </div>

      {err && <div className="ad-error">{err}</div>}
      {!data && !err && <div className="ad-loading">Loading users …</div>}

      {data && (
        <table className="ad-table">
          <thead>
            <tr>
              <th>Email</th><th>Name</th><th>Admin</th><th>상태</th>
              <th className="num">Threads</th><th className="num">Messages</th>
              <th>Last login</th><th>Joined</th><th></th>
            </tr>
          </thead>
          <tbody>
            {data.map(u => {
              const isSelf = u.id === currentUserId;
              const activeAdminCount = data.filter(x => x.is_admin && x.is_active).length;
              const lastAdmin = u.is_admin && activeAdminCount === 1;
              return (
                <tr key={u.id} className={isSelf ? 'me' : ''} style={!u.is_active ? {opacity:0.55} : null}>
                  <td><span className="ad-mono">{u.email}</span>{isSelf && <span className="ad-tag" style={{marginLeft:6}}>you</span>}</td>
                  <td>{u.display_name || <span className="ad-dim">—</span>}</td>
                  <td>
                    <button className={'ad-pill' + (u.is_admin ? ' on' : '')}
                      disabled={busyId === u.id || (lastAdmin && u.is_admin)}
                      onClick={() => toggleAdmin(u)}
                      title={lastAdmin && u.is_admin ? "마지막 admin 은 해제 불가" : (u.is_admin ? 'Demote' : 'Promote to admin')}
                    >{u.is_admin ? 'admin' : 'member'}</button>
                  </td>
                  <td>
                    <button className={'ad-pill' + (u.is_active ? ' on' : '')}
                      disabled={busyId === u.id || isSelf || (lastAdmin && u.is_active)}
                      onClick={() => toggleActive(u)}
                      title={
                        isSelf ? '자신은 정지 불가' :
                        (lastAdmin && u.is_active ? '마지막 활성 admin 은 정지 불가' :
                          (u.is_active ? '정지' : '활성화'))
                      }
                    >{u.is_active ? 'active' : 'suspended'}</button>
                  </td>
                  <td className="num ad-mono">{u.thread_count}</td>
                  <td className="num ad-mono">{u.message_count}</td>
                  <td className="ad-mono ad-dim">{u.last_login_at ? fmtDate(u.last_login_at) : '—'}</td>
                  <td className="ad-mono ad-dim">{fmtDate(u.created_at)}</td>
                  <td className="actions" style={{display:'flex', gap:6, justifyContent:'flex-end'}}>
                    <button className="ad-btn-mini" disabled={busyId === u.id} onClick={() => resetPw(u)}
                      title="비밀번호 초기화">Reset PW</button>
                    <button className="ad-btn-danger" disabled={busyId === u.id || isSelf}
                      title={isSelf ? '자신은 삭제 불가' : 'Delete user'}
                      onClick={() => setConfirmDel(u)}>Delete</button>
                  </td>
                </tr>
              );
            })}
            {data.length === 0 && <tr><td colSpan="9" className="ad-dim" style={{textAlign:'center',padding:'20px 0'}}>No users match.</td></tr>}
          </tbody>
        </table>
      )}

      {confirmDel && <ConfirmModal eb="Admin · Users"
        title={'Delete ' + confirmDel.email + '?'}
        desc={'This will remove the user and cascade-delete their ' + confirmDel.thread_count + ' thread' + (confirmDel.thread_count === 1 ? '' : 's') + ' and ' + confirmDel.message_count + ' message' + (confirmDel.message_count === 1 ? '' : 's') + '. Cannot be undone.'}
        confirmText="Delete user" requirePhrase={confirmDel.email}
        onClose={() => setConfirmDel(null)} onConfirm={() => remove(confirmDel)}/>}

      {addOpen && <AdminCreateUserModal
        onClose={() => setAddOpen(false)}
        onCreated={() => { setAddOpen(false); refresh(); }}/>}

      {resetOut && <AdminResetResultModal
        out={resetOut} onClose={() => setResetOut(null)}/>}
    </div>
  );
}

function AdminCreateUserModal({ onClose, onCreated }) {
  const [email, setEmail] = React.useState('');
  const [pw, setPw] = React.useState('');
  const [name, setName] = React.useState('');
  const [isAdmin, setIsAdmin] = React.useState(false);
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState(null);
  const valid = email.includes('@') && pw.length >= 8;

  const submit = async () => {
    if (!valid || busy) return;
    setBusy(true); setErr(null);
    try {
      await window.API.adminCreateUser({
        email: email.trim(),
        password: pw,
        display_name: name.trim() || null,
        is_admin: !!isAdmin,
      });
      onCreated();
    } catch (e) {
      on401(e);
      setErr(e.message || '생성 실패');
    } finally {
      setBusy(false);
    }
  };

  return (
    <div className="ch-modal-back" onClick={onClose}>
      <div className="ch-modal" onClick={e => e.stopPropagation()}>
        <div className="ch-modal-head">
          <div>
            <div className="pane-eb">Admin · Users</div>
            <h2>사용자 추가</h2>
          </div>
          <button className="ch-modal-x" onClick={onClose}>×</button>
        </div>
        <div className="ch-modal-body">
          <p className="desc">초대 코드 없이 즉시 활성 계정을 생성합니다. 본인에게 비밀번호를 직접 전달하세요.</p>
          <div className="ch-row"><div className="lbl">이메일</div>
            <div className="ctrl"><input type="email" value={email} onChange={e => setEmail(e.target.value)} autoComplete="off"/></div></div>
          <div className="ch-row"><div className="lbl">초기 비밀번호 <small>8자 이상</small></div>
            <div className="ctrl"><input type="text" value={pw} onChange={e => setPw(e.target.value)} autoComplete="new-password" style={{fontFamily:'var(--font-mono)'}}/></div></div>
          <div className="ch-row"><div className="lbl">표시 이름 <small>선택</small></div>
            <div className="ctrl"><input type="text" value={name} onChange={e => setName(e.target.value)} maxLength={80}/></div></div>
          <div className="ch-row"><div className="lbl">관리자 권한</div>
            <div className="ctrl"><label style={{display:'flex',gap:8,alignItems:'center'}}>
              <input type="checkbox" checked={isAdmin} onChange={e => setIsAdmin(e.target.checked)}/>
              <span style={{fontSize:13}}>이 사용자를 admin 으로 설정</span>
            </label></div></div>
          {err && <div className="ch-row"><div className="lbl"></div>
            <div className="ctrl"><span style={{color:'var(--zg-danger)', fontSize:12}}>{err}</span></div></div>}
        </div>
        <div className="ch-modal-foot">
          <button className="ghost" onClick={onClose}>취소</button>
          <button className={valid ? 'primary' : 'ghost'} disabled={!valid || busy} onClick={submit}>
            {busy ? '생성 중…' : '계정 생성'}
          </button>
        </div>
      </div>
    </div>
  );
}

function AdminResetResultModal({ out, onClose }) {
  const [copied, setCopied] = React.useState(false);
  const copy = async () => {
    try { await navigator.clipboard.writeText(out.temporary_password); setCopied(true); setTimeout(() => setCopied(false), 1500); }
    catch (_) {}
  };
  return (
    <div className="ch-modal-back" onClick={onClose}>
      <div className="ch-modal" onClick={e => e.stopPropagation()}>
        <div className="ch-modal-head">
          <div>
            <div className="pane-eb">Admin · Users · 비밀번호 초기화</div>
            <h2>임시 비밀번호 발급됨</h2>
          </div>
          <button className="ch-modal-x" onClick={onClose}>×</button>
        </div>
        <div className="ch-modal-body">
          <p className="desc" style={{color:'var(--zg-danger)'}}>
            이 비밀번호는 <strong>한 번만 표시</strong>됩니다. 창을 닫기 전에 복사하여 본인에게 직접 전달하세요.
            사용자에게 로그인 후 즉시 비밀번호를 변경하도록 안내하세요.
          </p>
          <div className="ch-row"><div className="lbl">대상</div>
            <div className="ctrl"><span className="ad-mono">{out.email}</span></div></div>
          <div className="ch-row"><div className="lbl">임시 비밀번호</div>
            <div className="ctrl">
              <input type="text" value={out.temporary_password} readOnly
                style={{fontFamily:'var(--font-mono)', fontSize:14, letterSpacing:'0.05em'}}/>
              <button className="ghost" onClick={copy} style={{padding:'8px 14px'}}>
                {copied ? '복사됨' : '복사'}
              </button>
            </div></div>
        </div>
        <div className="ch-modal-foot">
          <button className="primary" onClick={onClose}>확인 (다시 볼 수 없습니다)</button>
        </div>
      </div>
    </div>
  );
}


// ---------- Invites view ----------

function AdminInvitesView() {
  const [data, setData] = React.useState(null);
  const [err, setErr]   = React.useState(null);
  const [tick, setTick] = React.useState(0);
  const [busy, setBusy] = React.useState(false);
  const [note, setNote] = React.useState('');
  const [newCode, setNewCode] = React.useState(null);

  React.useEffect(() => {
    let cancelled = false;
    window.API.adminListInvites()
      .then(j => { if (!cancelled) { setData(j); setErr(null); } })
      .catch(e => { if (cancelled) return; on401(e); setErr(e.message); });
    return () => { cancelled = true; };
  }, [tick]);

  const refresh = () => setTick(t => t + 1);

  const create = async () => {
    setBusy(true);
    try {
      const inv = await window.API.adminCreateInvite({ note: note.trim() || null });
      setNote('');
      setNewCode(inv);
      refresh();
    } catch (e) { on401(e); alert(e.message); }
    finally { setBusy(false); }
  };

  const revoke = async (code) => {
    if (!window.confirm(`초대 코드 '${code}' 를 폐기하시겠어요?`)) return;
    setBusy(true);
    try { await window.API.adminDeleteInvite(code); refresh(); }
    catch (e) { on401(e); alert(e.message); }
    finally { setBusy(false); }
  };

  const copy = async (text) => {
    try { await navigator.clipboard.writeText(text); } catch (_) {}
  };

  const now = Math.floor(Date.now() / 1000);

  return (
    <div className="ad-users">
      <div className="ad-row-actions" style={{gap:8}}>
        <input className="ad-search" type="text" placeholder="메모 (선택, 누구에게 줄 코드인지)"
               value={note} onChange={e => setNote(e.target.value)} maxLength={200}/>
        <button className="ad-btn-mini" onClick={create} disabled={busy}
                style={{background:'var(--accent,var(--fg-2))',color:'var(--bg-canvas)'}}>
          + 초대 코드 발급
        </button>
        <button className="ad-btn-mini" onClick={refresh}>Refresh</button>
        <span className="ad-mono ad-dim" style={{marginLeft:'auto'}}>{data ? data.length + ' invites' : ''}</span>
      </div>

      {err && <div className="ad-error">{err}</div>}
      {!data && !err && <div className="ad-loading">Loading invites …</div>}

      {data && (
        <table className="ad-table">
          <thead>
            <tr>
              <th>Code</th><th>상태</th><th>발급자</th><th>사용자</th>
              <th>발급</th><th>만료</th><th>메모</th><th></th>
            </tr>
          </thead>
          <tbody>
            {data.map(inv => {
              const used = inv.used_by_user_id != null;
              const expired = !used && inv.expires_at < now;
              const state = used ? '사용됨' : (expired ? '만료' : '활성');
              return (
                <tr key={inv.code} style={(used || expired) ? {opacity:0.55} : null}>
                  <td>
                    <button className="ad-mono" title="클릭해 복사"
                      onClick={() => copy(inv.code)}
                      style={{background:'transparent',border:'none',color:'var(--fg-1)',padding:0,cursor:'pointer'}}>
                      {inv.code}
                    </button>
                  </td>
                  <td><span className={'ad-pill' + ((!used && !expired) ? ' on' : '')}>{state}</span></td>
                  <td className="ad-mono">{inv.created_by_email || ('#' + inv.created_by_user_id)}</td>
                  <td className="ad-mono">{inv.used_by_email || <span className="ad-dim">—</span>}</td>
                  <td className="ad-mono ad-dim">{fmtDate(inv.created_at)}</td>
                  <td className="ad-mono ad-dim">{fmtDate(inv.expires_at)}</td>
                  <td>{inv.note || <span className="ad-dim">—</span>}</td>
                  <td className="actions">
                    {!used && (
                      <button className="ad-btn-danger" disabled={busy} onClick={() => revoke(inv.code)}>폐기</button>
                    )}
                  </td>
                </tr>
              );
            })}
            {data.length === 0 && <tr><td colSpan="8" className="ad-dim" style={{textAlign:'center',padding:'20px 0'}}>발급된 초대 코드가 없습니다.</td></tr>}
          </tbody>
        </table>
      )}

      {newCode && (
        <div className="ch-modal-back" onClick={() => setNewCode(null)}>
          <div className="ch-modal" onClick={e => e.stopPropagation()}>
            <div className="ch-modal-head">
              <div>
                <div className="pane-eb">Admin · Invites</div>
                <h2>초대 코드 발급됨</h2>
              </div>
              <button className="ch-modal-x" onClick={() => setNewCode(null)}>×</button>
            </div>
            <div className="ch-modal-body">
              <p className="desc">아래 코드를 본인에게 전달하세요. 1회용이며 {fmtDate(newCode.expires_at)} 까지 유효합니다.</p>
              <div className="ch-row"><div className="lbl">코드</div>
                <div className="ctrl">
                  <input type="text" value={newCode.code} readOnly
                    style={{fontFamily:'var(--font-mono)', fontSize:14, letterSpacing:'0.05em'}}/>
                  <button className="ghost" onClick={() => copy(newCode.code)} style={{padding:'8px 14px'}}>복사</button>
                </div></div>
            </div>
            <div className="ch-modal-foot">
              <button className="primary" onClick={() => setNewCode(null)}>확인</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}


// ---------- Threads view ----------

function AdminThreadsView() {
  const [data, setData] = React.useState(null);
  const [err, setErr] = React.useState(null);
  const [q, setQ] = React.useState('');
  const [tick, setTick] = React.useState(0);
  const [busyId, setBusyId] = React.useState(null);

  React.useEffect(() => {
    let cancelled = false;
    window.API.adminListThreads(q)
      .then(j => { if (!cancelled) { setData(j); setErr(null); } })
      .catch(e => { if (cancelled) return; on401(e); setErr(e.message); });
    return () => { cancelled = true; };
  }, [tick, q]);

  const remove = async (t) => {
    if (!window.confirm('Delete thread "' + t.title + '" (owner ' + t.user_email + ', ' + t.message_count + ' msgs)?')) return;
    setBusyId(t.id);
    try { await window.API.adminDeleteThread(t.id); setTick(x => x + 1); }
    catch (e) { on401(e); alert(e.message); }
    finally { setBusyId(null); }
  };

  return (
    <div className="ad-threads">
      <div className="ad-row-actions">
        <input className="ad-search" type="text" placeholder="search title or owner email …" value={q} onChange={e => setQ(e.target.value)}/>
        <button className="ad-btn-mini" onClick={() => setTick(t => t + 1)}>Refresh</button>
        <span className="ad-mono ad-dim" style={{marginLeft:'auto'}}>{data ? data.length + ' threads' : ''}</span>
      </div>

      {err && <div className="ad-error">{err}</div>}
      {!data && !err && <div className="ad-loading">Loading threads …</div>}

      {data && (
        <table className="ad-table">
          <thead>
            <tr><th>Title</th><th>Owner</th><th>Model</th><th className="num">Msgs</th><th>Updated</th><th></th></tr>
          </thead>
          <tbody>
            {data.map(t => (
              <tr key={t.id}>
                <td>{t.pinned && <span className="ad-tag" style={{marginRight:6}}>pinned</span>}{t.title}</td>
                <td><span className="ad-mono">{t.user_email}</span></td>
                <td><span className="ad-mono ad-dim">{t.model}</span></td>
                <td className="num ad-mono">{t.message_count}</td>
                <td className="ad-mono ad-dim">{fmtDate(t.updated_at)}</td>
                <td className="actions">
                  <button className="ad-btn-danger" disabled={busyId === t.id} onClick={() => remove(t)}>Delete</button>
                </td>
              </tr>
            ))}
            {data.length === 0 && <tr><td colSpan="6" className="ad-dim" style={{textAlign:'center',padding:'20px 0'}}>No threads match.</td></tr>}
          </tbody>
        </table>
      )}
    </div>
  );
}


// ---------- Datasets view ----------

function AdminDatasetsView() {
  const [data, setData] = React.useState(null);
  const [err, setErr] = React.useState(null);
  const [tick, setTick] = React.useState(0);
  const [uploading, setUploading] = React.useState(false);
  const [uploadProgress, setUploadProgress] = React.useState(null);
  const fileInputRef = React.useRef(null);

  React.useEffect(() => {
    let cancelled = false;
    window.API.adminListDatasets()
      .then(j => { if (!cancelled) { setData(j); setErr(null); } })
      .catch(e => { if (cancelled) return; on401(e); setErr(e.message); });
    return () => { cancelled = true; };
  }, [tick]);

  const handleFile = async (file) => {
    if (!file) return;
    if (file.size > 200 * 1024 * 1024) { alert('File exceeds 200 MB cap.'); return; }
    setUploading(true);
    setUploadProgress({ name: file.name, sent: 0, total: file.size });
    try {
      await window.API.adminUploadDataset(file, (sent, total) => setUploadProgress({ name: file.name, sent, total }));
      setTick(t => t + 1);
    } catch (e) {
      on401(e);
      alert('Upload failed · ' + e.message);
    } finally {
      setUploading(false);
      setUploadProgress(null);
      if (fileInputRef.current) fileInputRef.current.value = '';
    }
  };

  const deleteDataset = async (d) => {
    if (d.kind === 'directory') { alert('Directories cannot be deleted via this UI.'); return; }
    if (!window.confirm('Delete ' + d.name + ' (' + fmtBytes(d.size_bytes) + ')?')) return;
    try { await window.API.adminDeleteDataset(d.name); setTick(t => t + 1); }
    catch (e) { on401(e); alert(e.message); }
  };

  const downloadDataset = (d) => {
    if (d.kind === 'directory') { alert('Directories cannot be downloaded via this UI.'); return; }
    window.API.adminDownloadDataset(d.name).catch(e => { on401(e); alert('Download failed · ' + e.message); });
  };

  return (
    <div className="ad-datasets">
      <div className="ad-section">
        <h3>Upload corpus</h3>
        <div className="ad-upload-row">
          <input
            ref={fileInputRef}
            type="file"
            accept=".txt,.csv,.tsv,.jsonl,.json,.md"
            onChange={e => handleFile(e.target.files && e.target.files[0])}
            disabled={uploading}
          />
          <span className="ad-mono ad-dim">accepted: .txt .csv .tsv .jsonl .json .md · max 200 MB</span>
        </div>
        {uploadProgress && (
          <div style={{marginTop: 12}}>
            <div className="ad-bar-row">
              <div className="ad-bar-label">{uploadProgress.name}</div>
              <ProgressBar percent={uploadProgress.total ? (uploadProgress.sent / uploadProgress.total) * 100 : 0}/>
              <div className="ad-bar-value">{fmtBytes(uploadProgress.sent)} / {fmtBytes(uploadProgress.total)}</div>
            </div>
          </div>
        )}
      </div>

      <div className="ad-row-actions" style={{marginTop: 20}}>
        <span className="ad-mono ad-dim">data/</span>
        <button className="ad-btn-mini" onClick={() => setTick(t => t + 1)}>Refresh</button>
        <span className="ad-mono ad-dim" style={{marginLeft:'auto'}}>{data ? data.length + ' entries' : ''}</span>
      </div>

      {err && <div className="ad-error">{err}</div>}
      {!data && !err && <div className="ad-loading">Loading datasets …</div>}

      {data && (
        <table className="ad-table">
          <thead><tr><th>Name</th><th>Kind</th><th className="num">Size</th><th>Modified</th><th></th></tr></thead>
          <tbody>
            {data.map(d => (
              <tr key={d.name}>
                <td><span className="ad-mono">{d.name}</span></td>
                <td>
                  <span className="ad-tag">{d.kind}</span>
                  {d.ext && <span className="ad-mono ad-dim" style={{marginLeft:6}}>{d.ext}</span>}
                </td>
                <td className="num ad-mono">{fmtBytes(d.size_bytes)}</td>
                <td className="ad-mono ad-dim">{fmtDate(d.modified_at)}</td>
                <td className="actions">
                  {d.kind === 'file' && <>
                    <button className="ad-btn-mini" onClick={() => downloadDataset(d)}>Get</button>
                    <button className="ad-btn-danger" style={{marginLeft:6}} onClick={() => deleteDataset(d)}>Delete</button>
                  </>}
                </td>
              </tr>
            ))}
            {data.length === 0 && <tr><td colSpan="5" className="ad-dim" style={{textAlign:'center',padding:'20px 0'}}>No datasets. Upload one above.</td></tr>}
          </tbody>
        </table>
      )}
    </div>
  );
}


// ---------- Training view + log tail ----------

function StatusBadge({ status }) {
  const map = {
    running:   { color: 'var(--accent)',     bg: 'var(--accent-tint)' },
    completed: { color: 'var(--zg-success)', bg: 'rgba(111,158,92,0.12)' },
    failed:    { color: 'var(--zg-danger)',  bg: 'rgba(201,92,74,0.12)' },
    stopped:   { color: 'var(--fg-3)',       bg: 'var(--bg-elevated)' },
    queued:    { color: 'var(--zg-warning)', bg: 'rgba(217,162,62,0.12)' },
  };
  const c = map[status] || { color: 'var(--fg-3)', bg: 'var(--bg-elevated)' };
  return (
    <span className="ad-status-badge" style={{ color: c.color, background: c.bg }}>
      {status === 'running' && <span className="ad-status-dot" style={{ background: c.color }}/>}
      {status}
    </span>
  );
}

function AdminTrainingView() {
  const [scripts, setScripts] = React.useState(null);
  const [jobs, setJobs] = React.useState(null);
  const [err, setErr] = React.useState(null);
  const [selectedScript, setSelectedScript] = React.useState('');
  const [launching, setLaunching] = React.useState(false);
  const [selectedJobId, setSelectedJobId] = React.useState(null);
  const [tick, setTick] = React.useState(0);
  const [confirmLaunch, setConfirmLaunch] = React.useState(false);
  const [confirmStop, setConfirmStop] = React.useState(null);

  React.useEffect(() => {
    let cancelled = false;
    Promise.all([
      window.API.adminListTrainingScripts(),
      window.API.adminListTrainingJobs(50),
    ])
      .then(([s, j]) => {
        if (cancelled) return;
        setScripts(s);
        if (s.length && !selectedScript) setSelectedScript(s[s.length - 1].name);
        setJobs(j);
        if (!selectedJobId && j.length) setSelectedJobId(j[0].id);
        setErr(null);
      })
      .catch(e => { if (cancelled) return; on401(e); setErr(e.message); });
    return () => { cancelled = true; };
  // eslint-disable-next-line
  }, [tick]);

  // re-poll when any job is running
  React.useEffect(() => {
    const hasRunning = jobs && jobs.some(j => j.status === 'running');
    if (!hasRunning) return;
    const id = setInterval(() => setTick(t => t + 1), 3000);
    return () => clearInterval(id);
  }, [jobs]);

  const runningJob = jobs && jobs.find(j => j.status === 'running');
  const slotFree = !runningJob;

  const launch = async () => {
    setConfirmLaunch(false);
    if (!selectedScript) { alert('Pick a script first.'); return; }
    setLaunching(true);
    try {
      const created = await window.API.adminLaunchTraining(selectedScript);
      setSelectedJobId(created.id);
      setTick(t => t + 1);
    } catch (e) {
      on401(e); alert(e.message);
    } finally { setLaunching(false); }
  };

  const stop = async (j) => {
    setConfirmStop(null);
    try { await window.API.adminStopTraining(j.id); setTick(t => t + 1); }
    catch (e) { on401(e); alert('Stop failed · ' + e.message); }
  };

  const remove = async (j) => {
    if (!window.confirm('Delete record of job #' + j.id + ' and its log?')) return;
    try {
      await window.API.adminDeleteTrainingJob(j.id);
      if (selectedJobId === j.id) setSelectedJobId(null);
      setTick(t => t + 1);
    } catch (e) { on401(e); alert(e.message); }
  };

  return (
    <div className="ad-training">
      <div className="ad-section">
        <h3>Launch a training job <span className="ad-h3-sub">{slotFree ? '◯ slot free' : '● slot busy · job #' + runningJob.id}</span></h3>
        <div className="ad-launch-row">
          <select
            className="ad-select"
            value={selectedScript}
            onChange={e => setSelectedScript(e.target.value)}
            disabled={launching || !scripts || !slotFree}
          >
            <option value="">— select a script —</option>
            {scripts && scripts.map(s => (
              <option key={s.name} value={s.name}>{s.name} · {s.description}</option>
            ))}
          </select>
          <button className="ad-btn-primary" onClick={() => setConfirmLaunch(true)} disabled={launching || !selectedScript || !slotFree}>
            {launching ? 'Launching …' : (slotFree ? 'Launch' : 'Slot busy')}
          </button>
        </div>
        <div className="ad-mono ad-dim" style={{marginTop: 8, fontSize: 11.5}}>
          Single-slot: only one training subprocess at a time (GPU contention prevention). New launches are blocked while a job is running.
        </div>
      </div>

      <div className="ad-row-actions" style={{marginTop: 20}}>
        <span>Jobs</span>
        <button className="ad-btn-mini" onClick={() => setTick(t => t + 1)}>Refresh</button>
        <span className="ad-mono ad-dim" style={{marginLeft:'auto'}}>{jobs ? jobs.length + ' total' : ''}</span>
      </div>

      {err && <div className="ad-error">{err}</div>}
      {!jobs && !err && <div className="ad-loading">Loading jobs …</div>}

      {jobs && (
        <table className="ad-table">
          <thead>
            <tr><th>ID</th><th>Script</th><th>Status</th><th>PID</th><th>Started</th><th>Duration</th><th>By</th><th></th></tr>
          </thead>
          <tbody>
            {jobs.map(j => {
              const dur = j.finished_at ? j.finished_at - j.started_at
                       : j.status === 'running' ? (Date.now()/1000 - j.started_at)
                       : null;
              return (
                <tr key={j.id} className={selectedJobId === j.id ? 'me' : ''} onClick={() => setSelectedJobId(j.id)} style={{cursor:'pointer'}}>
                  <td className="ad-mono">#{j.id}</td>
                  <td className="ad-mono">{j.script_name}</td>
                  <td><StatusBadge status={j.status}/></td>
                  <td className="ad-mono ad-dim">{j.pid || '—'}</td>
                  <td className="ad-mono ad-dim">{fmtDate(j.started_at)}</td>
                  <td className="ad-mono">{dur != null ? fmtDuration(dur) : '—'}</td>
                  <td className="ad-mono ad-dim">{j.started_by_email}</td>
                  <td className="actions" onClick={e => e.stopPropagation()}>
                    {j.status === 'running'
                      ? <button className="ad-btn-danger" onClick={() => setConfirmStop(j)}>Stop</button>
                      : <button className="ad-btn-danger" onClick={() => remove(j)}>Delete</button>}
                  </td>
                </tr>
              );
            })}
            {jobs.length === 0 && <tr><td colSpan="8" className="ad-dim" style={{textAlign:'center',padding:'20px 0'}}>No jobs yet. Pick a script above and launch.</td></tr>}
          </tbody>
        </table>
      )}

      {selectedJobId != null && <TrainingLogTail jobId={selectedJobId} onClose={() => setSelectedJobId(null)}/>}

      {confirmLaunch && <ConfirmModal eb="Admin · Training" title={'Launch ' + selectedScript + '?'} desc="The script will run as a subprocess and stream stdout/stderr to a log file. Slot will be busy until the job exits." confirmText="Launch job" onClose={() => setConfirmLaunch(false)} onConfirm={launch}/>}
      {confirmStop && <ConfirmModal eb="Admin · Training" title={'Stop job #' + confirmStop.id + '?'} desc="Sends SIGTERM to PID. The process has 5 seconds to flush before SIGKILL. Checkpoints since the last save will be lost." confirmText="Stop job" onClose={() => setConfirmStop(null)} onConfirm={() => stop(confirmStop)}/>}
    </div>
  );
}


function TrainingLogTail({ jobId, onClose }) {
  const [content, setContent] = React.useState('');
  const [status, setStatus] = React.useState('');
  const offsetRef = React.useRef(0);
  const preRef = React.useRef(null);
  const autoScrollRef = React.useRef(true);

  React.useEffect(() => {
    offsetRef.current = 0;
    setContent('');
    setStatus('');
  }, [jobId]);

  React.useEffect(() => {
    let cancelled = false;
    let timer = null;
    const poll = async () => {
      if (cancelled) return;
      try {
        const j = await window.API.adminTrainingLogChunk(jobId, offsetRef.current, 64 * 1024);
        if (cancelled) return;
        if (j.content) {
          setContent(prev => prev + j.content);
          offsetRef.current = j.next_offset;
        }
        setStatus(j.status);
        const delay = j.status === 'running' ? 1500 : 4000;
        timer = setTimeout(poll, delay);
      } catch (e) {
        if (!cancelled) timer = setTimeout(poll, 5000);
      }
    };
    poll();
    return () => { cancelled = true; if (timer) clearTimeout(timer); };
  }, [jobId]);

  React.useEffect(() => {
    if (autoScrollRef.current && preRef.current) {
      preRef.current.scrollTop = preRef.current.scrollHeight;
    }
  }, [content]);

  const onScroll = (e) => {
    const el = e.target;
    autoScrollRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 40;
  };

  return (
    <div className="ad-section ad-logpanel">
      <h3>
        Log · job #{jobId}
        <span className="ad-h3-sub">{status}</span>
        <button className="ad-btn-mini" style={{marginLeft:'auto'}} onClick={onClose}>Close</button>
      </h3>
      <pre ref={preRef} onScroll={onScroll} className="ad-logpre">{content || '(no log yet — waiting for first flush)'}</pre>
    </div>
  );
}


Object.assign(window, { AdminShell });
// Training lives in the same module now; export a stub so the old <script src="AdminTraining.jsx"> doesn't fail if the file is still listed.
window.AdminTraining = AdminTrainingView;
