diff --git a/internal/app/status.go b/internal/app/status.go index 231d3bf..b72f633 100644 --- a/internal/app/status.go +++ b/internal/app/status.go @@ -44,9 +44,6 @@ type publicStatusResponse struct { func statusSnapshot(info buildinfo.Info, tracker *auth.AccessTracker, oauthEnabled bool, now time.Time) statusAPIResponse { entries := tracker.Snapshot() metrics := tracker.Metrics(20) - metrics.TopIPs = nil - metrics.TopAgents = nil - metrics.TopTools = nil return statusAPIResponse{ Title: "Avelon Memory Crystal Server (AMCS)", Description: "AMCS is a memory server that captures, links, and retrieves structured project thoughts for AI assistants using semantic search, summaries, and MCP tools.", diff --git a/internal/app/status_test.go b/internal/app/status_test.go index 686c32b..3fd2d2e 100644 --- a/internal/app/status_test.go +++ b/internal/app/status_test.go @@ -58,8 +58,14 @@ func TestStatusSnapshotShowsTrackedAccess(t *testing.T) { if snapshot.Metrics.UniqueTools != 1 { t.Fatalf("Metrics.UniqueTools = %d, want 1", snapshot.Metrics.UniqueTools) } - if len(snapshot.Metrics.TopIPs) != 0 || len(snapshot.Metrics.TopAgents) != 0 || len(snapshot.Metrics.TopTools) != 0 { - t.Fatalf("Top breakdowns should be hidden in counts-only status: %+v", snapshot.Metrics) + if len(snapshot.Metrics.TopIPs) != 1 || len(snapshot.Metrics.TopAgents) != 1 || len(snapshot.Metrics.TopTools) != 1 { + t.Fatalf("Top breakdowns not populated: %+v", snapshot.Metrics) + } + if len(snapshot.Metrics.RecentLog) != 1 { + t.Fatalf("RecentLog len = %d, want 1", len(snapshot.Metrics.RecentLog)) + } + if snapshot.Metrics.RecentLog[0].Tool != "list_projects" { + t.Fatalf("RecentLog[0].Tool = %q, want %q", snapshot.Metrics.RecentLog[0].Tool, "list_projects") } } diff --git a/internal/auth/access_tracker.go b/internal/auth/access_tracker.go index 06019ce..765ef31 100644 --- a/internal/auth/access_tracker.go +++ b/internal/auth/access_tracker.go @@ -17,12 +17,24 @@ type AccessSnapshot struct { LastAccessedAt time.Time `json:"last_accessed_at"` } +const maxRecentLog = 100 + +type AccessLogEntry struct { + Timestamp time.Time `json:"timestamp"` + KeyID string `json:"key_id"` + IP string `json:"ip"` + UserAgent string `json:"user_agent"` + Tool string `json:"tool"` + Path string `json:"path"` +} + type AccessTracker struct { mu sync.RWMutex entries map[string]AccessSnapshot ipCounts map[string]int agentCounts map[string]int toolCounts map[string]int + recentLog []AccessLogEntry totalRequests int } @@ -32,6 +44,7 @@ func NewAccessTracker() *AccessTracker { ipCounts: make(map[string]int), agentCounts: make(map[string]int), toolCounts: make(map[string]int), + recentLog: make([]AccessLogEntry, 0, maxRecentLog), } } @@ -64,6 +77,19 @@ func (t *AccessTracker) Record(keyID, path, remoteAddr, userAgent, toolName stri if tool := strings.TrimSpace(toolName); tool != "" { t.toolCounts[tool]++ } + + logEntry := AccessLogEntry{ + Timestamp: now.UTC(), + KeyID: keyID, + IP: normalizedRemoteAddr, + UserAgent: userAgent, + Tool: strings.TrimSpace(toolName), + Path: path, + } + t.recentLog = append([]AccessLogEntry{logEntry}, t.recentLog...) + if len(t.recentLog) > maxRecentLog { + t.recentLog = t.recentLog[:maxRecentLog] + } } func normalizeRemoteAddr(value string) string { @@ -130,6 +156,7 @@ type AccessMetrics struct { TopIPs []RequestAggregate `json:"top_ips"` TopAgents []RequestAggregate `json:"top_agents"` TopTools []RequestAggregate `json:"top_tools"` + RecentLog []AccessLogEntry `json:"recent_log"` } func (t *AccessTracker) Metrics(topN int) AccessMetrics { @@ -143,6 +170,9 @@ func (t *AccessTracker) Metrics(topN int) AccessMetrics { t.mu.RLock() defer t.mu.RUnlock() + log := make([]AccessLogEntry, len(t.recentLog)) + copy(log, t.recentLog) + return AccessMetrics{ TotalRequests: t.totalRequests, UniquePrincipals: len(t.entries), @@ -152,6 +182,7 @@ func (t *AccessTracker) Metrics(topN int) AccessMetrics { TopIPs: topAggregates(t.ipCounts, topN), TopAgents: topAggregates(t.agentCounts, topN), TopTools: topAggregates(t.toolCounts, topN), + RecentLog: log, } } diff --git a/ui/src/components/dashboard/DashboardPage.svelte b/ui/src/components/dashboard/DashboardPage.svelte index a756e32..bc865a4 100644 --- a/ui/src/components/dashboard/DashboardPage.svelte +++ b/ui/src/components/dashboard/DashboardPage.svelte @@ -2,6 +2,7 @@ import type { StatusResponse } from '../../types'; import AccessTable from './AccessTable.svelte'; import ConnectionBreakdown from './ConnectionBreakdown.svelte'; + import RecentActivityLog from './RecentActivityLog.svelte'; import StatusCards from './StatusCards.svelte'; const { @@ -67,4 +68,7 @@ emptyLabel="No MCP tool calls recorded yet." /> +
| Time | +Principal | +IP | +User Agent | +Tool | +Path | +
|---|---|---|---|---|---|
| {formatTime(entry.timestamp)} | +{entry.key_id} |
+ {entry.ip || '—'} |
+ {entry.user_agent || '—'} | +
+ {#if entry.tool}
+ {entry.tool}
+ {:else}
+ —
+ {/if}
+ |
+ {entry.path} |
+