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:
13
app/main.py
13
app/main.py
@@ -263,6 +263,19 @@ async def api_reboot(request: Request, db: Session = Depends(get_db)):
|
||||
return {"status": "ok", "mac": mac}
|
||||
|
||||
|
||||
@app.get("/api/all-clients")
|
||||
async def api_all_clients(request: Request):
|
||||
user = get_current_user(request)
|
||||
if not user:
|
||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||
try:
|
||||
data = await omada_client.get_all_clients()
|
||||
return {"ap_clients": data}
|
||||
except Exception as exc:
|
||||
logger.error("Failed to fetch all clients: %s", exc)
|
||||
raise HTTPException(status_code=502, detail=str(exc))
|
||||
|
||||
|
||||
@app.get("/api/ap-clients")
|
||||
async def api_ap_clients(request: Request, mac: str = "", site_key: str = ""):
|
||||
user = get_current_user(request)
|
||||
|
||||
21
app/omada.py
21
app/omada.py
@@ -188,6 +188,27 @@ class OmadaClient:
|
||||
return int(device.get("uptimeLong", 0))
|
||||
raise RuntimeError(f"AP {mac} not found in site {site_key}")
|
||||
|
||||
async def get_all_clients(self) -> dict[str, list[dict]]:
|
||||
"""Return {ap_mac: [clients]} for every site (wireless clients only)."""
|
||||
await self._ensure_ready()
|
||||
ap_clients: dict[str, list] = {}
|
||||
for site_name, site_key in self._sites.items():
|
||||
try:
|
||||
data = await self._request_with_retry(
|
||||
"GET",
|
||||
f"{OMADA_BASE_URL}/{self._omadac_id}/api/v2/sites/{site_key}/clients",
|
||||
params={"page": 1, "pageSize": 1000, "filters.active": "true"},
|
||||
)
|
||||
result = data.get("result", {})
|
||||
clients = result.get("data", []) if isinstance(result, dict) else (result if isinstance(result, list) else [])
|
||||
for c in clients:
|
||||
mac = c.get("apMac", "").upper().replace("-", ":")
|
||||
if mac:
|
||||
ap_clients.setdefault(mac, []).append(c)
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to fetch clients for site '%s': %s", site_name, exc)
|
||||
return ap_clients
|
||||
|
||||
async def get_ap_clients(self, ap_mac: str, site_key: str) -> list[dict]:
|
||||
"""Fetch wireless clients currently connected to a specific AP."""
|
||||
await self._ensure_ready()
|
||||
|
||||
@@ -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