feat(ui): add maintenance page for task management
Some checks failed
CI / build-and-test (push) Failing after -31m53s

* Implement maintenance page with task and log display
* Add backfill and metadata retry functionality
* Integrate grid component for project display in thoughts page
* Update types for maintenance tasks and logs
* Enhance sidebar and shell for new maintenance navigation
This commit is contained in:
2026-04-26 23:13:41 +02:00
parent b39cd3ba72
commit 927a118338
48 changed files with 2228 additions and 868 deletions

View File

@@ -1,7 +1,9 @@
package auth
import (
"net"
"sort"
"strings"
"sync"
"time"
)
@@ -16,12 +18,19 @@ type AccessSnapshot struct {
}
type AccessTracker struct {
mu sync.RWMutex
entries map[string]AccessSnapshot
mu sync.RWMutex
entries map[string]AccessSnapshot
ipCounts map[string]int
agentCounts map[string]int
totalRequests int
}
func NewAccessTracker() *AccessTracker {
return &AccessTracker{entries: make(map[string]AccessSnapshot)}
return &AccessTracker{
entries: make(map[string]AccessSnapshot),
ipCounts: make(map[string]int),
agentCounts: make(map[string]int),
}
}
func (t *AccessTracker) Record(keyID, path, remoteAddr, userAgent string, now time.Time) {
@@ -32,14 +41,36 @@ func (t *AccessTracker) Record(keyID, path, remoteAddr, userAgent string, now ti
t.mu.Lock()
defer t.mu.Unlock()
normalizedRemoteAddr := normalizeRemoteAddr(remoteAddr)
entry := t.entries[keyID]
entry.KeyID = keyID
entry.LastPath = path
entry.RemoteAddr = remoteAddr
entry.RemoteAddr = normalizedRemoteAddr
entry.UserAgent = userAgent
entry.LastAccessedAt = now.UTC()
entry.RequestCount++
t.entries[keyID] = entry
t.totalRequests++
if normalizedRemoteAddr != "" {
t.ipCounts[normalizedRemoteAddr]++
}
if userAgent != "" {
t.agentCounts[userAgent]++
}
}
func normalizeRemoteAddr(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return ""
}
host, _, err := net.SplitHostPort(trimmed)
if err == nil {
return host
}
return trimmed
}
func (t *AccessTracker) Snapshot() []AccessSnapshot {
@@ -79,3 +110,55 @@ func (t *AccessTracker) ConnectedCount(now time.Time, window time.Duration) int
}
return count
}
type RequestAggregate struct {
Key string `json:"key"`
RequestCount int `json:"request_count"`
}
type AccessMetrics struct {
TotalRequests int `json:"total_requests"`
UniquePrincipals int `json:"unique_principals"`
UniqueIPs int `json:"unique_ips"`
UniqueAgents int `json:"unique_agents"`
TopIPs []RequestAggregate `json:"top_ips"`
TopAgents []RequestAggregate `json:"top_agents"`
}
func (t *AccessTracker) Metrics(topN int) AccessMetrics {
if t == nil {
return AccessMetrics{}
}
if topN <= 0 {
topN = 10
}
t.mu.RLock()
defer t.mu.RUnlock()
return AccessMetrics{
TotalRequests: t.totalRequests,
UniquePrincipals: len(t.entries),
UniqueIPs: len(t.ipCounts),
UniqueAgents: len(t.agentCounts),
TopIPs: topAggregates(t.ipCounts, topN),
TopAgents: topAggregates(t.agentCounts, topN),
}
}
func topAggregates(items map[string]int, topN int) []RequestAggregate {
out := make([]RequestAggregate, 0, len(items))
for key, count := range items {
out = append(out, RequestAggregate{Key: key, RequestCount: count})
}
sort.Slice(out, func(i, j int) bool {
if out[i].RequestCount == out[j].RequestCount {
return out[i].Key < out[j].Key
}
return out[i].RequestCount > out[j].RequestCount
})
if len(out) > topN {
out = out[:topN]
}
return out
}

View File

@@ -30,6 +30,9 @@ func TestAccessTrackerRecordAndSnapshot(t *testing.T) {
if snap[0].UserAgent != "agent-a2" {
t.Fatalf("snapshot[0].UserAgent = %q, want agent-a2", snap[0].UserAgent)
}
if snap[0].RemoteAddr != "10.0.0.1" {
t.Fatalf("snapshot[0].RemoteAddr = %q, want 10.0.0.1", snap[0].RemoteAddr)
}
}
func TestAccessTrackerConnectedCount(t *testing.T) {
@@ -43,3 +46,42 @@ func TestAccessTrackerConnectedCount(t *testing.T) {
t.Fatalf("ConnectedCount() = %d, want 1", got)
}
}
func TestAccessTrackerMetrics(t *testing.T) {
tracker := NewAccessTracker()
now := time.Date(2026, 4, 4, 12, 0, 0, 0, time.UTC)
tracker.Record("client-a", "/mcp", "10.0.0.1:1234", "agent-a", now)
tracker.Record("client-a", "/mcp", "10.0.0.1:1234", "agent-a", now.Add(1*time.Second))
tracker.Record("client-b", "/files", "10.0.0.2:5678", "agent-b", now.Add(2*time.Second))
tracker.Record("client-c", "/files", "10.0.0.2:5678", "agent-b", now.Add(3*time.Second))
metrics := tracker.Metrics(5)
if metrics.TotalRequests != 4 {
t.Fatalf("TotalRequests = %d, want 4", metrics.TotalRequests)
}
if metrics.UniquePrincipals != 3 {
t.Fatalf("UniquePrincipals = %d, want 3", metrics.UniquePrincipals)
}
if metrics.UniqueIPs != 2 {
t.Fatalf("UniqueIPs = %d, want 2", metrics.UniqueIPs)
}
if metrics.UniqueAgents != 2 {
t.Fatalf("UniqueAgents = %d, want 2", metrics.UniqueAgents)
}
if len(metrics.TopIPs) != 2 {
t.Fatalf("len(TopIPs) = %d, want 2", len(metrics.TopIPs))
}
if metrics.TopIPs[0].RequestCount != 2 || metrics.TopIPs[1].RequestCount != 2 {
t.Fatalf("TopIPs counts = %+v, want both counts to be 2", metrics.TopIPs)
}
if metrics.TopIPs[0].Key != "10.0.0.1" && metrics.TopIPs[0].Key != "10.0.0.2" {
t.Fatalf("TopIPs[0].Key = %q, want normalized IP", metrics.TopIPs[0].Key)
}
if len(metrics.TopAgents) != 2 {
t.Fatalf("len(TopAgents) = %d, want 2", len(metrics.TopAgents))
}
if metrics.TopAgents[0].RequestCount != 2 || metrics.TopAgents[1].RequestCount != 2 {
t.Fatalf("TopAgents counts = %+v, want both counts to be 2", metrics.TopAgents)
}
}