<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>UCAgent Master Dashboard</title>
<link rel="icon" type="image/svg+xml" href="/static/share/ucagent-favicon.svg">
<link rel="stylesheet" href="/static/share/ucagent-shell.css">
<script>
(function(){
  var CH='ucagent-master-dashboard';
  var bc=typeof BroadcastChannel!=='undefined'?new BroadcastChannel(CH):null;
  var _overlay=null;
  function showSuperseded(){
    if(_overlay)return;
    _overlay=document.createElement('div');
    _overlay.style.cssText='position:fixed;inset:0;z-index:99999;background:rgba(15,17,23,.96);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;font-family:monospace;';
    _overlay.innerHTML='<div style="font-size:22px;color:#ef4444;">&#9888; Dashboard opened in another tab</div>'
      +'<div style="color:#8892a4;font-size:13px;">This tab has been superseded. Close it or</div>'
      +'<button onclick="location.reload()" style="padding:8px 22px;background:#6366f1;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:14px;">Reactivate this tab</button>';
    document.body?document.body.appendChild(_overlay):document.addEventListener('DOMContentLoaded',function(){document.body.appendChild(_overlay)});
  }
  if(bc){
    bc.onmessage=function(e){if(e.data==='opened')showSuperseded();};
    bc.postMessage('opened');
    window.addEventListener('beforeunload',function(){bc.close();});
  }
})();
</script>
<style>
  :root{
    --bg:#0f1117; --surface:#1a1d26; --surface2:#22263a;
    --border:#2e3248; --text:#e2e8f0; --text-dim:#8892a4;
    --online:#22c55e; --offline:#ef4444; --accent:#6366f1;
    --accent-hover:#818cf8; --warn:#f59e0b; --red:#ef4444;
    --radius:8px; --font:'Segoe UI',system-ui,sans-serif;
  }
  *{box-sizing:border-box;margin:0;padding:0}
  body{background:var(--bg);color:var(--text);font-family:var(--font);font-size:14px;min-height:100vh}
  a{color:var(--accent);text-decoration:none} a:hover{color:var(--accent-hover);text-decoration:underline}
  /* ── Header ─────────────────────────────────────────────────────────── */
  header{background:var(--surface);border-bottom:1px solid var(--border);
    padding:14px 28px;display:flex;align-items:center;gap:16px;flex-wrap:wrap}
  .logo{font-size:18px;font-weight:700;letter-spacing:.5px;color:#fff}
  .logo span{color:var(--accent)}
  .header-actions{margin-left:auto;display:flex;align-items:center;gap:12px;flex-wrap:wrap}
  .refresh-info{color:var(--text-dim);font-size:12px}
  #countdown{font-weight:600;color:var(--accent)}
  /* ── Buttons ─────────────────────────────────────────────────────────── */
  .btn{display:inline-flex;align-items:center;gap:4px;padding:5px 10px;
    border-radius:var(--radius);border:none;cursor:pointer;font-size:13px;
    font-family:var(--font);font-weight:500;transition:background .15s,transform .1s}
  .btn:active{transform:scale(.97)}
  .btn-primary{background:var(--accent);color:#fff} .btn-primary:hover{background:var(--accent-hover)}
  .btn-danger{background:#7f1d1d;color:#fca5a5;border:1px solid #b91c1c}
  .btn-danger:hover{background:#991b1b}
  .btn-ghost{background:transparent;color:var(--text-dim);border:1px solid var(--border)}
  .btn-ghost:hover{background:var(--surface2);color:var(--text)}
  .btn-secondary{background:var(--surface2);color:var(--text);border:1px solid var(--border)}
  .btn-secondary:hover{background:#334155;border-color:var(--accent)}
  .btn-sm{padding:3px 8px;font-size:11px}
  .btn-icon{padding:4px 6px}
  .btn:disabled{opacity:.4;cursor:not-allowed}
  /* ── Main ────────────────────────────────────────────────────────────── */
  main{padding:24px 28px;max-width:1600px;width:100%;overflow-x:hidden}
  /* ── Stats ───────────────────────────────────────────────────────────── */
  .stats{display:flex;gap:16px;margin-bottom:24px;flex-wrap:wrap}
  .stat-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
    padding:16px 22px;min-width:130px;flex:0 0 auto}
  .stat-label{font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.8px;margin-bottom:6px}
  .stat-value{font-size:28px;font-weight:700}
  .stat-online{color:var(--online)} .stat-offline{color:var(--offline)} .stat-total{color:var(--accent)}
  /* ── Toolbar ─────────────────────────────────────────────────────────── */
  .toolbar{display:flex;align-items:center;gap:10px;margin-bottom:14px;flex-wrap:wrap}
  .search-box{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
    color:var(--text);padding:7px 12px;font-size:13px;font-family:var(--font);flex:1;min-width:160px;max-width:320px}
  .search-box:focus{outline:none;border-color:var(--accent)}
  .filter-select{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
    color:var(--text);padding:7px 10px;font-size:13px;font-family:var(--font);cursor:pointer}
  .filter-select:focus{outline:none;border-color:var(--accent)}
  /* ── Table ───────────────────────────────────────────────────────────── */
  .table-wrap{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
  table{width:100%;border-collapse:collapse;table-layout:auto}
  thead th{background:var(--surface2);padding:8px 8px;text-align:left;font-size:10px;
    text-transform:uppercase;letter-spacing:.7px;color:var(--text-dim);white-space:nowrap;
    border-bottom:1px solid var(--border)}
  thead th:first-child{width:30px;text-align:center}
  tbody tr{border-bottom:1px solid var(--border);transition:background .12s}
  tbody tr:last-child{border-bottom:none}
  tbody tr:hover{background:var(--surface2)}
  tbody td{padding:7px 8px;vertical-align:middle;white-space:nowrap}
  tbody td:first-child{text-align:center}
  .empty-row td{text-align:center;padding:48px;color:var(--text-dim);font-style:italic}
  .loading-row td{text-align:center;padding:32px;color:var(--text-dim)}
  /* ── Badges ──────────────────────────────────────────────────────────── */
  .badge{display:inline-block;padding:1px 6px;border-radius:999px;font-size:10px;font-weight:600;white-space:nowrap}
  .badge-online{background:#14532d;color:var(--online)}
  .badge-offline{background:#450a0a;color:#fca5a5}
  .badge-yes{background:#1e3a5f;color:#93c5fd}
  .badge-break{background:#451a03;color:#fed7aa}
  .progress-bar-wrap{background:var(--bg);border-radius:4px;height:5px;min-width:48px;overflow:hidden;margin-bottom:2px}
  .progress-bar{height:100%;border-radius:4px;background:var(--accent);transition:width .3s}
  .progress-label{font-size:10px;color:var(--text-dim)}
  .cmd-cell{display:inline-block;max-width:132px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
    font-family:'Consolas','Courier New',monospace;font-size:11px;color:var(--text-dim)}
  .mission-cell{display:inline-block;max-width:110px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
    font-size:11px;color:var(--text-dim)}
  .id-cell{display:inline-block;font-family:'Consolas','Courier New',monospace;font-size:11px;max-width:128px;
    overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .stage-cell{display:inline-block;max-width:96px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px}
  .host-cell{display:inline-block;max-width:108px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px}
  .version-cell{display:inline-block;max-width:72px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px}
  input[type=checkbox]{width:13px;height:13px;cursor:pointer;accent-color:var(--accent)}
  /* ── Modal ───────────────────────────────────────────────────────────── */
  .modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.65);
    z-index:100;align-items:center;justify-content:center;padding:20px}
  .modal-overlay.open{display:flex}
  .modal{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
    width:100%;max-width:720px;max-height:90vh;display:flex;flex-direction:column;overflow:hidden}
  .modal-header{display:flex;align-items:center;padding:16px 20px;border-bottom:1px solid var(--border);gap:12px}
  .modal-title{font-size:15px;font-weight:600;flex:1}
  .modal-body{padding:20px;overflow-y:auto;flex:1}
  .modal-actions{display:flex;gap:10px;padding:14px 20px;border-top:1px solid var(--border);justify-content:flex-end}
  pre.detail-json{background:var(--bg);border:1px solid var(--border);border-radius:6px;
    padding:14px;font-size:12px;font-family:'Consolas','Courier New',monospace;
    white-space:pre-wrap;word-break:break-all;color:#c3e88d;max-height:400px;overflow-y:auto}
  .detail-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px;margin-bottom:16px}
  .detail-field{background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:10px 12px}
  .detail-field-label{font-size:10px;text-transform:uppercase;letter-spacing:.7px;color:var(--text-dim);margin-bottom:4px}
  .detail-field-value{font-size:13px;word-break:break-all}
  .section-label{font-size:11px;text-transform:uppercase;letter-spacing:.7px;color:var(--text-dim);
    margin-bottom:8px;margin-top:16px}
  details.json-collapse{margin-top:16px}
  details.json-collapse>summary{cursor:pointer;user-select:none;list-style:none;
    font-size:11px;text-transform:uppercase;letter-spacing:.7px;color:var(--text-dim);
    margin-bottom:8px;display:flex;align-items:center;gap:6px;outline:none}
  details.json-collapse>summary::-webkit-details-marker{display:none}
  details.json-collapse>summary::before{content:'▶';font-size:9px}
  details.json-collapse[open]>summary::before{content:'▼'}
  details.json-collapse>summary:hover{color:var(--text)}
  /* ── Confirm overlay ─────────────────────────────────────────────────── */
  .confirm-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.65);
    z-index:200;align-items:center;justify-content:center;padding:20px}
  .confirm-overlay.open{display:flex}
  .confirm-box{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
    padding:28px 32px;max-width:420px;width:100%;text-align:center}
  .confirm-box h3{margin-bottom:10px;font-size:16px}
  .confirm-box p{color:var(--text-dim);margin-bottom:22px;line-height:1.5}
  .confirm-actions{display:flex;gap:12px;justify-content:center}
  /* ── Toast ───────────────────────────────────────────────────────────── */
  #toast-container{position:fixed;bottom:24px;right:24px;z-index:300;display:flex;flex-direction:column;gap:8px}
  .toast{background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius);
    padding:10px 16px;font-size:13px;min-width:200px;max-width:380px;box-shadow:0 4px 20px rgba(0,0,0,.4);
    animation:slide-in .25s ease}
  .toast-ok{border-left:3px solid var(--online)}
  .toast-err{border-left:3px solid var(--red)}
  @keyframes slide-in{from{transform:translateX(100%);opacity:0}to{transform:none;opacity:1}}
  /* ── Responsive ─────────────────────────────────────────────────────── */
  @media(max-width:768px){
    header{padding:10px 14px}
    main{padding:14px 12px}
    .stat-value{font-size:22px}
    .cmd-cell{max-width:100px}
    .mission-cell{max-width:100px}
    .stage-cell{max-width:90px}
    .id-cell{max-width:120px}
  }
</style>
</head>
<body>
<header>
  <div class="logo uc-shell-brand"><span class="uc-shell-brand-mark">UC</span><span>UC<span>Agent</span> Master</span></div>
  <div class="refresh-info">Auto-refresh in <span id="countdown">5</span>s</div>
  <div class="header-actions" id="uc-shell-header-actions">
    <a href="/launch/" class="btn btn-secondary btn-sm">Launch</a>
    <a href="/task/" class="btn btn-secondary btn-sm">Task</a>
    <label style="color:var(--text-dim);font-size:13px;display:flex;align-items:center;gap:6px;cursor:pointer" title="Display offline agents (not removed from database)">
      <input type="checkbox" id="show-offline" checked style="accent-color:var(--accent)"> Show offline
    </label>
    <label style="color:var(--text-dim);font-size:13px;display:flex;align-items:center;gap:6px;cursor:pointer" title="Automatically delete agents that have been offline for the configured duration">
      <input type="checkbox" id="auto-delete-offline" style="accent-color:var(--accent)"> Auto-delete offline
    </label>
    <button class="btn btn-ghost btn-sm" onclick="openSettings()" title="Configure auto-delete settings">⚙ Settings</button>
    <button class="btn btn-ghost btn-sm" onclick="refreshNow()">⟳ Refresh</button>
    <a href="/docs" target="_blank" class="btn btn-primary btn-sm">📖 API Docs</a>
  </div>
</header>
<main>
  <div class="stats">
    <div class="stat-card"><div class="stat-label">Total</div><div class="stat-value stat-total" id="stat-total">—</div></div>
    <div class="stat-card"><div class="stat-label">Online</div><div class="stat-value stat-online" id="stat-online">—</div></div>
    <div class="stat-card"><div class="stat-label">Offline</div><div class="stat-value stat-offline" id="stat-offline">—</div></div>
  </div>
  <div class="toolbar">
    <input type="search" class="search-box" id="search-box" placeholder="Filter by ID / host / mission…" oninput="onSearchChange()"/>
    <select class="filter-select" id="filter-status" onchange="renderTable()">
      <option value="all">All status</option>
      <option value="online">Online only</option>
      <option value="offline">Offline only</option>
    </select>
    <select class="filter-select" id="sort-by" onchange="onSortChange()" title="Sort agent list">
      <option value="id">Sort by: ID</option>
      <option value="host">Sort by: Host</option>
      <option value="status">Sort by: Status</option>
      <option value="last_seen">Sort by: Last seen</option>
      <option value="first_seen">Sort by: First seen</option>
      <option value="current_stage_index">Sort by: Progress</option>
    </select>
    <button class="btn btn-ghost btn-sm" id="sort-order-btn" onclick="toggleSortOrder()" title="Toggle ascending/descending">↓ Desc</button>
    <button class="btn btn-ghost btn-sm" id="select-all-btn" onclick="toggleSelectAll()">☐ Select all</button>
    <button class="btn btn-danger btn-sm" id="batch-delete-btn" onclick="confirmBatchDelete()" disabled>🗑 Delete (<span id="selected-count">0</span>)</button>
  </div>
  <div style="display:flex;align-items:center;gap:12px;margin-bottom:14px;flex-wrap:wrap;font-size:12px">
    <span style="color:var(--text-dim)">Per page:</span>
    <select id="page-size-select" onchange="onPageSizeChange()" style="background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);color:var(--text);padding:4px 8px;font-size:12px;cursor:pointer">
      <option value="10">10</option>
      <option value="20" selected>20</option>
      <option value="50">50</option>
      <option value="100">100</option>
    </select>
    <span id="pagination-info" style="color:var(--text-dim);margin-left:auto">—</span>
    <button class="btn btn-ghost btn-sm" id="page-prev" onclick="prevPage()" disabled>← Prev</button>
    <span id="page-num" style="color:var(--text-dim);min-width:40px;text-align:center">0/0</span>
    <button class="btn btn-ghost btn-sm" id="page-next" onclick="nextPage()" disabled>Next →</button>
  </div>
  <div class="table-wrap">
    <table>
      <thead>
        <tr>
          <th><input type="checkbox" id="header-checkbox" onchange="onHeaderCheckboxChange()" title="Select all visible"/></th>
          <th>ID</th>
          <th>Host</th>
          <th>Status</th>
          <th>Local</th>
          <th>Version</th>
          <th>Progress</th>
          <th>Run Time</th>
          <th>Stage</th>
          <th>MCP</th>
          <th>Break</th>
          <th>Last CMD</th>
          <th>Mission</th>
          <th>Actions</th>
        </tr>
      </thead>
      <tbody id="agents-tbody">
        <tr class="loading-row"><td colspan="14">Loading…</td></tr>
      </tbody>
    </table>
  </div>
</main>
<footer id="uc-shell-footer" class="uc-shell-footer"></footer>
<!-- ── Detail Modal ──────────────────────────────────────────────────── -->
<div class="modal-overlay" id="detail-modal">
  <div class="modal">
    <div class="modal-header">
      <span class="modal-title" id="detail-modal-title">Agent Detail</span>
      <button class="btn btn-ghost btn-icon btn-sm" onclick="closeDetailModal()">✕</button>
    </div>
    <div class="modal-body" id="detail-modal-body"></div>
    <div class="modal-actions">
      <button class="btn btn-danger btn-sm" id="detail-delete-btn" onclick="confirmDeleteFromDetail()">🗑 Delete this agent</button>
      <button class="btn btn-secondary btn-sm" id="detail-refresh-btn" onclick="refreshDetail()">↻ Refresh</button>
      <button class="btn btn-ghost btn-sm" onclick="closeDetailModal()">Close</button>
    </div>
  </div>
</div>
<!-- ── Settings Modal ──────────────────────────────────────────────────── -->
<div class="modal-overlay" id="settings-modal">
  <div class="modal">
    <div class="modal-header">
      <span class="modal-title">Settings</span>
      <button class="btn btn-ghost btn-icon btn-sm" onclick="closeSettings()">✕</button>
    </div>
    <div class="modal-body">
      <div class="detail-field" style="margin-bottom:16px">
        <div class="detail-field-label">Check interval (seconds)</div>
        <div style="display:flex;gap:8px;align-items:center;margin-top:8px">
          <input type="number" id="auto-delete-check-interval-input" min="1" max="3600" value="30" 
                 style="background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--text);
                        padding:6px 10px;font-size:13px;width:80px;font-family:var(--font)">
          <span style="color:var(--text-dim);font-size:12px">seconds</span>
          <button class="btn btn-secondary btn-sm" onclick="saveCheckInterval()">Save</button>
        </div>
        <div style="margin-top:8px;font-size:11px;color:var(--text-dim)">
          How often to check for offline agents to delete.
        </div>
      </div>
      <div class="detail-field" style="margin-bottom:16px">
        <div class="detail-field-label">Offline threshold (minutes)</div>
        <div style="display:flex;gap:8px;align-items:center;margin-top:8px">
          <input type="number" id="auto-delete-threshold-input" min="1" max="1440" value="60" 
                 style="background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--text);
                        padding:6px 10px;font-size:13px;width:80px;font-family:var(--font)">
          <span style="color:var(--text-dim);font-size:12px">minutes</span>
          <button class="btn btn-secondary btn-sm" onclick="saveThreshold()">Save</button>
        </div>
        <div style="margin-top:8px;font-size:11px;color:var(--text-dim)">
          Agents offline for longer than this duration will be automatically deleted if auto-delete is enabled.
        </div>
      </div>
      <div class="detail-field">
        <div class="detail-field-label">Auto-delete Status</div>
        <div style="margin-top:8px;font-size:13px;color:var(--text-dim)" id="auto-delete-status">
          <span id="auto-delete-status-text">Disabled</span>
        </div>
      </div>
    </div>
    <div class="modal-actions">
      <button class="btn btn-ghost btn-sm" onclick="closeSettings()">Close</button>
    </div>
  </div>
</div>
<!-- ── Confirm Dialog ────────────────────────────────────────────────── -->
<div class="confirm-overlay" id="confirm-overlay">
  <div class="confirm-box">
    <h3 id="confirm-title">Confirm deletion</h3>
    <p id="confirm-msg">Are you sure?</p>
    <div class="confirm-actions">
      <button class="btn btn-danger" id="confirm-yes-btn">Delete</button>
      <button class="btn btn-ghost" onclick="closeConfirm()">Cancel</button>
    </div>
  </div>
</div>
<!-- ── Toast container ───────────────────────────────────────────────── -->
<div id="toast-container"></div>
<script>
// ── State ──────────────────────────────────────────────────────────────────
let _allAgents = [];          // raw API data (always include_offline=true)
let _selectedIds = new Set(); // currently checked rows
let _currentDetailId = null;  // agent shown in detail modal
let _countdownVal = 5;
let _countdownTimer = null;
let _refreshTimer = null;
let _autoDeleteOfflineThreshold = 60 * 60; // 1 hour: agent offline before deletion (seconds)
let _autoDeleteCheckInterval = 30;          // 30 seconds: check interval frequency
let _lastAutoDeleteCheck = 0;               // timestamp of last auto-delete check
// Pagination and sorting state
let _currentPage = 1;         // current page number (1-indexed)
let _pageSize = 20;           // items per page
let _totalPages = 1;          // total pages (from server)
let _totalCount = 0;          // total count (from server)
let _sortBy = 'last_seen';    // current sort field: id, host, status, last_seen, first_seen, current_stage_index
let _sortDesc = true;         // sort descending by default
let _isSearching = false;     // whether user is searching (if true, use client-side filtering)
// ── Toast ──────────────────────────────────────────────────────────────────
function toast(msg, ok = true) {
  const c = document.getElementById('toast-container');
  const t = document.createElement('div');
  t.className = 'toast ' + (ok ? 'toast-ok' : 'toast-err');
  t.textContent = msg;
  c.appendChild(t);
  setTimeout(() => t.remove(), 3500);
}
// ── Fetch agents ───────────────────────────────────────────────────────────
async function fetchAgents() {
  try {
    const params = new URLSearchParams({
      'include_offline': 'true',
      'strip_ansi': 'false',
      'page': _currentPage,
      'page_size': _pageSize,
      'sort_by': _sortBy,
      'sort_desc': _sortDesc ? 'true' : 'false'
    });
    const url = `/api/agents?${params}`;
    console.log(`[API] Fetching: ${url}`);
    const r = await fetch(url);
    if (!r.ok) throw new Error(r.statusText);
    const d = await r.json();
    _allAgents = d.agents || [];
    // Update pagination info from server response
    _currentPage = d.page || 1;
    _totalPages = d.total_pages || 1;
    _totalCount = d.count || 0;
  } catch (e) {
    console.error('Fetch error:', e);
  }
  updateStats();
  checkAutoDeleteOffline();
  renderTable();
}
function updateStats() {
  const online  = _allAgents.filter(a => a.status === 'online').length;
  const offline = _allAgents.filter(a => a.status === 'offline').length;
  document.getElementById('stat-total').textContent   = _totalCount;
  document.getElementById('stat-online').textContent  = online;
  document.getElementById('stat-offline').textContent = offline;
}

async function openCmdApi(agentId) {
  const agent = (_allAgents || []).find((item) => item.id === agentId);
  if (!agent) {
    toast(`Agent ${agentId} not found`, false);
    return;
  }
  const proxy = agent.cmd_api_proxy || '';
  const direct = agent.cmd_api_tcp || '';
  if (proxy) {
    try {
      const resp = await fetch(`${proxy.replace(/\/$/, '')}/api/status`, { method: 'GET' });
      if (resp.ok) {
        window.open(proxy, '_blank');
        return;
      }
    } catch {}
  }
  if (direct) {
    window.open(direct, '_blank');
    return;
  }
  toast('CMD API not running', false);
}
// ── Auto-delete offline agents ─────────────────────────────────────────────
function checkAutoDeleteOffline() {
  const autoDeleteSwitch = document.getElementById('auto-delete-offline');
  if (!autoDeleteSwitch || !autoDeleteSwitch.checked) return;
  const now = Math.floor(Date.now() / 1000);
  // Only check at specified interval to avoid too frequent API calls
  if (now - _lastAutoDeleteCheck < _autoDeleteCheckInterval) return;
  _lastAutoDeleteCheck = now;
  // Find offline agents that have been offline for more than threshold
  const toDelete = _allAgents.filter(a => {
    if (a.status !== 'offline') return false;
    const lastSeen = a.last_seen || 0;
    const offlineTime = now - lastSeen;
    return offlineTime > _autoDeleteOfflineThreshold;
  });
  if (toDelete.length === 0) return;
  // Auto-delete them silently (no confirmation, just log)
  const ids = toDelete.map(a => a.id);
  console.log(`[Auto-delete] Removing ${ids.length} offline agent(s):`, ids);
  batchDeleteSilent(ids);
}
async function batchDeleteSilent(ids) {
  let ok = 0, fail = 0;
  for (const id of ids) {
    try {
      const r = await fetch(`/api/agent/${encodeURIComponent(id)}`, { method: 'DELETE' });
      if (r.ok) { ok++; _allAgents = _allAgents.filter(a => a.id !== id); _selectedIds.delete(id); }
      else fail++;
    } catch { fail++; }
  }
  if (ok > 0) {
    console.log(`[Auto-delete] Deleted ${ok} agent(s)${fail ? `, ${fail} failed` : ''}`);
    updateStats(); 
    renderTable();
  }
}
// ── Filtering & sorting ───────────────────────────────────────────────────
function getVisible() {
  const q      = document.getElementById('search-box').value.trim().toLowerCase();
  const filter = document.getElementById('filter-status').value;
  const showOff = document.getElementById('show-offline').checked;
  // If user is searching, we need to filter the current data
  // Note: when searching, _allAgents contains the full result set from a full fetch
  if (q) {
    _isSearching = true;
    let result = _allAgents.filter(a => {
      if (!showOff && a.status === 'offline') return false;
      if (filter === 'online'  && a.status !== 'online')  return false;
      if (filter === 'offline' && a.status !== 'offline') return false;
      const hay = `${a.id} ${a.host} ${a.mission || ''} ${a.current_stage_name || ''}`.toLowerCase();
      if (!hay.includes(q)) return false;
      return true;
    });
    // Apply sorting (server already sorted, but search results need re-sorting)
    result.sort((a, b) => {
      let aVal, bVal;
      switch (_sortBy) {
        case 'id':
        case 'host':
          aVal = a[_sortBy];
          bVal = b[_sortBy];
          break;
        case 'status':
          aVal = a.status === 'online' ? 0 : 1;
          bVal = b.status === 'online' ? 0 : 1;
          break;
        case 'last_seen':
        case 'first_seen':
        case 'current_stage_index':
          aVal = a[_sortBy];
          bVal = b[_sortBy];
          break;
        default:
          return 0;
      }
      if (aVal < bVal) return _sortDesc ? 1 : -1;
      if (aVal > bVal) return _sortDesc ? -1 : 1;
      return 0;
    });
    return result;
  } else {
    _isSearching = false;
    // No search: return server-paginated results (already sorted by backend)
    // But still apply status filter since backend returns all statuses
    return _allAgents.filter(a => {
      if (!showOff && a.status === 'offline') return false;
      if (filter === 'online'  && a.status !== 'online')  return false;
      if (filter === 'offline' && a.status !== 'offline') return false;
      return true;
    });
  }
}
// ── Render table ───────────────────────────────────────────────────────────
function renderTable() {
  const tbody   = document.getElementById('agents-tbody');
  const visible = getVisible();
  // Determine if we need to do pagination (only when searching)
  let pageItems = visible;
  let totalPages = 1;
  let totalFilteredCount = visible.length;
  if (_isSearching) {
    // When searching: do client-side pagination on filtered results
    totalPages = Math.max(1, Math.ceil(visible.length / _pageSize));
    if (_currentPage > totalPages) _currentPage = totalPages;
    if (_currentPage < 1) _currentPage = 1;
    const startIdx = (_currentPage - 1) * _pageSize;
    const endIdx = Math.min(startIdx + _pageSize, visible.length);
    pageItems = visible.slice(startIdx, endIdx);
  } else {
    // When not searching: use server pagination info
    totalPages = _totalPages;
    totalFilteredCount = _totalCount;
  }
  // clean up selected ids that are no longer visible
  const visibleIds = new Set(visible.map(a => a.id));
  for (const id of [..._selectedIds]) { if (!visibleIds.has(id)) _selectedIds.delete(id); }
  updateBatchBtn();
  if (pageItems.length === 0) {
      tbody.innerHTML = '<tr class="empty-row"><td colspan="14"><div style="display:flex;flex-direction:column;align-items:center;gap:12px"><div>No agents found.</div><a href="/launch/" class="btn btn-primary" style="text-decoration:none;font-size:24px;padding:6px 18px" title="Launch a new managed task">＋</a></div></td></tr>';
    syncHeaderCheckbox();
    updatePaginationControls(_currentPage, totalPages, totalFilteredCount);
    return;
  }
  tbody.innerHTML = pageItems.map(a => {
    const st     = a.status === 'online';
    const cur    = a.current_stage_index >= 0 ? a.current_stage_index : -1;
    const tot    = a.total_stage_count    || 0;
    const pct    = tot > 0 ? Math.min(Math.round(((cur + 1) / tot) * 100), 100) : 0;
    const progLabel = tot > 0 ? `${cur >= 0 ? cur : '?'}/${tot}` : '—';
    const done   = a.is_mission_complete;
    const checked = _selectedIds.has(a.id) ? 'checked' : '';
    const rowClass = st ? '' : 'style="opacity:.6"';
    return `<tr ${rowClass}>
      <td><input type="checkbox" class="row-cb" data-id="${esc(a.id)}" ${checked} onchange="onRowCheckbox(this)"/></td>
      <td><span class="id-cell" title="${esc(a.id)}">${esc(a.id)}</span></td>
      <td><span class="host-cell" title="${esc(a.host)}">${esc(a.host || '—')}</span></td>
      <td><span class="badge ${st ? 'badge-online' : 'badge-offline'}">${esc(a.status)}</span></td>
      <td>${a.launch ? '<span class="badge badge-yes">yes</span>' : '<span style="color:var(--text-dim);font-size:12px">—</span>'}</td>
      <td><span class="version-cell" title="${esc(a.version || '—')}">${esc(a.version || '—')}</span></td>
      <td>
        ${tot > 0
          ? `<div class="progress-bar-wrap"><div class="progress-bar" style="width:${pct}%;background:${done?'var(--online)':'var(--accent)'}"></div></div>
             <div class="progress-label">${progLabel}${done ? ' ✓' : ''}</div>`
          : '<span style="color:var(--text-dim)">—</span>'}
      </td>
      <td style="font-size:12px;white-space:nowrap;color:var(--text-dim)">${esc(a.run_time || '—')}</td>
      <td><span class="stage-cell" title="${esc(a.current_stage_name || '')}">${esc(a.current_stage_name || '—')}</span></td>
      <td>${a.mcp_running ? '<span class="badge badge-yes">on</span>' : '<span style="color:var(--text-dim);font-size:12px">—</span>'}</td>
      <td>${a.is_break    ? '<span class="badge badge-break">yes</span>' : '<span style="color:var(--text-dim);font-size:12px">—</span>'}</td>
      <td><span class="cmd-cell" title="${esc(a.last_cmd || '')}">${esc(a.last_cmd || '—')}</span></td>
      <td><span class="mission-cell" title="${esc(a.mission || '')}">${esc(a.mission || '—')}</span></td>
      <td style="white-space:nowrap;display:flex;gap:4px">
        <button class="btn btn-ghost btn-sm" onclick="openDetail('${esc(a.id)}')">👁 Detail</button>
        ${(()=> {
          const proxy = a.cmd_api_proxy || '';
          const direct = a.cmd_api_tcp || '';
          if (!proxy && !direct) {
            return '<button class="btn btn-ghost btn-sm" disabled title="CMD API not running" style="opacity:.35;cursor:not-allowed">📡 API</button>';
          }
          return `<button class="btn btn-secondary btn-sm" type="button" onclick="openCmdApi('${esc(a.id)}')" style="text-decoration:none" title="Open CMD API">📡 API</button>`;
        })()}
        <button class="btn btn-danger btn-sm" onclick="confirmDelete('${esc(a.id)}')">🗑</button>
      </td>
    </tr>`;
  }).join('');
  syncHeaderCheckbox();
  updatePaginationControls(_currentPage, totalPages, totalFilteredCount);
}
function updatePaginationControls(currentPage, totalPages, totalCount) {
  const prevBtn = document.getElementById('page-prev');
  const nextBtn = document.getElementById('page-next');
  const pageNumSpan = document.getElementById('page-num');
  const paginationInfo = document.getElementById('pagination-info');
  prevBtn.disabled = currentPage <= 1;
  nextBtn.disabled = currentPage >= totalPages;
  pageNumSpan.textContent = `${currentPage}/${totalPages}`;
  const currentPageSize = _isSearching && getVisible().length > 0 
    ? Math.min(_pageSize, getVisible().length - (currentPage - 1) * _pageSize)
    : _pageSize;
  const startIdx = (currentPage - 1) * _pageSize + 1;
  const endIdx = Math.min(currentPage * _pageSize, totalCount);
  paginationInfo.textContent = totalCount > 0 ? `${startIdx}-${endIdx} of ${totalCount}` : '—';
}
function prevPage() {
  if (_currentPage > 1) {
    _currentPage--;
    localStorage.setItem('master-current-page', String(_currentPage));
    renderTable();
  }
}
function nextPage() {
  const visible = getVisible();
  const pages = _isSearching ? Math.ceil(visible.length / _pageSize) : _totalPages;
  if (_currentPage < pages) {
    _currentPage++;
    localStorage.setItem('master-current-page', String(_currentPage));
    renderTable();
  }
}
function onPageSizeChange() {
  const select = document.getElementById('page-size-select');
  _pageSize = parseInt(select.value);
  _currentPage = 1;
  localStorage.setItem('master-page-size', String(_pageSize));
  localStorage.setItem('master-current-page', '1');
  renderTable();
}
function onSortChange() {
  const select = document.getElementById('sort-by');
  _sortBy = select.value;
  _currentPage = 1; // reset to first page when sorting changes
  localStorage.setItem('master-sort-by', _sortBy);
  localStorage.setItem('master-current-page', '1');
  // If searching, renderTable will re-sort the filtered results
  // If not searching, need to refetch from server with new sort
  if (!_isSearching) {
    fetchAgents(); // This will use the new _sortBy
  } else {
    renderTable();
  }
}
function toggleSortOrder() {
  _sortDesc = !_sortDesc;
  _currentPage = 1;
  localStorage.setItem('master-sort-desc', _sortDesc ? '1' : '0');
  localStorage.setItem('master-current-page', '1');
  updateSortOrderButton();
  console.log(`[Sort] Changed sort direction to: ${_sortDesc ? 'DESC' : 'ASC'}, isSearching=${_isSearching}`);
  // If searching, renderTable will re-sort the filtered results
  // If not searching, need to refetch from server with new sort order
  if (!_isSearching) {
    console.log(`[Sort] Fetching agents with sort_desc=${_sortDesc}`);
    fetchAgents(); // This will use the new _sortDesc
  } else {
    console.log(`[Sort] Re-rendering table with new sort order`);
    renderTable();
  }
}
function updateSortOrderButton() {
  const btn = document.getElementById('sort-order-btn');
  btn.textContent = _sortDesc ? '↓ Desc' : '↑ Asc';
}
function onSearchChange() {
  const searchBox = document.getElementById('search-box');
  const q = searchBox.value.trim().toLowerCase();
  _currentPage = 1;
  localStorage.setItem('master-current-page', '1');
  // If user is now searching and wasn't before, need to fetch all data for client-side filtering
  if (q && !_isSearching) {
    _isSearching = true;
    // Fetch all data (large page_size) for client-side search filtering
    const params = new URLSearchParams({
      'include_offline': 'true',
      'strip_ansi': 'false',
      'page': 1,
      'page_size': 10000,  // Get many results for searching
      'sort_by': _sortBy,
      'sort_desc': _sortDesc ? 'true' : 'false'
    });
    fetch(`/api/agents?${params}`)
      .then(r => r.json())
      .then(d => {
        _allAgents = d.agents || [];
        renderTable();
      })
      .catch(e => console.error('Search fetch error:', e));
  } else if (!q && _isSearching) {
    _isSearching = false;
    // User cleared search: go back to normal pagination from server
    fetchAgents();
  } else {
    // Searching continues or no search: just re-render
    renderTable();
  }
}
// ── Select / batch ─────────────────────────────────────────────────────────
function onRowCheckbox(cb) {
  const id = cb.dataset.id;
  if (cb.checked) _selectedIds.add(id);
  else _selectedIds.delete(id);
  updateBatchBtn();
  syncHeaderCheckbox();
}
function onHeaderCheckboxChange() {
  const hc = document.getElementById('header-checkbox');
  getVisible().forEach(a => { if (hc.checked) _selectedIds.add(a.id); else _selectedIds.delete(a.id); });
  renderTable();
}
function toggleSelectAll() {
  const vis = getVisible();
  const allSel = vis.every(a => _selectedIds.has(a.id));
  vis.forEach(a => { if (allSel) _selectedIds.delete(a.id); else _selectedIds.add(a.id); });
  renderTable();
}
function syncHeaderCheckbox() {
  const vis = getVisible();
  const hc  = document.getElementById('header-checkbox');
  hc.checked       = vis.length > 0 && vis.every(a => _selectedIds.has(a.id));
  hc.indeterminate = !hc.checked && vis.some(a => _selectedIds.has(a.id));
}
function updateBatchBtn() {
  const n   = _selectedIds.size;
  const btn = document.getElementById('batch-delete-btn');
  btn.disabled = n === 0;
  document.getElementById('selected-count').textContent = n;
}
// ── ANSI → HTML ───────────────────────────────────────────────────────────
const _ANSI_COLORS = {
  '30':'#6b7280','31':'#ef4444','32':'#22c55e','33':'#eab308',
  '34':'#3b82f6','35':'#a855f7','36':'#06b6d4','37':'#d1d5db',
  '90':'#9ca3af','91':'#f87171','92':'#4ade80','93':'#fbbf24',
  '94':'#60a5fa','95':'#c084fc','96':'#22d3ee','97':'#f9fafb',
};
function ansiToHtml(text) {
  if (!text) return '';
  // HTML-escape first (ANSI bytes are never &<>)
  const safe = text
    .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  // Split on ESC[ ... m sequences
  const parts = safe.split(/\x1b\[([0-9;]*)m/);
  let html = '';
  let spanOpen = false;
  for (let i = 0; i < parts.length; i++) {
    if (i % 2 === 0) {
      html += parts[i];
    } else {
      if (spanOpen) { html += '</span>'; spanOpen = false; }
      const codes = parts[i] ? parts[i].split(';') : ['0'];
      if (!codes.includes('0')) {
        for (const c of codes) {
          const col = _ANSI_COLORS[c];
          if (col) { html += `<span style="color:${col}">`;  spanOpen = true; break; }
        }
      }
    }
  }
  if (spanOpen) html += '</span>';
  return html;
}
// ── Detail modal ───────────────────────────────────────────────────────────
function openDetail(id) {
  const a = _allAgents.find(x => x.id === id);
  if (!a) return;
  _currentDetailId = id;
  document.getElementById('detail-modal-title').textContent = `Agent: ${id}`;
  const fields = [
    ['ID',           a.id],
    ['Host',         a.host],
    ['Status',       a.status],
    ['Version',      a.version || '—'],
    ['Mission',      a.mission || '—'],
    ['Stage',        a.current_stage_name || '—'],
    ['Progress',     a.total_stage_count > 0 ? `${a.current_stage_index}/${a.total_stage_count}` : '—'],
    ['Run Time',     a.run_time || '—'],
    ['Complete',     a.is_mission_complete ? 'Yes ✓' : 'No'],
    ['MCP Running',  a.mcp_running ? 'Yes' : 'No'],
    ['Break',        a.is_break    ? 'Yes' : 'No'],
    ['Last CMD',     a.last_cmd || '—'],
    ['CMD API Sock', a.cmd_api_sock || '—'],
    ['First Seen',   a.first_seen  ? new Date(a.first_seen  * 1000).toLocaleString() : '—'],
    ['Last Seen',    a.last_seen   ? new Date(a.last_seen   * 1000).toLocaleString() : '—'],
  ];
  const cmdApiTarget = a.cmd_api_proxy || a.cmd_api_tcp || '';
  const cmdApiHtml = cmdApiTarget
    ? `<div class="detail-field"><div class="detail-field-label">CMD API</div><div class="detail-field-value"><a href="${esc(cmdApiTarget)}" target="_blank" style="color:var(--accent)" onclick="event.preventDefault();openCmdApi('${esc(a.id)}')">📡 ${esc(cmdApiTarget)}</a></div></div>`
    : `<div class="detail-field"><div class="detail-field-label">CMD API</div><div class="detail-field-value" style="color:var(--text-dim)">📡 —</div></div>`;
  const grid = fields.map(([k, v]) =>
    `<div class="detail-field"><div class="detail-field-label">${esc(k)}</div><div class="detail-field-value">${esc(String(v))}</div></div>`
  ).join('') + cmdApiHtml;
  // ── Stage timeline (ANSI-rendered mission info) ────────────────────────
  const stageHtml = buildMissionInfo(a);
  document.getElementById('detail-modal-body').innerHTML =
    `<div class="detail-grid">${grid}</div>
     ${stageHtml}
     <details class="json-collapse">
       <summary>Raw JSON</summary>
       <pre class="detail-json">${esc(JSON.stringify(a, null, 2))}</pre>
     </details>`;
  document.getElementById('detail-modal').classList.add('open');
}
function buildMissionInfo(a) {
  const mi = a.mission_info_ansi || '';
  const cur  = a.current_stage_index >= 0 ? a.current_stage_index : -1;
  const tot  = a.total_stage_count  || 0;
  const pct  = tot > 0 ? Math.min(Math.round(((cur + 1) / tot) * 100), 100) : 0;
  const done = a.is_mission_complete;
  const bar = tot > 0 ? `
    <div style="margin-bottom:10px">
      <div style="display:flex;justify-content:space-between;align-items:center;
                  margin-bottom:5px;font-size:12px">
        <span style="color:var(--text-dim)">${esc(a.mission || '')}</span>
        <span style="color:var(--text-dim)">
          ${cur >= 0 ? cur : '?'}/${tot} stages${done
            ? ' &nbsp;<span style="color:var(--online)">&#x2713; Complete</span>' : ''}
        </span>
      </div>
      <div class="progress-bar-wrap" style="height:8px">
        <div class="progress-bar" style="width:${pct}%;background:${
          done ? 'var(--online)' : 'var(--accent)'}"></div>
      </div>
    </div>` : '';
  if (!mi) return bar ? `<div class="section-label">Task Progress</div>${bar}` : '';
  const rendered = ansiToHtml(mi);
  return `<div class="section-label">Task Progress</div>
    ${bar}
    <pre id="detail-mission-pre" style="background:var(--bg);border:1px solid var(--border);border-radius:6px;
                padding:12px 14px;font-size:13px;font-family:'Consolas','Courier New',monospace;
                white-space:pre-wrap;word-break:break-word;max-height:360px;overflow-y:auto;
                line-height:1.6;margin:0">${rendered}</pre>`;
}
async function refreshDetail() {
  if (!_currentDetailId) return;
  const btn = document.getElementById('detail-refresh-btn');
  const savedId = _currentDetailId;
  if (btn) { btn.disabled = true; btn.textContent = '↻ …'; }
  try {
    const body = document.getElementById('detail-modal-body');
    const scrollTop    = body ? body.scrollTop : 0;
    const pre          = document.getElementById('detail-mission-pre');
    const preScrollTop = pre  ? pre.scrollTop  : 0;
    await fetchAgents();
    if (savedId === _currentDetailId) {
      openDetail(savedId);
      requestAnimationFrame(() => requestAnimationFrame(() => {
        const b2 = document.getElementById('detail-modal-body');
        if (b2) b2.scrollTop = scrollTop;
        const p2 = document.getElementById('detail-mission-pre');
        if (p2) p2.scrollTop = preScrollTop;
      }));
    }
  } catch(e) { console.warn('refreshDetail failed', e); }
  finally {
    const b = document.getElementById('detail-refresh-btn');
    if (b) { b.disabled = false; b.textContent = '↻ Refresh'; }
  }
}
function closeDetailModal() {
  document.getElementById('detail-modal').classList.remove('open');
  _currentDetailId = null;
}
function confirmDeleteFromDetail() {
  if (_currentDetailId) { closeDetailModal(); confirmDelete(_currentDetailId); }
}
// ── Settings modal ─────────────────────────────────────────────────────────
function openSettings() {
  const intervalInput = document.getElementById('auto-delete-check-interval-input');
  const thresholdInput = document.getElementById('auto-delete-threshold-input');
  const savedInterval = localStorage.getItem('master-auto-delete-check-interval');
  if (savedInterval) {
    intervalInput.value = parseInt(savedInterval);
  } else {
    intervalInput.value = 30; // default 30 seconds
  }
  const savedThreshold = localStorage.getItem('master-auto-delete-threshold');
  if (savedThreshold) {
    thresholdInput.value = Math.floor(parseInt(savedThreshold) / 60);
  } else {
    thresholdInput.value = 60; // default 1 hour
  }
  updateAutoDeleteStatus();
  document.getElementById('settings-modal').classList.add('open');
}
function closeSettings() {
  document.getElementById('settings-modal').classList.remove('open');
}
function saveCheckInterval() {
  const input = document.getElementById('auto-delete-check-interval-input');
  const seconds = parseInt(input.value);
  if (isNaN(seconds) || seconds < 1 || seconds > 3600) {
    toast('Invalid value. Please enter a number between 1 and 3600.', false);
    return;
  }
  _autoDeleteCheckInterval = seconds;
  localStorage.setItem('master-auto-delete-check-interval', String(seconds));
  toast(`Check interval set to ${seconds} second(s).`, true);
  updateAutoDeleteStatus();
}
function saveThreshold() {
  const input = document.getElementById('auto-delete-threshold-input');
  const minutes = parseInt(input.value);
  if (isNaN(minutes) || minutes < 1 || minutes > 1440) {
    toast('Invalid value. Please enter a number between 1 and 1440.', false);
    return;
  }
  _autoDeleteOfflineThreshold = minutes * 60; // convert to seconds
  localStorage.setItem('master-auto-delete-threshold', String(_autoDeleteOfflineThreshold));
  toast(`Offline threshold set to ${minutes} minute(s).`, true);
  updateAutoDeleteStatus();
}
function updateAutoDeleteStatus() {
  const autoDeleteSwitch = document.getElementById('auto-delete-offline');
  const statusText = document.getElementById('auto-delete-status-text');
  const intervalSec = _autoDeleteCheckInterval;
  const thresholdMin = Math.floor(_autoDeleteOfflineThreshold / 60);
  if (autoDeleteSwitch.checked) {
    statusText.textContent = `Enabled – Check every ${intervalSec}s, delete agents offline for ${thresholdMin}min`;
  } else {
    statusText.textContent = `Disabled – Check every ${intervalSec}s, threshold ${thresholdMin}min`;
  }
}
// ── Confirm dialog ─────────────────────────────────────────────────────────
let _confirmCallback = null;
function showConfirm(title, msg, onYes) {
  document.getElementById('confirm-title').textContent = title;
  document.getElementById('confirm-msg').textContent   = msg;
  _confirmCallback = onYes;
  document.getElementById('confirm-overlay').classList.add('open');
}
function closeConfirm() {
  document.getElementById('confirm-overlay').classList.remove('open');
  _confirmCallback = null;
}
document.getElementById('confirm-yes-btn').addEventListener('click', () => {
  if (_confirmCallback) { const cb = _confirmCallback; closeConfirm(); cb(); }
});
function confirmDelete(id) {
  showConfirm('Delete agent', `Remove agent "${id}" from the master?`, () => deleteAgent(id));
}
function confirmBatchDelete() {
  const ids = [..._selectedIds];
  if (ids.length === 0) return;
  showConfirm('Batch delete', `Delete ${ids.length} selected agent(s)?`, () => batchDelete(ids));
}
// ── Delete API ─────────────────────────────────────────────────────────────
async function deleteAgent(id) {
  try {
    const r = await fetch(`/api/agent/${encodeURIComponent(id)}`, { method: 'DELETE' });
    const d = await r.json();
    if (r.ok) {
      toast(`Deleted: ${id}`);
      _allAgents = _allAgents.filter(a => a.id !== id);
      _selectedIds.delete(id);
      updateStats(); renderTable();
    } else {
      toast(`Error: ${d.detail || r.statusText}`, false);
    }
  } catch (e) { toast(`Error: ${e}`, false); }
}
async function batchDelete(ids) {
  let ok = 0, fail = 0;
  for (const id of ids) {
    try {
      const r = await fetch(`/api/agent/${encodeURIComponent(id)}`, { method: 'DELETE' });
      if (r.ok) { ok++; _allAgents = _allAgents.filter(a => a.id !== id); _selectedIds.delete(id); }
      else fail++;
    } catch { fail++; }
  }
  toast(`Deleted ${ok} agent(s)${fail ? `, ${fail} failed` : ''}.`, fail === 0);
  updateStats(); renderTable();
}
// ── Countdown + auto-refresh ───────────────────────────────────────────────
function startCountdown() {
  clearInterval(_countdownTimer);
  clearTimeout(_refreshTimer);
  _countdownVal = 5;
  document.getElementById('countdown').textContent = _countdownVal;
  _countdownTimer = setInterval(() => {
    _countdownVal--;
    document.getElementById('countdown').textContent = _countdownVal;
    if (_countdownVal <= 0) { clearInterval(_countdownTimer); fetchAgents().then(startCountdown); }
  }, 1000);
}
function refreshNow() { clearInterval(_countdownTimer); fetchAgents().then(startCountdown); }
// ── Show-offline toggle ────────────────────────────────────────────────────
document.getElementById('show-offline').addEventListener('change', renderTable);
// ── Auto-delete toggle ─────────────────────────────────────────────────
document.getElementById('auto-delete-offline').addEventListener('change', e => {
  const checked = e.target.checked;
  localStorage.setItem('master-auto-delete-offline', checked ? '1' : '0');
  if (checked) {
    const intervalSec = _autoDeleteCheckInterval;
    const thresholdMin = Math.floor(_autoDeleteOfflineThreshold / 60);
    toast(`Auto-delete enabled. Check every ${intervalSec}s, delete after ${thresholdMin}min offline.`, true);
    _lastAutoDeleteCheck = 0; // reset timer to check immediately
    checkAutoDeleteOffline();
  } else {
    toast('Auto-delete disabled.', true);
  }
});
// ── Helper ────────────────────────────────────────────────────────────────
function esc(str) {
  return String(str || '')
    .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
    .replace(/"/g,'&quot;').replace(/'/g,'&#39;');
}
// ── Close modals on overlay click ─────────────────────────────────────────
document.getElementById('detail-modal').addEventListener('click', e => {
  if (e.target === e.currentTarget) closeDetailModal();
});
document.getElementById('confirm-overlay').addEventListener('click', e => {
  if (e.target === e.currentTarget) closeConfirm();
});
document.getElementById('settings-modal').addEventListener('click', e => {
  if (e.target === e.currentTarget) closeSettings();
});
// keyboard ESC
document.addEventListener('keydown', e => {
  if (e.key === 'Escape') { closeDetailModal(); closeConfirm(); closeSettings(); }
});
// ── Boot ───────────────────────────────────────────────────────────────────
// Restore pagination and sorting settings from localStorage
const savedPageSize = localStorage.getItem('master-page-size');
if (savedPageSize) {
  _pageSize = parseInt(savedPageSize);
  document.getElementById('page-size-select').value = _pageSize;
}
const savedSortBy = localStorage.getItem('master-sort-by');
if (savedSortBy) {
  _sortBy = savedSortBy;
  document.getElementById('sort-by').value = _sortBy;
}
const savedSortDesc = localStorage.getItem('master-sort-desc');
if (savedSortDesc) {
  _sortDesc = savedSortDesc === '1';
}
updateSortOrderButton();
const savedPage = localStorage.getItem('master-current-page');
if (savedPage) {
  _currentPage = parseInt(savedPage);
}
// Restore auto-delete check interval from localStorage
const savedInterval = localStorage.getItem('master-auto-delete-check-interval');
if (savedInterval) {
  _autoDeleteCheckInterval = parseInt(savedInterval);
}
// Restore auto-delete offline threshold from localStorage
const savedThreshold = localStorage.getItem('master-auto-delete-threshold');
if (savedThreshold) {
  _autoDeleteOfflineThreshold = parseInt(savedThreshold);
}
// Restore auto-delete setting from localStorage
const savedAutoDelete = localStorage.getItem('master-auto-delete-offline');
if (savedAutoDelete === '1') {
  document.getElementById('auto-delete-offline').checked = true;
}
fetchAgents().then(startCountdown);
</script>
<script src="/static/share/ucagent-shell.js"></script>
<script>
window.UCAgentShell.init({actionsSelector:'#uc-shell-header-actions',metaUrl:'/api/ui-meta'});
</script>
</body>
</html>
