mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-05 23:03:55 +00:00
Fix autoupdate, add healtcheck status to the dashboard
This commit is contained in:
@@ -132,3 +132,8 @@ func (s *Store) QueryRowContext(ctx context.Context, query string, args ...inter
|
||||
}
|
||||
return stmt.QueryRowContext(ctx, args...)
|
||||
}
|
||||
|
||||
// Ping checks if the database connection is alive.
|
||||
func (s *Store) Ping() error {
|
||||
return s.db.Ping()
|
||||
}
|
||||
|
||||
@@ -573,21 +573,30 @@ func (u *Updater) Restart() error {
|
||||
|
||||
log.Info().Str("path", workerPath).Msg("Restarting worker with new binary")
|
||||
|
||||
// Start the new process
|
||||
cmd := exec.Command(workerPath) // #nosec G204 -- workerPath is from internal installDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = os.Environ()
|
||||
// Use nohup to start a detached process that survives parent exit
|
||||
// The new worker will retry binding to the port after the old process exits
|
||||
cmd := exec.Command("nohup", workerPath) // #nosec G204 -- workerPath is from internal installDir
|
||||
cmd.Stdout = nil // Detach stdout
|
||||
cmd.Stderr = nil // Detach stderr
|
||||
cmd.Stdin = nil // Detach stdin
|
||||
cmd.Env = append(os.Environ(), "CLAUDE_MNEMONIC_RESTART=1")
|
||||
|
||||
// Start in background - don't wait
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start new worker: %w", err)
|
||||
}
|
||||
|
||||
// Give the new process time to start
|
||||
time.Sleep(RestartDelay)
|
||||
// Release the child process so it's not a zombie
|
||||
go func() {
|
||||
_ = cmd.Wait()
|
||||
}()
|
||||
|
||||
// Exit current process - the new one is now running
|
||||
log.Info().Int("new_pid", cmd.Process.Pid).Msg("New worker started, exiting old process")
|
||||
|
||||
// Give a moment for the log to flush
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Exit current process - the new one will bind to the port
|
||||
os.Exit(0)
|
||||
|
||||
return nil // Never reached
|
||||
|
||||
@@ -449,6 +449,13 @@ func (c *Client) nextID() int {
|
||||
return c.requestID
|
||||
}
|
||||
|
||||
// IsConnected returns whether the client is currently connected to ChromaDB.
|
||||
func (c *Client) IsConnected() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.connected
|
||||
}
|
||||
|
||||
// Close closes the connection to ChromaDB.
|
||||
func (c *Client) Close() error {
|
||||
c.mu.Lock()
|
||||
|
||||
@@ -751,6 +751,110 @@ func (s *Service) handleUpdateStatus(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, status)
|
||||
}
|
||||
|
||||
// ComponentHealth represents the health status of a single component.
|
||||
type ComponentHealth struct {
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"` // "healthy", "degraded", "unhealthy"
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// SelfCheckResponse contains the health status of all components.
|
||||
type SelfCheckResponse struct {
|
||||
Overall string `json:"overall"` // "healthy", "degraded", "unhealthy"
|
||||
Version string `json:"version"`
|
||||
Uptime string `json:"uptime"`
|
||||
Components []ComponentHealth `json:"components"`
|
||||
}
|
||||
|
||||
// handleSelfCheck returns the health status of all components.
|
||||
func (s *Service) handleSelfCheck(w http.ResponseWriter, r *http.Request) {
|
||||
components := []ComponentHealth{}
|
||||
overall := "healthy"
|
||||
|
||||
// Check Worker Service
|
||||
workerStatus := ComponentHealth{Name: "Worker Service", Status: "healthy"}
|
||||
if !s.ready.Load() {
|
||||
if err := s.GetInitError(); err != nil {
|
||||
workerStatus.Status = "unhealthy"
|
||||
workerStatus.Message = err.Error()
|
||||
overall = "unhealthy"
|
||||
} else {
|
||||
workerStatus.Status = "degraded"
|
||||
workerStatus.Message = "Initializing"
|
||||
if overall == "healthy" {
|
||||
overall = "degraded"
|
||||
}
|
||||
}
|
||||
}
|
||||
components = append(components, workerStatus)
|
||||
|
||||
// Check SQLite Database
|
||||
dbStatus := ComponentHealth{Name: "SQLite Database", Status: "healthy"}
|
||||
if s.store == nil {
|
||||
dbStatus.Status = "unhealthy"
|
||||
dbStatus.Message = "Not initialized"
|
||||
overall = "unhealthy"
|
||||
} else if err := s.store.Ping(); err != nil {
|
||||
dbStatus.Status = "unhealthy"
|
||||
dbStatus.Message = err.Error()
|
||||
overall = "unhealthy"
|
||||
}
|
||||
components = append(components, dbStatus)
|
||||
|
||||
// Check ChromaDB
|
||||
chromaStatus := ComponentHealth{Name: "ChromaDB", Status: "healthy"}
|
||||
if s.chromaClient == nil {
|
||||
chromaStatus.Status = "degraded"
|
||||
chromaStatus.Message = "Not configured"
|
||||
if overall == "healthy" {
|
||||
overall = "degraded"
|
||||
}
|
||||
} else if !s.chromaClient.IsConnected() {
|
||||
chromaStatus.Status = "degraded"
|
||||
chromaStatus.Message = "Not connected"
|
||||
if overall == "healthy" {
|
||||
overall = "degraded"
|
||||
}
|
||||
}
|
||||
components = append(components, chromaStatus)
|
||||
|
||||
// Check SDK Processor
|
||||
sdkStatus := ComponentHealth{Name: "SDK Processor", Status: "healthy"}
|
||||
if s.processor == nil {
|
||||
sdkStatus.Status = "degraded"
|
||||
sdkStatus.Message = "Not initialized"
|
||||
if overall == "healthy" {
|
||||
overall = "degraded"
|
||||
}
|
||||
} else if !s.processor.IsAvailable() {
|
||||
sdkStatus.Status = "degraded"
|
||||
sdkStatus.Message = "Claude CLI not available"
|
||||
if overall == "healthy" {
|
||||
overall = "degraded"
|
||||
}
|
||||
}
|
||||
components = append(components, sdkStatus)
|
||||
|
||||
// Check SSE Broadcaster
|
||||
sseStatus := ComponentHealth{Name: "SSE Broadcaster", Status: "healthy"}
|
||||
if s.sseBroadcaster == nil {
|
||||
sseStatus.Status = "unhealthy"
|
||||
sseStatus.Message = "Not initialized"
|
||||
overall = "unhealthy"
|
||||
}
|
||||
components = append(components, sseStatus)
|
||||
|
||||
// Calculate uptime
|
||||
uptime := time.Since(s.startTime).Round(time.Second).String()
|
||||
|
||||
writeJSON(w, SelfCheckResponse{
|
||||
Overall: overall,
|
||||
Version: s.version,
|
||||
Uptime: uptime,
|
||||
Components: components,
|
||||
})
|
||||
}
|
||||
|
||||
// handleUpdateRestart restarts the worker with the new binary.
|
||||
func (s *Service) handleUpdateRestart(w http.ResponseWriter, r *http.Request) {
|
||||
status := s.updater.GetStatus()
|
||||
|
||||
@@ -92,6 +92,12 @@ func NewProcessor(observationStore *sqlite.ObservationStore, summaryStore *sqlit
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IsAvailable checks if the Claude CLI is available for processing.
|
||||
func (p *Processor) IsAvailable() bool {
|
||||
_, err := os.Stat(p.claudePath)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// ProcessObservation processes a single tool observation and extracts insights.
|
||||
func (p *Processor) ProcessObservation(ctx context.Context, sdkSessionID, project string, toolName string, toolInput, toolResponse interface{}, promptNumber int, cwd string) error {
|
||||
p.mu.Lock()
|
||||
|
||||
@@ -602,6 +602,9 @@ func (s *Service) setupRoutes() {
|
||||
s.router.Get("/api/update/status", s.handleUpdateStatus)
|
||||
s.router.Post("/api/update/restart", s.handleUpdateRestart)
|
||||
|
||||
// Selfcheck endpoint (works before DB is ready - checks all components)
|
||||
s.router.Get("/api/selfcheck", s.handleSelfCheck)
|
||||
|
||||
// SSE endpoint (works before DB is ready)
|
||||
s.router.Get("/api/events", s.sseBroadcaster.HandleSSE)
|
||||
|
||||
@@ -668,11 +671,34 @@ func (s *Service) Start() error {
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
// Check if we're in restart mode (after update)
|
||||
isRestart := os.Getenv("CLAUDE_MNEMONIC_RESTART") == "1"
|
||||
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
if err := s.server.ListenAndServe(); err != http.ErrServerClosed {
|
||||
log.Error().Err(err).Msg("HTTP server error")
|
||||
|
||||
var lastErr error
|
||||
maxRetries := 1
|
||||
if isRestart {
|
||||
maxRetries = 10 // Retry up to 10 times during restart
|
||||
}
|
||||
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
lastErr = s.server.ListenAndServe()
|
||||
if lastErr == http.ErrServerClosed {
|
||||
return // Normal shutdown
|
||||
}
|
||||
|
||||
if i < maxRetries-1 && isRestart {
|
||||
log.Warn().Err(lastErr).Int("retry", i+1).Msg("Port not ready, retrying...")
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
log.Error().Err(lastErr).Msg("HTTP server error")
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -681,6 +707,7 @@ func (s *Service) Start() error {
|
||||
log.Info().
|
||||
Int("port", port).
|
||||
Int("pid", getPID()).
|
||||
Bool("restart_mode", isRestart).
|
||||
Msg("Worker HTTP server started (initialization in progress)")
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<!-- Placeholder for go:embed - replaced by UI build -->
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue'
|
||||
import { useSSE, useStats, useTimeline, useUpdate } from '@/composables'
|
||||
import { useSSE, useStats, useTimeline, useUpdate, useHealth } from '@/composables'
|
||||
import Header from '@/components/Header.vue'
|
||||
import StatsCards from '@/components/StatsCards.vue'
|
||||
import FilterTabs from '@/components/FilterTabs.vue'
|
||||
@@ -11,6 +11,7 @@ import Sidebar from '@/components/Sidebar.vue'
|
||||
const { isConnected, isProcessing, queueDepth, lastEvent } = useSSE()
|
||||
const { stats } = useStats()
|
||||
const { updateInfo, updateStatus, isUpdating, applyUpdate } = useUpdate()
|
||||
const { health } = useHealth()
|
||||
const {
|
||||
filteredItems,
|
||||
loading,
|
||||
@@ -66,6 +67,7 @@ watch(lastEvent, (event) => {
|
||||
:prompt-count="promptCount"
|
||||
:summary-count="summaryCount"
|
||||
:current-project="currentProject"
|
||||
:health="health"
|
||||
@update:project="setProject"
|
||||
/>
|
||||
|
||||
|
||||
@@ -22,15 +22,35 @@ const restartWorker = async () => {
|
||||
isRestarting.value = true
|
||||
try {
|
||||
await fetch('/api/update/restart', { method: 'POST' })
|
||||
// Wait a moment for the new process to start, then reload
|
||||
setTimeout(() => {
|
||||
globalThis.location.reload()
|
||||
}, 2000)
|
||||
// Poll for new worker to be ready before reloading
|
||||
await waitForWorker()
|
||||
globalThis.location.reload()
|
||||
} catch (error) {
|
||||
console.error('Failed to restart:', error)
|
||||
isRestarting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Poll health endpoint until worker is ready
|
||||
const waitForWorker = async (maxAttempts = 30, delayMs = 500): Promise<void> => {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs))
|
||||
try {
|
||||
const response = await fetch('/api/health', {
|
||||
signal: AbortSignal.timeout(2000)
|
||||
})
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
if (data.status === 'ready') {
|
||||
return // Worker is ready
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Worker not ready yet, continue polling
|
||||
}
|
||||
}
|
||||
// Timeout - reload anyway
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
+232
-116
@@ -1,148 +1,264 @@
|
||||
<script setup lang="ts">
|
||||
import type { Stats } from '@/types'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Stats, SelfCheckResponse } from '@/types'
|
||||
import ProjectFilter from './ProjectFilter.vue'
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
stats: Stats | null
|
||||
observationCount: number
|
||||
promptCount: number
|
||||
summaryCount: number
|
||||
currentProject: string | null
|
||||
health: SelfCheckResponse | null
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
'update:project': [project: string | null]
|
||||
}>()
|
||||
|
||||
// Collapse state - persisted in localStorage
|
||||
const isCollapsed = ref(localStorage.getItem('sidebar-collapsed') === 'true')
|
||||
|
||||
function toggleCollapse() {
|
||||
isCollapsed.value = !isCollapsed.value
|
||||
localStorage.setItem('sidebar-collapsed', String(isCollapsed.value))
|
||||
}
|
||||
|
||||
function formatNumber(n: number): string {
|
||||
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'
|
||||
if (n >= 1000) return (n / 1000).toFixed(1) + 'K'
|
||||
return n.toString()
|
||||
}
|
||||
|
||||
// Health status helpers
|
||||
const overallHealthIcon = computed(() => {
|
||||
if (!props.health) return 'fa-circle-question'
|
||||
switch (props.health.overall) {
|
||||
case 'healthy': return 'fa-circle-check'
|
||||
case 'degraded': return 'fa-triangle-exclamation'
|
||||
case 'unhealthy': return 'fa-circle-xmark'
|
||||
}
|
||||
})
|
||||
|
||||
const overallHealthColor = computed(() => {
|
||||
if (!props.health) return 'text-slate-400'
|
||||
switch (props.health.overall) {
|
||||
case 'healthy': return 'text-green-400'
|
||||
case 'degraded': return 'text-amber-400'
|
||||
case 'unhealthy': return 'text-red-400'
|
||||
}
|
||||
})
|
||||
|
||||
function getStatusColor(status: string): string {
|
||||
switch (status) {
|
||||
case 'healthy': return 'text-green-400'
|
||||
case 'degraded': return 'text-amber-400'
|
||||
case 'unhealthy': return 'text-red-400'
|
||||
default: return 'text-slate-400'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="w-72 flex-shrink-0 space-y-4">
|
||||
<!-- Project Filter -->
|
||||
<div class="bg-slate-800/50 rounded-lg p-4 border border-slate-700/50">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<i class="fas fa-filter text-claude-400" />
|
||||
<h3 class="text-sm font-semibold text-white">Filter by Project</h3>
|
||||
</div>
|
||||
<ProjectFilter
|
||||
:current-project="currentProject"
|
||||
@update:project="$emit('update:project', $event)"
|
||||
<aside
|
||||
:class="[
|
||||
'flex-shrink-0 transition-all duration-300 ease-in-out',
|
||||
isCollapsed ? 'w-12' : 'w-72'
|
||||
]"
|
||||
>
|
||||
<!-- Collapse Toggle Button -->
|
||||
<button
|
||||
@click="toggleCollapse"
|
||||
class="w-full flex items-center justify-center py-2 mb-4 bg-slate-800/50 rounded-lg border border-slate-700/50 hover:bg-slate-700/50 transition-colors"
|
||||
:title="isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'"
|
||||
>
|
||||
<i
|
||||
:class="[
|
||||
'fas transition-transform duration-300',
|
||||
isCollapsed ? 'fa-chevron-right' : 'fa-chevron-left'
|
||||
]"
|
||||
class="text-slate-400"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div v-show="!isCollapsed" class="space-y-4">
|
||||
<!-- Project Filter -->
|
||||
<div class="bg-slate-800/50 rounded-lg p-4 border border-slate-700/50">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<i class="fas fa-filter text-claude-400" />
|
||||
<h3 class="text-sm font-semibold text-white">Filter by Project</h3>
|
||||
</div>
|
||||
<ProjectFilter
|
||||
:current-project="currentProject"
|
||||
@update:project="$emit('update:project', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Component Health -->
|
||||
<div class="bg-slate-800/50 rounded-lg p-4 border border-slate-700/50">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<i :class="['fas', overallHealthIcon, overallHealthColor]" />
|
||||
<h3 class="text-sm font-semibold text-white">System Health</h3>
|
||||
</div>
|
||||
|
||||
<div v-if="health" class="space-y-2">
|
||||
<div
|
||||
v-for="component in health.components"
|
||||
:key="component.name"
|
||||
class="flex items-center justify-between"
|
||||
>
|
||||
<span class="text-slate-400 text-sm truncate" :title="component.message">
|
||||
{{ component.name }}
|
||||
</span>
|
||||
<span
|
||||
:class="['text-xs font-medium capitalize', getStatusColor(component.status)]"
|
||||
>
|
||||
{{ component.status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-slate-500 text-sm">
|
||||
Loading health status...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Memory Stats -->
|
||||
<div class="bg-slate-800/50 rounded-lg p-4 border border-slate-700/50">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<i class="fas fa-brain text-purple-400" />
|
||||
<h3 class="text-sm font-semibold text-white">Memory Contents</h3>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- Observations -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fas fa-lightbulb text-amber-400 w-4" />
|
||||
<span class="text-slate-400 text-sm">Observations</span>
|
||||
</div>
|
||||
<span class="text-white font-medium">{{ formatNumber(observationCount) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Prompts -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fas fa-comment text-blue-400 w-4" />
|
||||
<span class="text-slate-400 text-sm">Prompts</span>
|
||||
</div>
|
||||
<span class="text-white font-medium">{{ formatNumber(promptCount) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Summaries -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fas fa-clipboard-list text-green-400 w-4" />
|
||||
<span class="text-slate-400 text-sm">Summaries</span>
|
||||
</div>
|
||||
<span class="text-white font-medium">{{ formatNumber(summaryCount) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Retrieval Stats -->
|
||||
<div v-if="stats?.retrieval" class="bg-slate-800/50 rounded-lg p-4 border border-slate-700/50">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<i class="fas fa-search text-cyan-400" />
|
||||
<h3 class="text-sm font-semibold text-white">Retrieval Stats</h3>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- Total Requests -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Total Requests</span>
|
||||
<span class="text-white font-medium">{{ formatNumber(stats.retrieval.TotalRequests) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Observations Served -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Obs Served</span>
|
||||
<span class="text-white font-medium">{{ formatNumber(stats.retrieval.ObservationsServed) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Search Requests -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Searches</span>
|
||||
<span class="text-white font-medium">{{ formatNumber(stats.retrieval.SearchRequests) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Context Injections -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Injections</span>
|
||||
<span class="text-white font-medium">{{ formatNumber(stats.retrieval.ContextInjections) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Verified Stale -->
|
||||
<div v-if="stats.retrieval.VerifiedStale > 0" class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Verified Stale</span>
|
||||
<span class="text-amber-400 font-medium">{{ formatNumber(stats.retrieval.VerifiedStale) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Deleted Invalid -->
|
||||
<div v-if="stats.retrieval.DeletedInvalid > 0" class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Deleted Invalid</span>
|
||||
<span class="text-red-400 font-medium">{{ formatNumber(stats.retrieval.DeletedInvalid) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<i class="fas fa-clock text-slate-400" />
|
||||
<h3 class="text-sm font-semibold text-white">Worker Info</h3>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- Uptime -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Uptime</span>
|
||||
<span class="text-white font-medium text-xs">{{ stats.uptime }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Sessions Today -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Sessions Today</span>
|
||||
<span class="text-white font-medium">{{ stats.sessionsToday }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Connected Clients -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Connected Clients</span>
|
||||
<span class="text-white font-medium">{{ stats.connectedClients }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Memory Stats -->
|
||||
<div class="bg-slate-800/50 rounded-lg p-4 border border-slate-700/50">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<!-- Collapsed State - Show icons only -->
|
||||
<div v-show="isCollapsed" class="space-y-2">
|
||||
<!-- Health indicator -->
|
||||
<div
|
||||
class="bg-slate-800/50 rounded-lg p-3 border border-slate-700/50 flex justify-center"
|
||||
:title="`System: ${health?.overall || 'Unknown'}`"
|
||||
>
|
||||
<i :class="['fas', overallHealthIcon, overallHealthColor]" />
|
||||
</div>
|
||||
|
||||
<!-- Memory indicator -->
|
||||
<div
|
||||
class="bg-slate-800/50 rounded-lg p-3 border border-slate-700/50 flex justify-center"
|
||||
:title="`${observationCount} observations, ${promptCount} prompts`"
|
||||
>
|
||||
<i class="fas fa-brain text-purple-400" />
|
||||
<h3 class="text-sm font-semibold text-white">Memory Contents</h3>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- Observations -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fas fa-lightbulb text-amber-400 w-4" />
|
||||
<span class="text-slate-400 text-sm">Observations</span>
|
||||
</div>
|
||||
<span class="text-white font-medium">{{ formatNumber(observationCount) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Prompts -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fas fa-comment text-blue-400 w-4" />
|
||||
<span class="text-slate-400 text-sm">Prompts</span>
|
||||
</div>
|
||||
<span class="text-white font-medium">{{ formatNumber(promptCount) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Summaries -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fas fa-clipboard-list text-green-400 w-4" />
|
||||
<span class="text-slate-400 text-sm">Summaries</span>
|
||||
</div>
|
||||
<span class="text-white font-medium">{{ formatNumber(summaryCount) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Retrieval Stats -->
|
||||
<div v-if="stats?.retrieval" class="bg-slate-800/50 rounded-lg p-4 border border-slate-700/50">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<!-- Stats indicator -->
|
||||
<div
|
||||
v-if="stats?.retrieval"
|
||||
class="bg-slate-800/50 rounded-lg p-3 border border-slate-700/50 flex justify-center"
|
||||
:title="`${stats.retrieval.TotalRequests} total requests`"
|
||||
>
|
||||
<i class="fas fa-search text-cyan-400" />
|
||||
<h3 class="text-sm font-semibold text-white">Retrieval Stats</h3>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- Total Requests -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Total Requests</span>
|
||||
<span class="text-white font-medium">{{ formatNumber(stats.retrieval.TotalRequests) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Observations Served -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Obs Served</span>
|
||||
<span class="text-white font-medium">{{ formatNumber(stats.retrieval.ObservationsServed) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Search Requests -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Searches</span>
|
||||
<span class="text-white font-medium">{{ formatNumber(stats.retrieval.SearchRequests) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Context Injections -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Injections</span>
|
||||
<span class="text-white font-medium">{{ formatNumber(stats.retrieval.ContextInjections) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Verified Stale -->
|
||||
<div v-if="stats.retrieval.VerifiedStale > 0" class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Verified Stale</span>
|
||||
<span class="text-amber-400 font-medium">{{ formatNumber(stats.retrieval.VerifiedStale) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Deleted Invalid -->
|
||||
<div v-if="stats.retrieval.DeletedInvalid > 0" class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Deleted Invalid</span>
|
||||
<span class="text-red-400 font-medium">{{ formatNumber(stats.retrieval.DeletedInvalid) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<i class="fas fa-clock text-slate-400" />
|
||||
<h3 class="text-sm font-semibold text-white">Worker Info</h3>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- Uptime -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Uptime</span>
|
||||
<span class="text-white font-medium text-xs">{{ stats.uptime }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Sessions Today -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Sessions Today</span>
|
||||
<span class="text-white font-medium">{{ stats.sessionsToday }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Connected Clients -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Connected Clients</span>
|
||||
<span class="text-white font-medium">{{ stats.connectedClients }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -2,3 +2,4 @@ export { useSSE } from './useSSE'
|
||||
export { useStats } from './useStats'
|
||||
export { useTimeline } from './useTimeline'
|
||||
export { useUpdate } from './useUpdate'
|
||||
export { useHealth } from './useHealth'
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import type { SelfCheckResponse } from '@/types'
|
||||
|
||||
const CHECK_INTERVAL = 30 * 1000 // 30 seconds
|
||||
|
||||
export function useHealth() {
|
||||
const health = ref<SelfCheckResponse | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
let intervalId: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
async function fetchHealth() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await fetch('/api/selfcheck')
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
}
|
||||
health.value = await response.json()
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Unknown error'
|
||||
health.value = null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
fetchHealth()
|
||||
intervalId = setInterval(fetchHealth, CHECK_INTERVAL)
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId)
|
||||
intervalId = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
startPolling()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopPolling()
|
||||
})
|
||||
|
||||
return {
|
||||
health,
|
||||
loading,
|
||||
error,
|
||||
refresh: fetchHealth
|
||||
}
|
||||
}
|
||||
@@ -50,3 +50,16 @@ export interface SSEEvent {
|
||||
}
|
||||
|
||||
export type FilterType = 'all' | 'observations' | 'summaries' | 'prompts'
|
||||
|
||||
export interface ComponentHealth {
|
||||
name: string
|
||||
status: 'healthy' | 'degraded' | 'unhealthy'
|
||||
message?: string
|
||||
}
|
||||
|
||||
export interface SelfCheckResponse {
|
||||
overall: 'healthy' | 'degraded' | 'unhealthy'
|
||||
version: string
|
||||
uptime: string
|
||||
components: ComponentHealth[]
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/components/index.ts","./src/composables/index.ts","./src/composables/usesse.ts","./src/composables/usestats.ts","./src/composables/usetimeline.ts","./src/composables/useupdate.ts","./src/types/api.ts","./src/types/index.ts","./src/types/observation.ts","./src/types/prompt.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/sidebar.vue","./src/components/statscards.vue","./src/components/summarycard.vue","./src/components/timeline.vue"],"version":"5.7.3"}
|
||||
{"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/useupdate.ts","./src/types/api.ts","./src/types/index.ts","./src/types/observation.ts","./src/types/prompt.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/sidebar.vue","./src/components/statscards.vue","./src/components/summarycard.vue","./src/components/timeline.vue"],"version":"5.7.3"}
|
||||
Reference in New Issue
Block a user