From 0bf2430297391f5152eb3c75d383202243ccf9ab Mon Sep 17 00:00:00 2001 From: benya Date: Fri, 5 Jun 2026 20:05:07 +0300 Subject: [PATCH] Add alert history filter and Go production package --- agent/README.md | 2 + .../package/rmm-agent-go-production/Makefile | 40 +++++++++++++++++++ .../package/rmm-agent-go-production/README.md | 35 ++++++++++++++++ .../files/etc/init.d/rmm-agent | 23 +++++++++++ .../files/etc/rmm-agent.conf | 7 ++++ server/internal/httpapi/server.go | 25 ++++++++++-- web/app.js | 28 +++++++++---- web/index.html | 5 +++ 8 files changed, 154 insertions(+), 11 deletions(-) create mode 100644 agent/package/rmm-agent-go-production/Makefile create mode 100644 agent/package/rmm-agent-go-production/README.md create mode 100644 agent/package/rmm-agent-go-production/files/etc/init.d/rmm-agent create mode 100644 agent/package/rmm-agent-go-production/files/etc/rmm-agent.conf diff --git a/agent/README.md b/agent/README.md index 9e4cede..8b3863d 100644 --- a/agent/README.md +++ b/agent/README.md @@ -143,9 +143,11 @@ Package skeleton: ```text agent/package/rmm-agent agent/package/rmm-agent-go +agent/package/rmm-agent-go-production ``` The shell package installs `/usr/bin/rmm-agent`, `/etc/init.d/rmm-agent`, and `/etc/rmm-agent.conf`. The Go package installs `/usr/bin/rmm-agent-go`, `/etc/init.d/rmm-agent-go`, and `/etc/rmm-agent-go.conf`. +The production Go package installs `/usr/bin/rmm-agent`, `/etc/init.d/rmm-agent`, and `/etc/rmm-agent.conf` using the Go runtime instead of the shell script. For Docker Compose reverse SSH access, install the generated tunnel private key at `/etc/rmm-agent/tunnel_key` with mode `600`. The agent automatically uses it for `remote_ssh_reverse`. diff --git a/agent/package/rmm-agent-go-production/Makefile b/agent/package/rmm-agent-go-production/Makefile new file mode 100644 index 0000000..734c23d --- /dev/null +++ b/agent/package/rmm-agent-go-production/Makefile @@ -0,0 +1,40 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=rmm-agent-go-production +PKG_VERSION:=0.5.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=RMM OpenWrt +PKG_LICENSE:=MIT + +include $(INCLUDE_DIR)/package.mk + +define Package/rmm-agent-go-production + SECTION:=admin + CATEGORY:=Administration + TITLE:=OpenWrt RMM Go agent (production replacement) + DEPENDS:=+ca-bundle +ip-tiny +iwinfo +openssh-client +openssh-keygen +endef + +define Package/rmm-agent-go-production/description + Go implementation of the outbound polling agent installed as the primary rmm-agent service. +endef + +define Build/Compile + [ -x ./files/usr/bin/rmm-agent ] +endef + +define Package/rmm-agent-go-production/conffiles +/etc/rmm-agent.conf +endef + +define Package/rmm-agent-go-production/install + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) ./files/usr/bin/rmm-agent $(1)/usr/bin/rmm-agent + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/etc/init.d/rmm-agent $(1)/etc/init.d/rmm-agent + $(INSTALL_DIR) $(1)/etc + $(INSTALL_CONF) ./files/etc/rmm-agent.conf $(1)/etc/rmm-agent.conf +endef + +$(eval $(call BuildPackage,rmm-agent-go-production)) diff --git a/agent/package/rmm-agent-go-production/README.md b/agent/package/rmm-agent-go-production/README.md new file mode 100644 index 0000000..4e2ad4d --- /dev/null +++ b/agent/package/rmm-agent-go-production/README.md @@ -0,0 +1,35 @@ +# rmm-agent-go-production OpenWrt Package + +OpenWrt package skeleton for installing the Go agent as the primary `rmm-agent` service. + +## Buildroot Usage + +Cross-build the Go binary for the router target first and place it into the package-local files directory: + +```sh +mkdir -p agent/package/rmm-agent-go-production/files/usr/bin +GOOS=linux GOARCH=mipsle GOMIPS=softfloat go build -trimpath -ldflags="-s -w" -o agent/package/rmm-agent-go-production/files/usr/bin/rmm-agent ./agent/go/cmd/rmm-agent +``` + +Copy or symlink this directory into an OpenWrt buildroot package path: + +```sh +cp -R agent/package/rmm-agent-go-production /path/to/openwrt/package/rmm-agent-go-production +cd /path/to/openwrt +make package/rmm-agent-go-production/compile V=s +``` + +Then install the generated `.ipk` on a router that already has `/etc/rmm-agent.conf` with the correct `DEVICE_ID` and `DEVICE_TOKEN`: + +```sh +opkg install /tmp/rmm-agent-go-production_*.ipk +/etc/init.d/rmm-agent restart +``` + +## Installed Files + +- `/usr/bin/rmm-agent` +- `/etc/init.d/rmm-agent` +- `/etc/rmm-agent.conf` + +This package is intended for the final shell-to-Go migration when the router should keep the same RMM object identity. diff --git a/agent/package/rmm-agent-go-production/files/etc/init.d/rmm-agent b/agent/package/rmm-agent-go-production/files/etc/init.d/rmm-agent new file mode 100644 index 0000000..d66f254 --- /dev/null +++ b/agent/package/rmm-agent-go-production/files/etc/init.d/rmm-agent @@ -0,0 +1,23 @@ +#!/bin/sh /etc/rc.common + +START=95 +STOP=10 +USE_PROCD=1 + +PROG=/usr/bin/rmm-agent +CONFIG_FILE=/etc/rmm-agent.conf + +start_service() { + procd_open_instance + procd_set_param command "$PROG" -config "$CONFIG_FILE" + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_set_param term_timeout 15 + procd_set_param respawn 3600 5 5 + procd_close_instance +} + +stop_service() { + service_stop "$PROG" + rm -rf /tmp/rmm-agent.lock +} diff --git a/agent/package/rmm-agent-go-production/files/etc/rmm-agent.conf b/agent/package/rmm-agent-go-production/files/etc/rmm-agent.conf new file mode 100644 index 0000000..24ebaac --- /dev/null +++ b/agent/package/rmm-agent-go-production/files/etc/rmm-agent.conf @@ -0,0 +1,7 @@ +SERVER_URL="http://127.0.0.1:8080" +ENROLLMENT_TOKEN="dev-enroll-token" +INTERVAL_SECONDS="30" +CHECK_TARGETS="1.1.1.1 8.8.8.8" +TUNNEL_IDENTITY_FILE="/etc/rmm-agent/tunnel_key" +DEVICE_ID="" +DEVICE_TOKEN="" diff --git a/server/internal/httpapi/server.go b/server/internal/httpapi/server.go index 03f31cd..86abbcf 100644 --- a/server/internal/httpapi/server.go +++ b/server/internal/httpapi/server.go @@ -500,16 +500,35 @@ func (a *App) handleListAlerts(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusNotFound, "not found") return } - alerts, found, err := a.refreshDeviceAlerts(r.Context(), parts[2]) + status := strings.TrimSpace(r.URL.Query().Get("status")) + if status == "" || status == "open" { + alerts, found, err := a.refreshDeviceAlerts(r.Context(), parts[2]) + if err != nil { + writeError(w, http.StatusInternalServerError, "failed to load alerts") + return + } + if !found { + writeError(w, http.StatusNotFound, "device not found") + return + } + writeJSON(w, http.StatusOK, map[string]any{"alerts": alerts}) + return + } + _, found, err := a.store.GetDevice(r.Context(), parts[2]) if err != nil { - writeError(w, http.StatusInternalServerError, "failed to load alerts") + writeError(w, http.StatusInternalServerError, "failed to load device") return } if !found { writeError(w, http.StatusNotFound, "device not found") return } - writeJSON(w, http.StatusOK, map[string]any{"alerts": alerts}) + list, err := a.store.ListAlerts(r.Context(), store.AlertListOptions{DeviceID: parts[2], Status: status, Limit: 200}) + if err != nil { + writeError(w, http.StatusInternalServerError, "failed to load alerts") + return + } + writeJSON(w, http.StatusOK, map[string]any{"alerts": list}) } func (a *App) handleAcknowledgeAlert(w http.ResponseWriter, r *http.Request) { diff --git a/web/app.js b/web/app.js index 87caa29..2456dd1 100644 --- a/web/app.js +++ b/web/app.js @@ -5,6 +5,7 @@ const state = { deviceTab: "overview", filter: "all", clientFilter: "all", + alertStatusFilter: "open", commandFilter: "all", commandStatusFilter: "", commandLimit: 25, @@ -95,6 +96,7 @@ const els = { saveFleetBtn: document.querySelector("#saveFleetBtn"), alertSummary: document.querySelector("#alertSummary"), alertList: document.querySelector("#alertList"), + alertStatusFilter: document.querySelector("#alertStatusFilter"), metricsHistorySummary: document.querySelector("#metricsHistorySummary"), metricsHistory: document.querySelector("#metricsHistory"), commandType: document.querySelector("#commandType"), @@ -749,7 +751,8 @@ function metricChart(label, unit, points, tone) { async function loadAlerts() { if (!state.selectedDeviceId) return; - const data = await api(`/api/devices/${encodeURIComponent(state.selectedDeviceId)}/alerts`); + const status = encodeURIComponent(state.alertStatusFilter || "open"); + const data = await api(`/api/devices/${encodeURIComponent(state.selectedDeviceId)}/alerts?status=${status}`); state.alerts = data.alerts || []; renderAlerts(state.alerts); } @@ -758,13 +761,19 @@ function renderAlerts(alerts) { els.alertList.innerHTML = ""; const activeCount = alerts.filter((alert) => alert.status === "active").length; const acknowledgedCount = alerts.filter((alert) => alert.status === "acknowledged").length; - els.alertSummary.textContent = `${activeCount} active / ${acknowledgedCount} ack`; + const resolvedCount = alerts.filter((alert) => alert.status === "resolved").length; + els.alertSummary.textContent = `${activeCount} active / ${acknowledgedCount} ack / ${resolvedCount} resolved`; if (alerts.length === 0) { - els.alertList.textContent = "No active alerts"; + els.alertList.textContent = state.alertStatusFilter === "resolved" ? "Нет resolved alerts" : "Нет активных алертов"; return; } for (const alert of alerts) { const details = alert.details ? Object.entries(alert.details).map(([key, value]) => `${key}: ${value}`).join(" / ") : ""; + const actionButtons = alert.status === "resolved" + ? `` + : ` + + `; const row = document.createElement("div"); row.className = `mini-row alert-row ${alert.severity || "warning"} ${alert.status || "active"}`; row.innerHTML = ` @@ -774,15 +783,14 @@ function renderAlerts(alerts) { ${escapeHtml(details || "Подробности")}
Первый раз: ${escapeHtml(formatDate(alert.first_seen_at || alert.created_at))}
Последний раз: ${escapeHtml(formatDate(alert.last_seen_at || alert.created_at))}
+ ${alert.resolved_at ? `
Resolved: ${escapeHtml(formatDate(alert.resolved_at))}
` : ""}
${escapeHtml(alert.message || "")}
- - - + ${actionButtons} `; - row.querySelector('[data-action="diagnose"]').addEventListener("click", () => runAlertDiagnostics(alert)); + row.querySelector('[data-action="diagnose"]')?.addEventListener("click", () => runAlertDiagnostics(alert)); row.querySelector('[data-action="commands"]').addEventListener("click", scrollToCommands); - row.querySelector('[data-action="ack"]').addEventListener("click", () => acknowledgeAlert(alert.id)); + row.querySelector('[data-action="ack"]')?.addEventListener("click", () => acknowledgeAlert(alert.id)); els.alertList.appendChild(row); } } @@ -1523,6 +1531,10 @@ els.openLuciBtn.addEventListener("click", openLuciOrRemoteAccess); els.runFullDiagnosticBtn.addEventListener("click", () => runFullDiagnostic().catch((error) => setStatus(error.message))); els.loadMoreCommandsBtn.addEventListener("click", () => loadCommands({ append: true }).catch((error) => setStatus(error.message))); els.loadMoreAuditBtn.addEventListener("click", () => loadAudit({ append: true }).catch((error) => setStatus(error.message))); +els.alertStatusFilter.addEventListener("change", () => { + state.alertStatusFilter = els.alertStatusFilter.value; + loadAlerts().catch((error) => setStatus(error.message)); +}); els.sendCommandBtn.addEventListener("click", () => sendCommand().catch((error) => setStatus(error.message))); els.sendBulkCommandBtn.addEventListener("click", () => sendBulkCommand().catch((error) => setStatus(error.message))); els.saveFleetBtn.addEventListener("click", () => saveFleetMetadata().catch((error) => setStatus(error.message))); diff --git a/web/index.html b/web/index.html index d01eff2..109c909 100644 --- a/web/index.html +++ b/web/index.html @@ -414,6 +414,11 @@

Проблемы

+ 0 active