Files
edgeguard-native/internal/handlers/alerts.go
noroot 4675c6062e feat(alerts): "Quittierte löschen" — Backlog war nur seitenweise raeumbar
Gemeldet als "Loeschen tut nichts". Das Loeschen hat funktioniert: der
Audit-Eintrag zeigt deleted=25 und die IDs waren danach weg. Nur war das
unsichtbar — die Kopf-Checkbox der Tabelle markiert ausschliesslich die
AKTUELLE Seite (25 Zeilen), und bei einem Backlog von ~950 optisch
identischen backend.down-Alarmen ruecken sofort die naechsten 25 nach.
Fuer den Operator sah es aus, als sei nichts passiert; um den Backlog
leerzubekommen waeren ~38 Klick-Runden noetig gewesen.

Neu: POST /alerts/events/delete-acknowledged loescht alle quittierten
Events auf einmal — Gegenstueck zum bereits vorhandenen "Alle
quittieren". Bewusst nur die quittierten: was noch niemand gesehen hat,
soll nicht per Sammelaktion verschwinden. Der Button zeigt die Anzahl
ueber den gesamten Datensatz, nicht nur die sichtbare Seite.

Ausserdem meldet das seitenweise Loeschen jetzt die Anzahl zurueck
("N Alarme geloescht") statt nur "Loeschen" und leert die Auswahl —
damit ist erkennbar, dass etwas passiert ist, auch wenn die Liste
optisch gleich aussieht.

Die automatische Aufraeumung (90 Tage) arbeitet uebrigens korrekt, sie
loescht taeglich die Eintraege die ueber die Grenze rutschen — der
Backlog stammt aus dem Zeitraum davor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 12:59:15 +02:00

212 lines
6.4 KiB
Go

