mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-10-19 06:18:13 +00:00
feat(dashboard): add graph stats and vector metrics endpoints
- [x] Add handleGraphStats endpoint for knowledge graph visualization - [x] Add handleVectorMetrics endpoint for vector database dashboard - [x] Improve update check error handling with JSON response - [x] Register new API routes for graph and vector metrics - [x] Migrate Font Awesome to npm package from CDN - [x] Fix observations API response type handling - [x] Update package version to v0.10.5-15-g385d05a
This commit is contained in:
@@ -593,3 +593,119 @@ func (s *Service) handleGetObservationByID(w http.ResponseWriter, r *http.Reques
|
|||||||
|
|
||||||
writeJSON(w, obs)
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package worker
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
@@ -13,7 +14,14 @@ import (
|
|||||||
func (s *Service) handleUpdateCheck(w http.ResponseWriter, r *http.Request) {
|
func (s *Service) handleUpdateCheck(w http.ResponseWriter, r *http.Request) {
|
||||||
info, err := s.updater.CheckForUpdate(r.Context())
|
info, err := s.updater.CheckForUpdate(r.Context())
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
writeJSON(w, info)
|
writeJSON(w, info)
|
||||||
|
|||||||
@@ -1198,6 +1198,8 @@ func (s *Service) setupRoutes() {
|
|||||||
// Vector management endpoints
|
// Vector management endpoints
|
||||||
s.router.Post("/api/vectors/rebuild", s.handleTriggerVectorRebuild)
|
s.router.Post("/api/vectors/rebuild", s.handleTriggerVectorRebuild)
|
||||||
s.router.Get("/api/vectors/health", s.handleVectorHealth)
|
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
|
// Readiness check - returns 200 only when fully initialized
|
||||||
s.router.Get("/api/ready", s.handleReady)
|
s.router.Get("/api/ready", s.handleReady)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Claude Mnemonic Dashboard</title>
|
<title>Claude Mnemonic Dashboard</title>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 min-h-screen text-white">
|
<body class="bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 min-h-screen text-white">
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-mnemonic-dashboard",
|
"name": "claude-mnemonic-dashboard",
|
||||||
"version": "v0.10.5-1-g7ab4b07-dirty",
|
"version": "v0.10.5-15-g385d05a-dirty",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "claude-mnemonic-dashboard",
|
"name": "claude-mnemonic-dashboard",
|
||||||
"version": "v0.10.5-1-g7ab4b07-dirty",
|
"version": "v0.10.5-15-g385d05a-dirty",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vis-data": "^7.1.9",
|
"vis-data": "^7.1.9",
|
||||||
"vis-network": "^9.1.9",
|
"vis-network": "^9.1.9",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-mnemonic-dashboard",
|
"name": "claude-mnemonic-dashboard",
|
||||||
"version": "v0.10.5-1-g7ab4b07-dirty",
|
"version": "v0.10.5-15-g385d05a-dirty",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import './assets/main.css'
|
import './assets/main.css'
|
||||||
|
import '@fortawesome/fontawesome-free/css/all.min.css'
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
createApp(App).mount('#app')
|
||||||
|
|||||||
+10
-1
@@ -89,10 +89,19 @@ async function fetchWithRetry<T>(url: string, options: FetchOptions = {}): Promi
|
|||||||
throw lastError!
|
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<Observation[]> {
|
export async function fetchObservations(limit: number = 100, project?: string, signal?: AbortSignal): Promise<Observation[]> {
|
||||||
const params = new URLSearchParams({ limit: String(limit) })
|
const params = new URLSearchParams({ limit: String(limit) })
|
||||||
if (project) params.append('project', project)
|
if (project) params.append('project', project)
|
||||||
return fetchWithRetry<Observation[]>(`${API_BASE}/observations?${params}`, { signal })
|
const response = await fetchWithRetry<ObservationsResponse>(`${API_BASE}/observations?${params}`, { signal })
|
||||||
|
return response.observations || []
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchPrompts(limit: number = 100, project?: string, signal?: AbortSignal): Promise<UserPrompt[]> {
|
export async function fetchPrompts(limit: number = 100, project?: string, signal?: AbortSignal): Promise<UserPrompt[]> {
|
||||||
|
|||||||
Reference in New Issue
Block a user