Files
edgeguard-native/internal/handlers/audit.go
Debian 4a34629023 feat(audit): Live-Stream im Dashboard via WebSocket
Recent-Activity-Karte zeigt neue audit_log-Events jetzt sofort statt
in 15s-Polls.

internal/services/audit/audit.go:
  - Repo bekommt Subscribe()-Methode mit fan-out-channel (Buffer 32,
    non-blocking-send — langsame Clients droppen Events statt die
    Pipeline zu blockieren).
  - Log() macht jetzt INSERT … RETURNING id, created_at und broadcastet
    den fertigen Entry an alle Subscribers. Broadcast nur nach
    erfolgreichem INSERT — failed inserts erscheinen nicht.

internal/handlers/audit.go:
  - Neuer GET /api/v1/audit/live (WebSocket): sendet beim Connect die
    letzten 50 Einträge (oldest→newest), danach Live-Stream aus
    Subscribe-Channel. 30s-Ping gegen HAProxy-Tunnel-Timeout.
  - Recent (Poll-Endpoint) bleibt für Fallbacks erhalten.

UI Dashboard:
  - useAuditLive(keep=15)-Hook ersetzt das 15s-useQuery-Poll.
  - WebSocket auf wss://<host>/api/v1/audit/live; Auto-Reconnect alle
    2s nach Drop.
  - dedupe per id (Snapshot + erste live-Events können sich kurz
    überschneiden während des Subscribe-Race).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:39:04 +02:00

116 lines
2.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handlers
import (
"context"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"git.netcell-it.de/projekte/edgeguard-native/internal/handlers/response"
"git.netcell-it.de/projekte/edgeguard-native/internal/services/audit"
)
type AuditHandler struct {
Repo *audit.Repo
}
func NewAuditHandler(repo *audit.Repo) *AuditHandler { return &AuditHandler{Repo: repo} }
func (h *AuditHandler) Register(rg *gin.RouterGroup) {
g := rg.Group("/audit")
g.GET("/recent", h.Recent)
g.GET("/live", h.Live)
}
// Recent returns the most recent audit_log entries — used by the
// dashboard fallback path (z.B. wenn WebSocket nicht verbinden kann).
// ?limit=N (1100, default 10).
func (h *AuditHandler) Recent(c *gin.Context) {
limit := 10
if v := c.Query("limit"); v != "" {
if n, err := strconv.Atoi(v); err == nil {
limit = n
}
}
rows, err := h.Repo.ListRecent(c.Request.Context(), limit)
if err != nil {
response.Internal(c, err)
return
}
response.OK(c, gin.H{"entries": rows})
}
// auditUpgrader: same-origin durch HAProxy, kein CheckOrigin.
var auditUpgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 4 * 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
// Live upgraded auf WebSocket: sendet einen Snapshot der letzten 50
// audit_log-Rows, danach jeden neuen Eintrag direkt aus dem
// Repo.broadcast()-Channel.
func (h *AuditHandler) Live(c *gin.Context) {
conn, err := auditUpgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
}
defer conn.Close()
// Snapshot
if rows, err := h.Repo.ListRecent(c.Request.Context(), 50); err == nil {
// In aufsteigender Reihenfolge schicken (newest last) damit der
// Client nach unten scrollt + neue Events natürlich anhängt.
for i := len(rows) - 1; i >= 0; i-- {
if err := conn.WriteJSON(rows[i]); err != nil {
return
}
}
}
// Live-Subscribe
ch, unsub := h.Repo.Subscribe()
defer unsub()
ctx, cancel := context.WithCancel(c.Request.Context())
defer cancel()
// Read-Loop für close-frame + ping-pong
go func() {
defer cancel()
for {
if _, _, err := conn.NextReader(); err != nil {
return
}
}
}()
ping := time.NewTicker(30 * time.Second)
defer ping.Stop()
_ = conn.SetWriteDeadline(time.Now().Add(60 * time.Second))
for {
select {
case <-ctx.Done():
return
case e, ok := <-ch:
if !ok {
return
}
_ = conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := conn.WriteJSON(e); err != nil {
return
}
case <-ping.C:
_ = conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := conn.WriteControl(websocket.PingMessage, nil,
time.Now().Add(5*time.Second)); err != nil {
return
}
}
}
}