package handlers
import (
"errors"
"strconv"
"github.com/gin-gonic/gin"
"git.netcell-it.de/projekte/edgeguard-native/internal/handlers/response"
"git.netcell-it.de/projekte/edgeguard-native/internal/services/alerts"
"git.netcell-it.de/projekte/edgeguard-native/internal/services/audit"
)
// AlertsHandler exposes:
//
// GET /api/v1/alerts/channels
// POST /api/v1/alerts/channels
// PUT /api/v1/alerts/channels/:id
// DELETE /api/v1/alerts/channels/:id
// POST /api/v1/alerts/test — Test-Event in alle aktiven Channels
// GET /api/v1/alerts/events?limit=N&open=true — History (open=nur offene)
// POST /api/v1/alerts/events/acknowledge — Bulk-Quittieren {ids:[…]}
// POST /api/v1/alerts/events/acknowledge-all — alle offenen quittieren
// POST /api/v1/alerts/events/delete — Bulk-Löschen {ids:[…]}
// POST /api/v1/alerts/events/delete-acknowledged — alle quittierten löschen
type AlertsHandler struct {
Service *alerts.Service
Audit *audit.Repo
NodeID string
}
func NewAlertsHandler(s *alerts.Service, a *audit.Repo, nodeID string) *AlertsHandler {
return &AlertsHandler{Service: s, Audit: a, NodeID: nodeID}
}
func (h *AlertsHandler) Register(rg *gin.RouterGroup) {
g := rg.Group("/alerts")
g.GET("/channels", h.ListChannels)
g.POST("/channels", h.CreateChannel)
g.PUT("/channels/:id", h.UpdateChannel)
g.DELETE("/channels/:id", h.DeleteChannel)
g.POST("/test", h.TestFire)
g.GET("/events", h.ListEvents)
g.POST("/events/acknowledge", h.AcknowledgeEvents)
g.POST("/events/acknowledge-all", h.AcknowledgeAllEvents)
g.POST("/events/delete", h.DeleteEvents)
g.POST("/events/delete-acknowledged", h.DeleteAcknowledgedEvents)
}
func (h *AlertsHandler) ListChannels(c *gin.Context) {
out, err := h.Service.ListChannels(c.Request.Context())
if err != nil {
response.Internal(c, err)
return
}
response.OK(c, gin.H{"channels": out})
}
func (h *AlertsHandler) CreateChannel(c *gin.Context) {
var req alerts.Channel
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err)
return
}
out, err := h.Service.CreateChannel(c.Request.Context(), req)
if err != nil {
response.Internal(c, err)
return
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "alert.channel.create",
out.Name, out, h.NodeID)
response.Created(c, out)
}
func (h *AlertsHandler) UpdateChannel(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
var req alerts.Channel
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err)
return
}
out, err := h.Service.UpdateChannel(c.Request.Context(), id, req)
if err != nil {
if errors.Is(err, alerts.ErrNotFound) {
response.NotFound(c, err)
return
}
response.Internal(c, err)
return
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "alert.channel.update",
out.Name, out, h.NodeID)
response.OK(c, out)
}
func (h *AlertsHandler) DeleteChannel(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
if err := h.Service.DeleteChannel(c.Request.Context(), id); err != nil {
if errors.Is(err, alerts.ErrNotFound) {
response.NotFound(c, err)
return
}
response.Internal(c, err)
return
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "alert.channel.delete",
strconv.FormatInt(id, 10), gin.H{"id": id}, h.NodeID)
response.NoContent(c)
}
func (h *AlertsHandler) TestFire(c *gin.Context) {
ev, err := h.Service.Fire(c.Request.Context(), "test", alerts.SeverityInfo,
"EdgeGuard-Test-Alert",
"Dies ist ein Test-Event. Wenn du das siehst, funktionieren deine Alert-Channels.")
if err != nil {
response.Internal(c, err)
return
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "alert.test", "test", ev, h.NodeID)
response.OK(c, ev)
}
func (h *AlertsHandler) ListEvents(c *gin.Context) {
limit := 100
if v := c.Query("limit"); v != "" {
if n, err := strconv.Atoi(v); err == nil {
limit = n
}
}
// ?open=true → nur offene (nicht quittierte) Events. Nutzt die
// Dashboard-Karte, damit Quittieren die Meldung verschwinden lässt.
openOnly := c.Query("open") == "true"
out, err := h.Service.ListEvents(c.Request.Context(), limit, openOnly)
if err != nil {
response.Internal(c, err)
return
}
response.OK(c, gin.H{"events": out})
}
// eventIDsRequest ist der Body für Bulk-Quittieren/-Löschen.
type eventIDsRequest struct {
IDs []int64 `json:"ids"`
}
// AcknowledgeEvents quittiert die übergebenen Event-IDs.
func (h *AlertsHandler) AcknowledgeEvents(c *gin.Context) {
var req eventIDsRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err)
return
}
n, err := h.Service.Acknowledge(c.Request.Context(), req.IDs)
if err != nil {
response.Internal(c, err)
return
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "alert.events.acknowledge",
strconv.Itoa(len(req.IDs)), gin.H{"ids": req.IDs, "acknowledged": n}, h.NodeID)
response.OK(c, gin.H{"acknowledged": n})
}
// AcknowledgeAllEvents quittiert alle offenen Events.
func (h *AlertsHandler) AcknowledgeAllEvents(c *gin.Context) {
n, err := h.Service.AcknowledgeAll(c.Request.Context())
if err != nil {
response.Internal(c, err)
return
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "alert.events.acknowledge_all",
"all", gin.H{"acknowledged": n}, h.NodeID)
response.OK(c, gin.H{"acknowledged": n})
}
// DeleteEvents löscht die übergebenen Event-IDs endgültig.
func (h *AlertsHandler) DeleteEvents(c *gin.Context) {
var req eventIDsRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err)
return
}
n, err := h.Service.DeleteEvents(c.Request.Context(), req.IDs)
if err != nil {
response.Internal(c, err)
return
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "alert.events.delete",
strconv.Itoa(len(req.IDs)), gin.H{"ids": req.IDs, "deleted": n}, h.NodeID)
response.OK(c, gin.H{"deleted": n})
}
// DeleteAcknowledgedEvents löscht alle quittierten Events auf einmal.
// Gegenstück zu AcknowledgeAllEvents — ohne das kam man an einen
// groesseren Backlog nur seitenweise heran (die Kopf-Checkbox der
// Tabelle markiert nur die aktuelle Seite).
func (h *AlertsHandler) DeleteAcknowledgedEvents(c *gin.Context) {
n, err := h.Service.DeleteAcknowledged(c.Request.Context())
if err != nil {
response.Internal(c, err)
return
}
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "alert.events.delete_acknowledged",
"acknowledged", gin.H{"deleted": n}, h.NodeID)
response.OK(c, gin.H{"deleted": n})
}