diff --git a/UI_CHECKLIST.md b/UI_CHECKLIST.md index e53be66..a06929b 100644 --- a/UI_CHECKLIST.md +++ b/UI_CHECKLIST.md @@ -72,27 +72,27 @@ ## Этап 3. Клиенты -- [ ] Объединить DHCP leases и Wi-Fi stations в единую модель клиента. -- [ ] Создать таблицу клиентов. -- [ ] Показывать online/offline состояние. -- [ ] Показывать имя, IP, MAC и тип подключения. +- [x] Объединить DHCP leases и Wi-Fi stations в единую модель клиента. +- [x] Создать таблицу клиентов. +- [~] Показывать online/offline состояние. +- [x] Показывать имя, IP, MAC и тип подключения. - [ ] Для Wi-Fi показывать сеть, сигнал и скорость. - [ ] Для Ethernet показывать интерфейс или порт, если доступно. -- [ ] Добавить поиск по имени, IP и MAC. -- [ ] Добавить фильтры по состоянию и типу подключения. -- [ ] Добавить понятное состояние отсутствия клиентов. -- [ ] Проверить длинные имена и неизвестные значения. +- [x] Добавить поиск по имени, IP и MAC. +- [~] Добавить фильтры по состоянию и типу подключения. +- [x] Добавить понятное состояние отсутствия клиентов. +- [x] Проверить длинные имена и неизвестные значения. ## Этап 3. Сеть -- [ ] Создать сводку WAN-подключения. -- [ ] Показывать состояние интернет-доступа и внешний IP. -- [ ] Создать список LAN/WAN/Wi-Fi/VPN интерфейсов. -- [ ] Показывать адреса, состояние и счетчики трафика. -- [ ] Показать основные маршруты. +- [x] Создать сводку WAN-подключения. +- [x] Показывать состояние интернет-доступа и внешний IP. +- [x] Создать список LAN/WAN/Wi-Fi/VPN интерфейсов. +- [~] Показывать адреса, состояние и счетчики трафика. +- [~] Показать основные маршруты. - [ ] Показывать Wi-Fi сети и подключенных клиентов. - [ ] Спрятать низкоуровневые детали в раскрываемые блоки. -- [ ] Добавить loading, empty и error states. +- [~] Добавить loading, empty и error states. ## Этап 4. Операции diff --git a/web/app.js b/web/app.js index 98a4c7f..4d15dff 100644 --- a/web/app.js +++ b/web/app.js @@ -4,6 +4,7 @@ const state = { selectedDeviceId: null, deviceTab: "overview", filter: "all", + clientFilter: "all", commandFilter: "all", commands: [], alerts: [], @@ -52,7 +53,11 @@ const els = { connectivityStatus: document.querySelector("#connectivityStatus"), inventoryJson: document.querySelector("#inventoryJson"), clientList: document.querySelector("#clientList"), + clientSummary: document.querySelector("#clientSummary"), + clientSearch: document.querySelector("#clientSearch"), interfaceCounters: document.querySelector("#interfaceCounters"), + networkSummary: document.querySelector("#networkSummary"), + networkHealth: document.querySelector("#networkHealth"), fleetSearch: document.querySelector("#fleetSearch"), fleetGroupFilter: document.querySelector("#fleetGroupFilter"), fleetTagFilter: document.querySelector("#fleetTagFilter"), @@ -464,39 +469,107 @@ function kbToMb(value) { return Math.round((Number(value || 0) / 1024) * 10) / 10; } -function renderClients(device) { +function formatBytes(value) { + let amount = Number(value || 0); + const units = ["Б", "КБ", "МБ", "ГБ", "ТБ"]; + let unit = 0; + while (amount >= 1024 && unit < units.length - 1) { + amount /= 1024; + unit += 1; + } + return `${amount >= 10 || unit === 0 ? Math.round(amount) : amount.toFixed(1)} ${units[unit]}`; +} + +function normalizedClients(device) { const leases = Array.isArray(device.inventory && device.inventory.dhcp_leases) ? device.inventory.dhcp_leases : []; const wifi = Array.isArray(device.inventory && device.inventory.wifi_clients) ? device.inventory.wifi_clients : []; + const byMac = new Map(); + for (const lease of leases) { + const mac = String(lease.mac || "").toLowerCase(); + const key = mac || `ip:${lease.ip || Math.random()}`; + byMac.set(key, { + name: lease.hostname && lease.hostname !== "*" ? lease.hostname : "", + ip: lease.ip || "-", + mac: lease.mac || "-", + connection: "Проводное / DHCP", + type: "wired", + online: true, + }); + } + for (const station of wifi) { + const mac = String(station.mac || "").toLowerCase(); + const existing = byMac.get(mac) || {}; + byMac.set(mac || `wifi:${station.interface || Math.random()}`, { + ...existing, + name: existing.name || `Wi-Fi клиент ${station.mac || ""}`, + ip: existing.ip || "-", + mac: station.mac || existing.mac || "-", + connection: `Wi-Fi ${station.interface || ""}`.trim(), + type: "wifi", + online: true, + }); + } + return [...byMac.values()]; +} + +function renderClients(device) { + const clients = normalizedClients(device); + const search = els.clientSearch.value.trim().toLowerCase(); + const filtered = clients.filter((client) => { + if (state.clientFilter !== "all" && client.type !== state.clientFilter) return false; + return !search || [client.name, client.ip, client.mac, client.connection].join(" ").toLowerCase().includes(search); + }); els.clientList.innerHTML = ""; - if (leases.length === 0 && wifi.length === 0) { - els.clientList.textContent = "No client data"; + const wifiCount = clients.filter((client) => client.type === "wifi").length; + els.clientSummary.textContent = `${clients.length} всего / ${wifiCount} Wi-Fi`; + if (filtered.length === 0) { + els.clientList.innerHTML = '
${escapeHtml(client.mac)}
+ ${escapeHtml(client.connection)}
+ В сети
+ `;
els.clientList.appendChild(row);
}
}
function renderInterfaceCounters(device) {
const counters = Array.isArray(device.metrics && device.metrics.interface_counters) ? device.metrics.interface_counters : [];
+ const addresses = Array.isArray(device.inventory && device.inventory.interfaces) ? device.inventory.interfaces : [];
+ const byName = new Map();
+ for (const address of addresses) {
+ if (!byName.has(address.name)) byName.set(address.name, []);
+ byName.get(address.name).push(address.address);
+ }
els.interfaceCounters.innerHTML = "";
+ els.networkSummary.textContent = `${counters.length} интерфейсов`;
+ els.networkHealth.innerHTML = `
+