Fix autoupdate, add healtcheck status to the dashboard

This commit is contained in:
2025-12-15 01:33:49 +00:00
parent 05a5cea5c7
commit 7a6182bb3b
14 changed files with 497 additions and 133 deletions
+5
View File
@@ -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()
}
+17 -8
View File
@@ -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
+7
View File
@@ -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()
+104
View File
@@ -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()
+6
View File
@@ -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()
+29 -2
View File
@@ -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
View File
@@ -1 +0,0 @@
<!-- Placeholder for go:embed - replaced by UI build -->
+3 -1
View File
@@ -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"
/>
+24 -4
View File
@@ -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
View File
@@ -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>
+1
View File
@@ -2,3 +2,4 @@ export { useSSE } from './useSSE'
export { useStats } from './useStats'
export { useTimeline } from './useTimeline'
export { useUpdate } from './useUpdate'
export { useHealth } from './useHealth'
+55
View File
@@ -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
}
}
+13
View File
@@ -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
View File
@@ -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"}