| | @@ -457,10 +457,11 @@ |
| 457 | 457 | <option value="revoked">revoked</option> |
| 458 | 458 | </select> |
| 459 | 459 | <div class="spacer"></div> |
| 460 | 460 | <span class="badge" id="agent-count" style="margin-right:4px">0</span> |
| 461 | 461 | <button class="sm" onclick="loadAgents()">↻ refresh</button> |
| 462 | + <button class="sm danger" id="bulk-delete-btn" style="display:none" onclick="bulkDeleteAgents()">delete selected</button> |
| 462 | 463 | <button class="sm primary" onclick="openDrawer()">+ register agent</button> |
| 463 | 464 | </div> |
| 464 | 465 | <div id="agent-pagination" style="display:none;padding:4px 16px;font-size:12px;color:#8b949e;display:flex;align-items:center;gap:8px"> |
| 465 | 466 | <button class="sm" id="agent-prev" onclick="agentPage--;renderAgentTable()">← prev</button> |
| 466 | 467 | <span id="agent-page-info"></span> |
| | @@ -1677,11 +1678,11 @@ |
| 1677 | 1678 | const chs = (a.config?.channels||[]).map(c=>`<span class="tag ch">${esc(c)}</span>`).join(''); |
| 1678 | 1679 | const rev = a.revoked ? '<span class="tag revoked">revoked</span>' : ''; |
| 1679 | 1680 | const seen = a.last_seen ? relTime(a.last_seen) : 'never'; |
| 1680 | 1681 | const seenStyle = a.online ? 'color:#3fb950' : 'color:#8b949e'; |
| 1681 | 1682 | return `<tr${a.revoked?' style="opacity:0.5"':''}> |
| 1682 | | - <td>${presenceDot(a)} <strong>${esc(a.nick)}</strong></td> |
| 1683 | + <td><input type="checkbox" class="agent-select" value="${esc(a.nick)}" onchange="updateBulkBtn()" style="margin-right:6px">${presenceDot(a)} <strong>${esc(a.nick)}</strong></td> |
| 1683 | 1684 | <td><span class="tag type-${a.type}">${esc(a.type)}</span>${rev}</td> |
| 1684 | 1685 | <td>${chs||'<span style="color:#8b949e">—</span>'}</td> |
| 1685 | 1686 | <td style="white-space:nowrap;${seenStyle}">${seen}</td> |
| 1686 | 1687 | <td><div class="actions">${!a.revoked?` |
| 1687 | 1688 | <button class="sm" onclick="rotateAgent('${esc(a.nick)}')">rotate</button> |
| | @@ -1702,10 +1703,27 @@ |
| 1702 | 1703 | async function deleteAgent(nick) { |
| 1703 | 1704 | if (!confirm(`Delete "${nick}"? This permanently removes the agent from the registry.`)) return; |
| 1704 | 1705 | try { await api('DELETE', `/v1/agents/${nick}`); await loadAgents(); await loadStatus(); } |
| 1705 | 1706 | catch(e) { alert('Delete failed: '+e.message); } |
| 1706 | 1707 | } |
| 1708 | +function updateBulkBtn() { |
| 1709 | + const checked = document.querySelectorAll('.agent-select:checked'); |
| 1710 | + const btn = document.getElementById('bulk-delete-btn'); |
| 1711 | + btn.style.display = checked.length > 0 ? '' : 'none'; |
| 1712 | + btn.textContent = `delete selected (${checked.length})`; |
| 1713 | +} |
| 1714 | +async function bulkDeleteAgents() { |
| 1715 | + const nicks = [...document.querySelectorAll('.agent-select:checked')].map(cb => cb.value); |
| 1716 | + if (!nicks.length) return; |
| 1717 | + if (!confirm(`Delete ${nicks.length} agent(s)? This permanently removes them from the registry.\n\n${nicks.join(', ')}`)) return; |
| 1718 | + try { |
| 1719 | + const result = await api('POST', '/v1/agents/bulk-delete', {nicks}); |
| 1720 | + await loadAgents(); |
| 1721 | + await loadStatus(); |
| 1722 | + if (result.failed > 0) alert(`Deleted ${result.deleted}, failed ${result.failed}`); |
| 1723 | + } catch(e) { alert('Bulk delete failed: ' + e.message); } |
| 1724 | +} |
| 1707 | 1725 | async function rotateAgent(nick) { |
| 1708 | 1726 | try { |
| 1709 | 1727 | const creds = await api('POST', `/v1/agents/${nick}/rotate`); |
| 1710 | 1728 | // Show result in whichever drawer is relevant. |
| 1711 | 1729 | showCredentials(nick, creds, null, 'rotate'); |
| 1712 | 1730 | |