feat(ui): add message cache management page and dashboard enhancements
- 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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user