Add IP range filter button (.150–.155)
New "Filter .150–.155" button fetches all connected clients via GET /api/all-clients (one request per site, grouped by AP MAC), then hides any AP row that has no client with a last-octet IP between 150 and 155. Clicking again clears the filter. The name/site search and IP filter compose (AND logic) via a shared applyFilters() function. Client data is cached in-memory for the current page session so repeated toggles don't re-fetch. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,14 @@
|
||||
</svg>
|
||||
Refresh
|
||||
</button>
|
||||
<button id="btn-ip-filter"
|
||||
class="flex items-center gap-1.5 px-3 py-2 text-sm rounded-lg bg-white hover:bg-gray-50 border border-gray-300 text-gray-600 transition-colors">
|
||||
<svg id="ip-filter-icon" class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2a1 1 0 01-.293.707L13 13.414V19a1 1 0 01-.553.894l-4 2A1 1 0 017 21v-7.586L3.293 6.707A1 1 0 013 6V4z"/>
|
||||
</svg>
|
||||
<span id="ip-filter-label">Filter .150–.155</span>
|
||||
</button>
|
||||
<button id="btn-reboot-selected" disabled
|
||||
class="flex items-center gap-1.5 px-3 py-2 text-sm rounded-lg bg-red-600 hover:bg-red-500 disabled:opacity-40 disabled:cursor-not-allowed border border-red-500 text-white transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -208,23 +216,103 @@
|
||||
<script>
|
||||
const CSRF_TOKEN = {{ csrf_token | tojson }};
|
||||
|
||||
// ── Name / site filter ────────────────────────────────────────────────────
|
||||
// ── Filters (name/site + IP range) ────────────────────────────────────────
|
||||
const filterInput = document.getElementById('filter-name');
|
||||
const visibleCount = document.getElementById('visible-count');
|
||||
const allRows = [...document.querySelectorAll('#ap-table-body tr')];
|
||||
|
||||
filterInput.addEventListener('input', () => {
|
||||
let ipMatchingMacs = null; // null = inactive; Set<string> = active
|
||||
|
||||
function normMac(s) { return (s || '').toUpperCase().replace(/-/g, ':'); }
|
||||
|
||||
function applyFilters() {
|
||||
const q = filterInput.value.toLowerCase().trim();
|
||||
let shown = 0;
|
||||
allRows.forEach(row => {
|
||||
const name = (row.querySelector('td:nth-child(2)')?.textContent || '').toLowerCase();
|
||||
const site = (row.querySelector('td:nth-child(3)')?.textContent || '').toLowerCase();
|
||||
const match = !q || name.includes(q) || site.includes(q);
|
||||
row.style.display = match ? '' : 'none';
|
||||
if (match) shown++;
|
||||
const nameMatch = !q || name.includes(q) || site.includes(q);
|
||||
|
||||
let ipMatch = true;
|
||||
if (ipMatchingMacs !== null) {
|
||||
const cb = row.querySelector('.ap-checkbox');
|
||||
ipMatch = ipMatchingMacs.has(normMac(cb?.dataset.mac));
|
||||
}
|
||||
|
||||
const visible = nameMatch && ipMatch;
|
||||
row.style.display = visible ? '' : 'none';
|
||||
if (visible) shown++;
|
||||
});
|
||||
visibleCount.textContent = shown;
|
||||
updateBulkButton();
|
||||
}
|
||||
|
||||
filterInput.addEventListener('input', applyFilters);
|
||||
|
||||
// ── IP range filter button ─────────────────────────────────────────────────
|
||||
const btnIpFilter = document.getElementById('btn-ip-filter');
|
||||
const ipFilterLabel = document.getElementById('ip-filter-label');
|
||||
const ipFilterIcon = document.getElementById('ip-filter-icon');
|
||||
let cachedApClients = null;
|
||||
|
||||
function isTargetIp(ip) {
|
||||
if (!ip) return false;
|
||||
const parts = ip.split('.');
|
||||
if (parts.length !== 4) return false;
|
||||
const last = parseInt(parts[3], 10);
|
||||
return last >= 150 && last <= 155;
|
||||
}
|
||||
|
||||
function setIpFilterActive(active) {
|
||||
if (active) {
|
||||
btnIpFilter.classList.replace('bg-white', 'bg-blue-50');
|
||||
btnIpFilter.classList.replace('hover:bg-gray-50', 'hover:bg-blue-100');
|
||||
btnIpFilter.classList.replace('border-gray-300', 'border-blue-400');
|
||||
btnIpFilter.classList.replace('text-gray-600', 'text-blue-700');
|
||||
ipFilterLabel.textContent = '✕ Clear filter';
|
||||
} else {
|
||||
btnIpFilter.classList.replace('bg-blue-50', 'bg-white');
|
||||
btnIpFilter.classList.replace('hover:bg-blue-100', 'hover:bg-gray-50');
|
||||
btnIpFilter.classList.replace('border-blue-400', 'border-gray-300');
|
||||
btnIpFilter.classList.replace('text-blue-700', 'text-gray-600');
|
||||
ipFilterLabel.textContent = 'Filter .150–.155';
|
||||
}
|
||||
}
|
||||
|
||||
btnIpFilter?.addEventListener('click', async () => {
|
||||
if (ipMatchingMacs !== null) {
|
||||
// Clear filter
|
||||
ipMatchingMacs = null;
|
||||
setIpFilterActive(false);
|
||||
applyFilters();
|
||||
return;
|
||||
}
|
||||
|
||||
btnIpFilter.disabled = true;
|
||||
ipFilterLabel.textContent = 'Loading…';
|
||||
ipFilterIcon.classList.add('animate-spin');
|
||||
|
||||
try {
|
||||
if (!cachedApClients) {
|
||||
const res = await fetch('/api/all-clients');
|
||||
if (!res.ok) { const e = await res.json(); throw new Error(e.detail || 'Request failed'); }
|
||||
cachedApClients = (await res.json()).ap_clients || {};
|
||||
}
|
||||
|
||||
ipMatchingMacs = new Set();
|
||||
for (const [apMac, clients] of Object.entries(cachedApClients)) {
|
||||
if (clients.some(c => isTargetIp(c.ip))) ipMatchingMacs.add(apMac);
|
||||
}
|
||||
|
||||
setIpFilterActive(true);
|
||||
applyFilters();
|
||||
} catch (e) {
|
||||
showToast('Failed to load client data: ' + e.message, 'error');
|
||||
ipFilterLabel.textContent = 'Filter .150–.155';
|
||||
} finally {
|
||||
btnIpFilter.disabled = false;
|
||||
ipFilterIcon.classList.remove('animate-spin');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Refresh ────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user