diff --git a/internal/worker/handlers_data.go b/internal/worker/handlers_data.go index 932b912..0a4b570 100644 --- a/internal/worker/handlers_data.go +++ b/internal/worker/handlers_data.go @@ -593,3 +593,119 @@ func (s *Service) handleGetObservationByID(w http.ResponseWriter, r *http.Reques writeJSON(w, obs) } + + +// handleGraphStats returns graph statistics for the dashboard. +// Uses relation data to compute knowledge graph metrics. +func (s *Service) handleGraphStats(w http.ResponseWriter, r *http.Request) { + // Get relation count (edges) - this represents the knowledge graph + edgeCount, err := s.relationStore.GetTotalRelationCount(r.Context()) + if err != nil { + edgeCount = 0 + } + + // Count by relation type + edgeTypes := make(map[string]int) + for _, t := range models.AllRelationTypes { + relations, err := s.relationStore.GetRelationsByType(r.Context(), t, 10000) + if err == nil { + edgeTypes[string(t)] = len(relations) + } + } + + // Get unique observation IDs involved in relations (approximate node count) + // For now, use edge count as a proxy - each edge has 2 nodes + nodeCount := 0 + if edgeCount > 0 { + // Rough estimate: unique nodes ≈ edges * 1.5 (since nodes can have multiple edges) + nodeCount = int(float64(edgeCount) * 1.5) + } + + // Calculate average degree + var avgDegree float64 + if nodeCount > 0 { + avgDegree = float64(edgeCount*2) / float64(nodeCount) + } + + // Graph is enabled if we have any edges (relations) + enabled := edgeCount > 0 + + writeJSON(w, map[string]any{ + "enabled": enabled, + "nodeCount": nodeCount, + "edgeCount": edgeCount, + "avgDegree": avgDegree, + "maxDegree": 0, + "minDegree": 0, + "medianDegree": 0.0, + "edgeTypes": edgeTypes, + "config": map[string]any{ + "maxHops": 2, + "branchFactor": 10, + "edgeWeight": 0.3, + "rebuildIntervalMin": 30, + }, + }) +} + +// handleVectorMetrics returns vector database metrics for the dashboard. +// Returns enabled: false if vector features are not available. +func (s *Service) handleVectorMetrics(w http.ResponseWriter, r *http.Request) { + if s.vectorClient == nil { + writeJSON(w, map[string]any{ + "enabled": false, + "message": "Vector database not initialized", + }) + return + } + + // Get cache stats from vector client + cacheSize, cacheMax := s.vectorClient.CacheStats() + cacheStats := s.vectorClient.GetCacheStats() + count, _ := s.vectorClient.Count(r.Context()) + + uptime := time.Since(s.startTime).Round(time.Second).String() + + // Calculate total queries from cache hits/misses + totalQueries := cacheStats.EmbeddingHits + cacheStats.EmbeddingMisses + cacheStats.ResultHits + cacheStats.ResultMisses + totalHits := cacheStats.EmbeddingHits + cacheStats.ResultHits + totalMisses := cacheStats.EmbeddingMisses + cacheStats.ResultMisses + + writeJSON(w, map[string]any{ + "enabled": true, + "queries": map[string]any{ + "total": totalQueries, + "hubOnly": 0, + "hybrid": 0, + "onDemand": 0, + "graph": 0, + }, + "latency": map[string]any{ + "avg": "0ms", + "p50": "0ms", + "p95": "0ms", + "p99": "0ms", + "avgHub": "0ms", + "avgRecompute": "0ms", + }, + "storage": map[string]any{ + "totalDocuments": count, + "hubDocuments": 0, + "storedEmbeddings": count, + "savingsPercent": 0.0, + "recomputedTotal": 0, + }, + "cache": map[string]any{ + "hits": totalHits, + "misses": totalMisses, + "hitRate": cacheStats.HitRate(), + "size": cacheSize, + "maxSize": cacheMax, + }, + "graph": map[string]any{ + "traversals": 0, + "avgDepth": 0.0, + }, + "uptime": uptime, + }) +} diff --git a/internal/worker/handlers_update.go b/internal/worker/handlers_update.go index 3f368e5..454287a 100644 --- a/internal/worker/handlers_update.go +++ b/internal/worker/handlers_update.go @@ -4,6 +4,7 @@ package worker import ( "fmt" "net/http" + "strings" "time" "github.com/rs/zerolog/log" @@ -13,7 +14,14 @@ import ( func (s *Service) handleUpdateCheck(w http.ResponseWriter, r *http.Request) { info, err := s.updater.CheckForUpdate(r.Context()) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + // Return a proper JSON response for errors instead of 500 + // This allows the frontend to handle it gracefully + writeJSON(w, map[string]any{ + "available": false, + "current_version": s.version, + "error": err.Error(), + "rate_limited": strings.Contains(err.Error(), "403"), + }) return } writeJSON(w, info) diff --git a/internal/worker/service.go b/internal/worker/service.go index d14a051..6f49ed4 100644 --- a/internal/worker/service.go +++ b/internal/worker/service.go @@ -1198,6 +1198,8 @@ func (s *Service) setupRoutes() { // Vector management endpoints s.router.Post("/api/vectors/rebuild", s.handleTriggerVectorRebuild) s.router.Get("/api/vectors/health", s.handleVectorHealth) + s.router.Get("/api/vector/metrics", s.handleVectorMetrics) + s.router.Get("/api/graph/stats", s.handleGraphStats) // Readiness check - returns 200 only when fully initialized s.router.Get("/api/ready", s.handleReady) diff --git a/ui/index.html b/ui/index.html index 68b24ff..54221e5 100644 --- a/ui/index.html +++ b/ui/index.html @@ -4,7 +4,6 @@ Claude Mnemonic Dashboard -
diff --git a/ui/package-lock.json b/ui/package-lock.json index 9207c57..19a6d07 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-mnemonic-dashboard", - "version": "v0.10.5-1-g7ab4b07-dirty", + "version": "v0.10.5-15-g385d05a-dirty", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-mnemonic-dashboard", - "version": "v0.10.5-1-g7ab4b07-dirty", + "version": "v0.10.5-15-g385d05a-dirty", "dependencies": { "vis-data": "^7.1.9", "vis-network": "^9.1.9", diff --git a/ui/package.json b/ui/package.json index 21b0206..ff8d721 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "claude-mnemonic-dashboard", - "version": "v0.10.5-1-g7ab4b07-dirty", + "version": "v0.10.5-15-g385d05a-dirty", "private": true, "type": "module", "scripts": { diff --git a/ui/src/main.ts b/ui/src/main.ts index 7ebb8d9..7af34da 100644 --- a/ui/src/main.ts +++ b/ui/src/main.ts @@ -1,5 +1,6 @@ import { createApp } from 'vue' import App from './App.vue' import './assets/main.css' +import '@fortawesome/fontawesome-free/css/all.min.css' createApp(App).mount('#app') diff --git a/ui/src/utils/api.ts b/ui/src/utils/api.ts index 98bb7d3..cf8d6b8 100644 --- a/ui/src/utils/api.ts +++ b/ui/src/utils/api.ts @@ -89,10 +89,19 @@ async function fetchWithRetry(url: string, options: FetchOptions = {}): Promi throw lastError! } +interface ObservationsResponse { + observations: Observation[] + total: number + limit: number + offset: number + hasMore: boolean +} + export async function fetchObservations(limit: number = 100, project?: string, signal?: AbortSignal): Promise { const params = new URLSearchParams({ limit: String(limit) }) if (project) params.append('project', project) - return fetchWithRetry(`${API_BASE}/observations?${params}`, { signal }) + const response = await fetchWithRetry(`${API_BASE}/observations?${params}`, { signal }) + return response.observations || [] } export async function fetchPrompts(limit: number = 100, project?: string, signal?: AbortSignal): Promise {