diff --git a/UI_CHECKLIST.md b/UI_CHECKLIST.md
index ea01b1d..f59787a 100644
--- a/UI_CHECKLIST.md
+++ b/UI_CHECKLIST.md
@@ -61,7 +61,7 @@
- [x] Показать load, память, uptime и число клиентов.
- [x] Убрать дублирующиеся и технические метрики.
- [ ] Показать активные предупреждения с приоритетами.
-- [ ] Добавить компактные графики метрик.
+- [x] Добавить компактные графики метрик.
- [ ] Добавить выбор периода графиков.
- [x] Добавить основную кнопку открытия LuCI.
- [x] Добавить кнопку перехода к диагностике.
@@ -76,7 +76,7 @@
- [x] Создать таблицу клиентов.
- [~] Показывать online/offline состояние.
- [x] Показывать имя, IP, MAC и тип подключения.
-- [ ] Для Wi-Fi показывать сеть, сигнал и скорость.
+- [~] Для Wi-Fi показывать сеть, сигнал и скорость.
- [ ] Для Ethernet показывать интерфейс или порт, если доступно.
- [x] Добавить поиск по имени, IP и MAC.
- [~] Добавить фильтры по состоянию и типу подключения.
@@ -137,8 +137,8 @@
## Убираем и объединяем
-- [ ] Убрать локальные Reload / Refresh из отдельных панелей.
-- [ ] Добавить автообновление данных.
+- [x] Убрать локальные Reload / Refresh из отдельных панелей.
+- [x] Добавить автообновление данных.
- [x] Оставить одну глобальную кнопку обновления на экране объектов.
- [x] Убрать постоянный выбор массовой команды.
- [ ] Показывать массовые действия только при выбранных объектах.
@@ -152,7 +152,7 @@
## Общие состояния и качество
- [ ] Добавить skeleton loading.
-- [ ] Добавить единый компонент пустого состояния.
+- [~] Добавить единый компонент пустого состояния.
- [ ] Добавить единый компонент ошибки.
- [ ] Добавить toast-уведомления.
- [ ] Добавить единые диалоги подтверждения.
@@ -169,7 +169,7 @@
## Требования к API
-- [ ] Нормализовать данные клиентов.
+- [~] Нормализовать данные клиентов.
- [ ] Нормализовать состояние интерфейсов.
- [ ] Подготовить временные ряды для графиков.
- [ ] Добавить операторские операции поверх сырых команд.
diff --git a/agent/openwrt/rmm-agent.sh b/agent/openwrt/rmm-agent.sh
index 458fb8e..9c74924 100644
--- a/agent/openwrt/rmm-agent.sh
+++ b/agent/openwrt/rmm-agent.sh
@@ -223,10 +223,31 @@ wifi_clients_json() {
if command -v iwinfo >/dev/null 2>&1; then
for iface in $(iwinfo 2>/dev/null | awk '/^[^ ]/ { print $1 }'); do
iwinfo "$iface" assoclist 2>/dev/null | awk -v iface="$iface" '
+ function flush() {
+ if (mac != "") {
+ print iface "\t" mac "\t" signal "\t" rx "\t" tx
+ }
+ }
/^[0-9A-Fa-f][0-9A-Fa-f]:/ {
+ flush()
gsub(/,$/, "", $1)
- print iface "\t" $1
- }'
+ mac=$1
+ signal=$2
+ rx=""
+ tx=""
+ next
+ }
+ /^[[:space:]]*RX:/ {
+ sub(/^[[:space:]]*RX:[[:space:]]*/, "")
+ rx=$0
+ next
+ }
+ /^[[:space:]]*TX:/ {
+ sub(/^[[:space:]]*TX:[[:space:]]*/, "")
+ tx=$0
+ next
+ }
+ END { flush() }'
done | awk '
function esc(v) {
gsub(/\\/, "\\\\", v)
@@ -235,11 +256,15 @@ wifi_clients_json() {
gsub(/\r/, "\\r", v)
return v
}
- BEGIN { printf "[" }
+ BEGIN { FS="\t"; printf "[" }
{
if (count > 0) { printf "," }
count++
- printf "{\"interface\":\"%s\",\"mac\":\"%s\"}", esc($1), esc($2)
+ printf "{\"interface\":\"%s\",\"mac\":\"%s\"", esc($1), esc($2)
+ if ($3 != "") { printf ",\"signal_dbm\":\"%s\"", esc($3) }
+ if ($4 != "") { printf ",\"rx_rate\":\"%s\"", esc($4) }
+ if ($5 != "") { printf ",\"tx_rate\":\"%s\"", esc($5) }
+ printf "}"
}
END { printf "]" }'
return
diff --git a/agent/package/rmm-agent/files/usr/bin/rmm-agent b/agent/package/rmm-agent/files/usr/bin/rmm-agent
index 42df273..ca45e8e 100644
--- a/agent/package/rmm-agent/files/usr/bin/rmm-agent
+++ b/agent/package/rmm-agent/files/usr/bin/rmm-agent
@@ -222,10 +222,31 @@ wifi_clients_json() {
if command -v iwinfo >/dev/null 2>&1; then
for iface in $(iwinfo 2>/dev/null | awk '/^[^ ]/ { print $1 }'); do
iwinfo "$iface" assoclist 2>/dev/null | awk -v iface="$iface" '
+ function flush() {
+ if (mac != "") {
+ print iface "\t" mac "\t" signal "\t" rx "\t" tx
+ }
+ }
/^[0-9A-Fa-f][0-9A-Fa-f]:/ {
+ flush()
gsub(/,$/, "", $1)
- print iface "\t" $1
- }'
+ mac=$1
+ signal=$2
+ rx=""
+ tx=""
+ next
+ }
+ /^[[:space:]]*RX:/ {
+ sub(/^[[:space:]]*RX:[[:space:]]*/, "")
+ rx=$0
+ next
+ }
+ /^[[:space:]]*TX:/ {
+ sub(/^[[:space:]]*TX:[[:space:]]*/, "")
+ tx=$0
+ next
+ }
+ END { flush() }'
done | awk '
function esc(v) {
gsub(/\\/, "\\\\", v)
@@ -234,11 +255,15 @@ wifi_clients_json() {
gsub(/\r/, "\\r", v)
return v
}
- BEGIN { printf "[" }
+ BEGIN { FS="\t"; printf "[" }
{
if (count > 0) { printf "," }
count++
- printf "{\"interface\":\"%s\",\"mac\":\"%s\"}", esc($1), esc($2)
+ printf "{\"interface\":\"%s\",\"mac\":\"%s\"", esc($1), esc($2)
+ if ($3 != "") { printf ",\"signal_dbm\":\"%s\"", esc($3) }
+ if ($4 != "") { printf ",\"rx_rate\":\"%s\"", esc($4) }
+ if ($5 != "") { printf ",\"tx_rate\":\"%s\"", esc($5) }
+ printf "}"
}
END { printf "]" }'
return
diff --git a/server/internal/httpapi/server.go b/server/internal/httpapi/server.go
index 7e2b2fe..176e5b2 100644
--- a/server/internal/httpapi/server.go
+++ b/server/internal/httpapi/server.go
@@ -839,7 +839,8 @@ func (a *App) handleListCommands(w http.ResponseWriter, r *http.Request) {
}
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
- commands, found, err := a.store.ListCommands(r.Context(), deviceID, store.CommandListOptions{Limit: limit})
+ offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
+ commands, found, err := a.store.ListCommands(r.Context(), deviceID, store.CommandListOptions{Limit: limit, Offset: offset})
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to load commands")
return
@@ -1243,9 +1244,11 @@ func (a *App) handleCloseRemoteSession(w http.ResponseWriter, r *http.Request) {
func (a *App) handleListAuditEvents(w http.ResponseWriter, r *http.Request) {
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
+ offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
events, err := a.store.ListAuditEvents(r.Context(), store.AuditListOptions{
DeviceID: r.URL.Query().Get("device_id"),
Limit: limit,
+ Offset: offset,
})
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to load audit events")
diff --git a/server/internal/store/sqlite.go b/server/internal/store/sqlite.go
index 61b7232..c1bae21 100644
--- a/server/internal/store/sqlite.go
+++ b/server/internal/store/sqlite.go
@@ -21,7 +21,8 @@ type Store struct {
}
type CommandListOptions struct {
- Limit int
+ Limit int
+ Offset int
}
type MetricHistoryOptions struct {
@@ -31,6 +32,7 @@ type MetricHistoryOptions struct {
type AuditListOptions struct {
DeviceID string
Limit int
+ Offset int
}
type AlertListOptions struct {
@@ -741,14 +743,18 @@ func (s *Store) ListCommands(ctx context.Context, deviceID string, opts CommandL
if limit <= 0 || limit > 200 {
limit = 50
}
+ offset := opts.Offset
+ if offset < 0 {
+ offset = 0
+ }
rows, err := s.db.QueryContext(ctx, `
SELECT id, device_id, type, args_json, status, result_json, output, exit_code, attempt_count, max_attempts, created_at, expires_at, claimed_at, completed_at, cancelled_at, expired_at
FROM commands
WHERE device_id = ?
ORDER BY created_at DESC
-LIMIT ?
-`, deviceID, limit)
+LIMIT ? OFFSET ?
+`, deviceID, limit, offset)
if err != nil {
return nil, false, err
}
@@ -988,6 +994,10 @@ func (s *Store) ListAuditEvents(ctx context.Context, opts AuditListOptions) ([]m
if limit <= 0 || limit > 200 {
limit = 50
}
+ offset := opts.Offset
+ if offset < 0 {
+ offset = 0
+ }
query := `
SELECT id, actor, action, device_id, command_id, details_json, created_at
@@ -998,8 +1008,8 @@ FROM audit_events
query += `WHERE device_id = ?` + "\n"
args = append(args, opts.DeviceID)
}
- query += `ORDER BY created_at DESC LIMIT ?`
- args = append(args, limit)
+ query += `ORDER BY created_at DESC LIMIT ? OFFSET ?`
+ args = append(args, limit, offset)
rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil {
diff --git a/web/app.js b/web/app.js
index a031f65..de049dc 100644
--- a/web/app.js
+++ b/web/app.js
@@ -7,7 +7,14 @@ const state = {
clientFilter: "all",
commandFilter: "all",
commandStatusFilter: "",
+ commandLimit: 25,
+ commandOffset: 0,
+ commandHasMore: false,
+ auditLimit: 25,
+ auditOffset: 0,
+ auditHasMore: false,
commands: [],
+ auditEvents: [],
alerts: [],
remoteSessions: [],
selectedCommand: null,
@@ -83,10 +90,9 @@ const els = {
fleetGroup: document.querySelector("#fleetGroup"),
fleetTags: document.querySelector("#fleetTags"),
saveFleetBtn: document.querySelector("#saveFleetBtn"),
- reloadAlertsBtn: document.querySelector("#reloadAlertsBtn"),
alertSummary: document.querySelector("#alertSummary"),
alertList: document.querySelector("#alertList"),
- reloadMetricsHistoryBtn: document.querySelector("#reloadMetricsHistoryBtn"),
+ metricsHistorySummary: document.querySelector("#metricsHistorySummary"),
metricsHistory: document.querySelector("#metricsHistory"),
commandType: document.querySelector("#commandType"),
commandTarget: document.querySelector("#commandTarget"),
@@ -101,7 +107,6 @@ const els = {
remoteLuCIScheme: document.querySelector("#remoteLuCIScheme"),
remoteDuration: document.querySelector("#remoteDuration"),
createRemoteSessionBtn: document.querySelector("#createRemoteSessionBtn"),
- reloadRemoteSessionsBtn: document.querySelector("#reloadRemoteSessionsBtn"),
remoteSummary: document.querySelector("#remoteSummary"),
remoteSessionList: document.querySelector("#remoteSessionList"),
uciConfig: document.querySelector("#uciConfig"),
@@ -129,10 +134,10 @@ const els = {
presetReviewOutput: document.querySelector("#presetReviewOutput"),
cancelPresetReviewBtn: document.querySelector("#cancelPresetReviewBtn"),
applyPresetReviewBtn: document.querySelector("#applyPresetReviewBtn"),
- reloadCommandsBtn: document.querySelector("#reloadCommandsBtn"),
- reloadAuditBtn: document.querySelector("#reloadAuditBtn"),
commandSummary: document.querySelector("#commandSummary"),
commandStatusFilter: document.querySelector("#commandStatusFilter"),
+ loadMoreCommandsBtn: document.querySelector("#loadMoreCommandsBtn"),
+ loadMoreAuditBtn: document.querySelector("#loadMoreAuditBtn"),
auditSummary: document.querySelector("#auditSummary"),
commandList: document.querySelector("#commandList"),
commandDetailPanel: document.querySelector("#commandDetailPanel"),
@@ -598,6 +603,8 @@ function normalizedClients(device) {
ip: existing.ip || "-",
mac: station.mac || existing.mac || "-",
connection: `Wi-Fi ${station.interface || ""}`.trim(),
+ signal: station.signal_dbm ? `${station.signal_dbm} dBm` : "",
+ rate: [station.rx_rate ? `RX ${station.rx_rate}` : "", station.tx_rate ? `TX ${station.tx_rate}` : ""].filter(Boolean).join(" / "),
type: "wifi",
online: true,
});
@@ -627,6 +634,7 @@ function renderClients(device) {
${escapeHtml(client.ip)}
${escapeHtml(client.mac)}
${escapeHtml(client.connection)}
+ ${escapeHtml([client.signal, client.rate].filter(Boolean).join(" · ") || "-")}
В сети
`;
els.clientList.appendChild(row);
@@ -676,15 +684,41 @@ async function loadMetricsHistory() {
function renderMetricsHistory(samples) {
els.metricsHistory.innerHTML = "";
if (samples.length === 0) {
- els.metricsHistory.textContent = "No history yet";
+ els.metricsHistorySummary.textContent = "Нет замеров";
+ els.metricsHistory.innerHTML = '