feat(ui): add message cache management page and dashboard enhancements
Some checks failed
CI / Test (1.23) (push) Failing after -30m37s
CI / Test (1.22) (push) Failing after -30m33s
CI / Build (push) Failing after -30m45s
CI / Lint (push) Failing after -30m39s

- Introduced MessageCachePage for browsing and managing cached webhook events.
- Enhanced DashboardPage to display runtime stats and message cache information.
- Added new API types for message cache events and system stats.
- Integrated SwaggerPage for API documentation and live request testing.
This commit is contained in:
2026-03-05 00:32:57 +02:00
parent 4b44340c58
commit 1490e0b596
47 changed files with 4430 additions and 611 deletions

View File

@@ -1,12 +1,18 @@
package api
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io/fs"
"math"
"net/http"
"os"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/google/uuid"
@@ -41,6 +47,16 @@ type Server struct {
wh WhatsHookedInterface
}
type systemStatsSampler struct {
mu sync.Mutex
lastCPUJiffies uint64
lastCPUTimestamp time.Time
lastNetTotal uint64
lastNetTimestamp time.Time
}
var runtimeStatsSampler = &systemStatsSampler{}
// NewServer creates a new API server with ResolveSpec integration
func NewServer(cfg *config.Config, db *bun.DB, wh WhatsHookedInterface) (*Server, error) {
// Create model registry and register models
@@ -85,6 +101,7 @@ func NewServer(cfg *config.Config, db *bun.DB, wh WhatsHookedInterface) (*Server
// Setup ResolveSpec routes on the protected /api/v1 subrouter (auto-generated CRUD)
restheadspec.SetupMuxRoutes(apiV1Router, handler, nil)
apiV1Router.HandleFunc("/system/stats", handleSystemStats).Methods("GET")
// Add custom routes (login, logout, etc.) on main router
SetupCustomRoutes(router, secProvider, db)
@@ -126,6 +143,179 @@ func NewServer(cfg *config.Config, db *bun.DB, wh WhatsHookedInterface) (*Server
}, nil
}
func handleSystemStats(w http.ResponseWriter, _ *http.Request) {
var mem runtime.MemStats
runtime.ReadMemStats(&mem)
cpuPercent := runtimeStatsSampler.sampleCPUPercent()
rxBytes, txBytes := readNetworkBytes()
netTotal := rxBytes + txBytes
netBytesPerSec := runtimeStatsSampler.sampleNetworkBytesPerSec(netTotal)
writeJSON(w, http.StatusOK, map[string]any{
"go_memory_bytes": mem.Alloc,
"go_memory_mb": float64(mem.Alloc) / (1024.0 * 1024.0),
"go_sys_memory_bytes": mem.Sys,
"go_sys_memory_mb": float64(mem.Sys) / (1024.0 * 1024.0),
"go_goroutines": runtime.NumGoroutine(),
"go_cpu_percent": cpuPercent,
"network_rx_bytes": rxBytes,
"network_tx_bytes": txBytes,
"network_total_bytes": netTotal,
"network_bytes_per_sec": netBytesPerSec,
})
}
func readProcessCPUJiffies() (uint64, bool) {
data, err := os.ReadFile("/proc/self/stat")
if err != nil {
return 0, false
}
parts := strings.Fields(string(data))
if len(parts) < 15 {
return 0, false
}
utime, err := strconv.ParseUint(parts[13], 10, 64)
if err != nil {
return 0, false
}
stime, err := strconv.ParseUint(parts[14], 10, 64)
if err != nil {
return 0, false
}
return utime + stime, true
}
func (s *systemStatsSampler) sampleCPUPercent() float64 {
const clockTicksPerSecond = 100.0 // Linux default USER_HZ
jiffies, ok := readProcessCPUJiffies()
if !ok {
return 0
}
now := time.Now()
s.mu.Lock()
defer s.mu.Unlock()
if s.lastCPUTimestamp.IsZero() || s.lastCPUJiffies == 0 {
s.lastCPUTimestamp = now
s.lastCPUJiffies = jiffies
return 0
}
elapsed := now.Sub(s.lastCPUTimestamp).Seconds()
deltaJiffies := jiffies - s.lastCPUJiffies
s.lastCPUTimestamp = now
s.lastCPUJiffies = jiffies
if elapsed <= 0 {
return 0
}
cpuSeconds := float64(deltaJiffies) / clockTicksPerSecond
cores := float64(runtime.NumCPU())
if cores < 1 {
cores = 1
}
percent := (cpuSeconds / elapsed) * 100.0 / cores
if percent < 0 {
return 0
}
if percent > 100 {
return 100
}
return math.Round(percent*100) / 100
}
func readNetworkBytes() (uint64, uint64) {
file, err := os.Open("/proc/net/dev")
if err != nil {
return 0, 0
}
defer file.Close()
var rxBytes uint64
var txBytes uint64
scanner := bufio.NewScanner(file)
lineNo := 0
for scanner.Scan() {
lineNo++
// Skip headers.
if lineNo <= 2 {
continue
}
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
parts := strings.SplitN(line, ":", 2)
if len(parts) != 2 {
continue
}
iface := strings.TrimSpace(parts[0])
if iface == "lo" {
continue
}
fields := strings.Fields(parts[1])
if len(fields) < 16 {
continue
}
rx, err := strconv.ParseUint(fields[0], 10, 64)
if err == nil {
rxBytes += rx
}
tx, err := strconv.ParseUint(fields[8], 10, 64)
if err == nil {
txBytes += tx
}
}
return rxBytes, txBytes
}
func (s *systemStatsSampler) sampleNetworkBytesPerSec(totalBytes uint64) float64 {
now := time.Now()
s.mu.Lock()
defer s.mu.Unlock()
if s.lastNetTimestamp.IsZero() {
s.lastNetTimestamp = now
s.lastNetTotal = totalBytes
return 0
}
elapsed := now.Sub(s.lastNetTimestamp).Seconds()
deltaBytes := totalBytes - s.lastNetTotal
s.lastNetTimestamp = now
s.lastNetTotal = totalBytes
if elapsed <= 0 {
return 0
}
bps := float64(deltaBytes) / elapsed
if bps < 0 {
return 0
}
return math.Round(bps*100) / 100
}
// Start starts the API server
func (s *Server) Start() error {
return s.serverMgr.ServeWithGracefulShutdown()
@@ -171,6 +361,7 @@ func SetupWhatsAppRoutes(router *mux.Router, wh WhatsHookedInterface, distFS fs.
// Account management (with auth)
router.HandleFunc("/api/accounts", h.Auth(h.Accounts))
router.HandleFunc("/api/accounts/status", h.Auth(h.AccountStatuses)).Methods("GET")
router.HandleFunc("/api/accounts/add", h.Auth(h.AddAccount)).Methods("POST")
router.HandleFunc("/api/accounts/update", h.Auth(h.UpdateAccount)).Methods("POST")
router.HandleFunc("/api/accounts/remove", h.Auth(h.RemoveAccount)).Methods("POST")