Merge origin/main into mnemonic-ralphised

Resolved conflicts in:
- internal/config/config.go: Combined new bool fields with maintenance settings
- internal/db/gorm/models.go: Merged field reordering with archival fields
- internal/mcp/server.go: Merged field reordering with maintenanceService
- internal/search/manager.go: Updated Metadata field to use 'any' type
- internal/worker/handlers.go: Kept HEAD version
- internal/worker/sdk/processor.go: Combined circuit breaker/dedup with claude path
- internal/worker/service.go: Kept HEAD version
- ui/src/components/Sidebar.vue: Combined new components with useGraphMetrics
- ui/src/utils/api.ts: Combined new APIs with graph/vector metrics
This commit is contained in:
2026-01-11 01:02:25 +00:00
88 changed files with 5333 additions and 629 deletions
+127
View File
@@ -5,6 +5,7 @@ import ProjectFilter from './ProjectFilter.vue'
import SearchAnalytics from './SearchAnalytics.vue'
import SystemHealthDetails from './SystemHealthDetails.vue'
import TopObservations from './TopObservations.vue'
import { useGraphMetrics } from '@/composables'
const props = defineProps<{
stats: Stats | null
@@ -21,6 +22,10 @@ defineEmits<{
// Collapse state - persisted in localStorage
const isCollapsed = ref(localStorage.getItem('sidebar-collapsed') === 'true')
const metricsExpanded = ref(localStorage.getItem('metrics-expanded') === 'true')
// Graph metrics composable
const { graphStats, vectorMetrics, loading: metricsLoading, refresh: refreshMetrics } = useGraphMetrics()
// Search Analytics modal state
const showSearchAnalytics = ref(false)
@@ -36,6 +41,11 @@ function toggleCollapse() {
localStorage.setItem('sidebar-collapsed', String(isCollapsed.value))
}
function toggleMetrics() {
metricsExpanded.value = !metricsExpanded.value
localStorage.setItem('metrics-expanded', String(metricsExpanded.value))
}
function formatNumber(n: number): string {
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'
if (n >= 1000) return (n / 1000).toFixed(1) + 'K'
@@ -244,6 +254,99 @@ function getStatusColor(status: string): string {
</div>
</div>
<!-- Advanced Metrics -->
<div class="bg-slate-800/50 rounded-lg border border-slate-700/50">
<button
@click="toggleMetrics"
class="w-full flex items-center justify-between p-4 hover:bg-slate-700/30 transition-colors rounded-lg"
>
<div class="flex items-center gap-2">
<i class="fas fa-chart-line text-violet-400" />
<h3 class="text-sm font-semibold text-white">Advanced Metrics</h3>
</div>
<i
:class="[
'fas text-slate-400 transition-transform duration-200',
metricsExpanded ? 'fa-chevron-up' : 'fa-chevron-down'
]"
/>
</button>
<Transition name="expand">
<div v-show="metricsExpanded" class="px-4 pb-4 space-y-4">
<!-- Loading State -->
<div v-if="metricsLoading" class="text-center py-4">
<i class="fas fa-spinner fa-spin text-slate-400" />
<p class="text-slate-500 text-sm mt-2">Loading metrics...</p>
</div>
<!-- Graph Stats -->
<div v-else-if="graphStats?.enabled">
<div class="flex items-center justify-between mb-2">
<span class="text-xs text-slate-400 uppercase tracking-wide">Graph</span>
<button
@click="refreshMetrics"
class="text-xs text-violet-400 hover:text-violet-300 transition-colors"
title="Refresh metrics"
>
<i class="fas fa-sync-alt" />
</button>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between">
<span class="text-slate-400 text-sm">Nodes</span>
<span class="text-white font-medium">{{ formatNumber(graphStats.nodeCount) }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-slate-400 text-sm">Edges</span>
<span class="text-white font-medium">{{ formatNumber(graphStats.edgeCount) }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-slate-400 text-sm">Avg Degree</span>
<span class="text-white font-medium">{{ graphStats.avgDegree.toFixed(1) }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-slate-400 text-sm">Max Degree</span>
<span class="text-white font-medium">{{ graphStats.maxDegree }}</span>
</div>
</div>
<!-- Vector Metrics -->
<div v-if="vectorMetrics?.enabled" class="mt-4 pt-4 border-t border-slate-700/50">
<div class="text-xs text-slate-400 uppercase tracking-wide mb-2">Vector Storage</div>
<div class="space-y-2">
<div class="flex items-center justify-between">
<span class="text-slate-400 text-sm">Savings</span>
<span class="text-green-400 font-medium">
{{ vectorMetrics.storage.savingsPercent.toFixed(1) }}%
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-slate-400 text-sm">Queries</span>
<span class="text-white font-medium">{{ formatNumber(vectorMetrics.queries.total) }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-slate-400 text-sm">Cache Hit</span>
<span class="text-cyan-400 font-medium">
{{ (vectorMetrics.cache.hitRate * 100).toFixed(1) }}%
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-slate-400 text-sm">Avg Latency</span>
<span class="text-white font-medium text-xs">{{ vectorMetrics.latency.avg }}</span>
</div>
</div>
</div>
</div>
<!-- Disabled State -->
<div v-else class="text-slate-500 text-sm py-2">
{{ graphStats?.message || 'Metrics not available' }}
</div>
</div>
</Transition>
</div>
<!-- Session Info -->
<div v-if="stats" class="bg-slate-800/50 rounded-lg p-4 border border-slate-700/50">
<div class="flex items-center gap-2 mb-3">
@@ -299,6 +402,15 @@ function getStatusColor(status: string): string {
>
<i class="fas fa-search text-cyan-400" />
</div>
<!-- Metrics indicator -->
<div
v-if="graphStats?.enabled"
class="bg-slate-800/50 rounded-lg p-3 border border-slate-700/50 flex justify-center"
:title="`${graphStats.nodeCount} nodes, ${graphStats.edgeCount} edges`"
>
<i class="fas fa-chart-line text-violet-400" />
</div>
</div>
<!-- Search Analytics Modal -->
@@ -322,3 +434,18 @@ function getStatusColor(status: string): string {
/>
</aside>
</template>
<style scoped>
.expand-enter-active,
.expand-leave-active {
transition: all 0.3s ease;
overflow: hidden;
max-height: 500px;
}
.expand-enter-from,
.expand-leave-to {
max-height: 0;
opacity: 0;
}
</style>
+1
View File
@@ -3,3 +3,4 @@ export { useStats } from './useStats'
export { useTimeline } from './useTimeline'
export { useUpdate } from './useUpdate'
export { useHealth } from './useHealth'
export { useGraphMetrics } from './useGraphMetrics'
+43
View File
@@ -0,0 +1,43 @@
import { ref, onMounted } from 'vue'
import type { GraphStats, VectorMetrics } from '@/types'
import { fetchGraphStats, fetchVectorMetrics } from '@/utils/api'
export function useGraphMetrics() {
const graphStats = ref<GraphStats | null>(null)
const vectorMetrics = ref<VectorMetrics | null>(null)
const loading = ref(false)
const error = ref<string | null>(null)
const refresh = async () => {
loading.value = true
error.value = null
try {
// Fetch both in parallel
const [graph, vector] = await Promise.all([
fetchGraphStats(),
fetchVectorMetrics()
])
graphStats.value = graph
vectorMetrics.value = vector
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to fetch metrics'
console.error('[GraphMetrics] Error:', err)
} finally {
loading.value = false
}
}
onMounted(() => {
refresh()
})
return {
graphStats,
vectorMetrics,
loading,
error,
refresh
}
}
+55
View File
@@ -63,3 +63,58 @@ export interface SelfCheckResponse {
uptime: string
components: ComponentHealth[]
}
export interface GraphStats {
enabled: boolean
nodeCount: number
edgeCount: number
avgDegree: number
maxDegree: number
minDegree: number
medianDegree: number
edgeTypes: Record<string, number>
config: {
maxHops: number
branchFactor: number
edgeWeight: number
rebuildIntervalMin: number
}
message?: string
}
export interface VectorMetrics {
enabled: boolean
queries: {
total: number
hubOnly: number
hybrid: number
onDemand: number
graph: number
}
latency: {
avg: string
p50: string
p95: string
p99: string
avgHub: string
avgRecompute: string
}
storage: {
totalDocuments: number
hubDocuments: number
storedEmbeddings: number
savingsPercent: number
recomputedTotal: number
}
cache: {
hits: number
misses: number
hitRate: number
}
graph: {
traversals: number
avgDepth: number
}
uptime: string
message?: string
}
+9 -1
View File
@@ -1,4 +1,4 @@
import type { Observation, UserPrompt, SessionSummary, Stats, FeedItem, ObservationFeedItem, PromptFeedItem, SummaryFeedItem, RelationWithDetails, RelationGraph, RelationStats } from '@/types'
import type { Observation, UserPrompt, SessionSummary, Stats, FeedItem, ObservationFeedItem, PromptFeedItem, SummaryFeedItem, RelationWithDetails, RelationGraph, RelationStats, GraphStats, VectorMetrics } from '@/types'
const API_BASE = '/api'
const DEFAULT_TIMEOUT = 10000 // 10 seconds
@@ -280,3 +280,11 @@ export interface SystemHealth {
export async function fetchSystemHealth(signal?: AbortSignal): Promise<SystemHealth> {
return fetchWithRetry<SystemHealth>(`${API_BASE}/selfcheck`, { signal })
}
export async function fetchGraphStats(signal?: AbortSignal): Promise<GraphStats> {
return fetchWithRetry<GraphStats>(`${API_BASE}/graph/stats`, { signal })
}
export async function fetchVectorMetrics(signal?: AbortSignal): Promise<VectorMetrics> {
return fetchWithRetry<VectorMetrics>(`${API_BASE}/vector/metrics`, { signal })
}
+1 -1
View File
@@ -1 +1 @@
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/components/index.ts","./src/composables/index.ts","./src/composables/usehealth.ts","./src/composables/usesse.ts","./src/composables/usestats.ts","./src/composables/usetimeline.ts","./src/composables/usetypes.ts","./src/composables/useupdate.ts","./src/types/api.ts","./src/types/index.ts","./src/types/observation.ts","./src/types/prompt.ts","./src/types/relation.ts","./src/types/summary.ts","./src/utils/api.ts","./src/utils/formatters.ts","./src/app.vue","./src/components/badge.vue","./src/components/card.vue","./src/components/filtertabs.vue","./src/components/header.vue","./src/components/iconbox.vue","./src/components/observationcard.vue","./src/components/projectfilter.vue","./src/components/promptcard.vue","./src/components/relationgraph.vue","./src/components/scorebreakdown.vue","./src/components/searchanalytics.vue","./src/components/sidebar.vue","./src/components/statscards.vue","./src/components/summarycard.vue","./src/components/systemhealthdetails.vue","./src/components/timeline.vue","./src/components/topobservations.vue"],"version":"5.7.3"}
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/components/index.ts","./src/composables/index.ts","./src/composables/usegraphmetrics.ts","./src/composables/usehealth.ts","./src/composables/usesse.ts","./src/composables/usestats.ts","./src/composables/usetimeline.ts","./src/composables/usetypes.ts","./src/composables/useupdate.ts","./src/types/api.ts","./src/types/index.ts","./src/types/observation.ts","./src/types/prompt.ts","./src/types/relation.ts","./src/types/summary.ts","./src/utils/api.ts","./src/utils/formatters.ts","./src/app.vue","./src/components/badge.vue","./src/components/card.vue","./src/components/filtertabs.vue","./src/components/header.vue","./src/components/iconbox.vue","./src/components/observationcard.vue","./src/components/projectfilter.vue","./src/components/promptcard.vue","./src/components/relationgraph.vue","./src/components/scorebreakdown.vue","./src/components/searchanalytics.vue","./src/components/sidebar.vue","./src/components/statscards.vue","./src/components/summarycard.vue","./src/components/systemhealthdetails.vue","./src/components/timeline.vue","./src/components/topobservations.vue"],"version":"5.7.3"}