mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-05 23:03:55 +00:00
5335a8a7a6
* Make things 'betterer' across the board * fix: reorganize struct fields and config parameters for consistency - [x] Reorder Config struct fields alphabetically and by related functionality - [x] Reorganize Observation model fields with archival fields grouped together - [x] Reorder ObservationStore fields to group related members - [x] Reorder Store struct fields with health check caching grouped - [x] Reorganize HealthInfo and PoolMetrics struct field order - [x] Reorder maintenance Service struct fields logically - [x] Reorganize MCP server handler parameter structs alphabetically - [x] Reorder pattern detector candidate tracking fields - [x] Reorganize search Manager struct fields by functionality - [x] Reorder vector Client struct fields with mutex protections grouped - [x] Reorganize handler request/response struct fields - [x] Update handlers_test.go to expect wrapped response format - [x] Reorder middleware TokenAuth and rate limiter fields - [x] Reorganize Service struct fields with grouped functionality - [x] Fix RateLimiter field ordering for clarity - [x] Reorder CircuitBreaker metrics fields * fix(security): improve JSON output safety and path traversal protection - [x] Replace unsafe JSON string formatting with proper json.Marshal in export handler - [x] Remove escapeJSONString helper function in favor of standard JSON marshaling - [x] Add safeResolvePath function to validate paths and prevent directory traversal - [x] Apply path traversal validation in captureFileMtimes operations - [x] Cap result slice capacity in getRecentSearchQueries to prevent DoS via excessive allocation * fix(sdk): improve path traversal protection and allocation safety - [x] Enhance safeResolvePath with stricter validation using filepath.Rel - [x] Reject paths containing ".." after cleaning to prevent traversal - [x] Validate absolute paths are within cwd when cwd is specified - [x] Apply safeResolvePath validation to GetFileContent for consistency - [x] Add comprehensive test coverage for path traversal protection - [x] Fix allocation safety in getRecentSearchQueries by using constant capacity * 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 * fixup! feat(dashboard): add graph stats and vector metrics endpoints * test: add comprehensive test coverage across multiple packages - [x] Add 298 tests for Python chunker functionality - [x] Add 213 tests for chunking types and constants - [x] Add 398 tests for TypeScript/JavaScript chunker - [x] Add 954 tests for MCP server handlers and validation - [x] Add 563 tests for pattern detector and analysis - [x] Add 1149 tests for vector client cache and operations - [x] Add 663 tests for SDK processor, circuit breaker, and deduplication - [x] Add 731 tests for session manager lifecycle and concurrency - [x] Add 331 tests for similarity clustering and term extraction * fix(pattern): add nil check and fmt import for GetPatternInsight - [x] Add `fmt` import for error formatting - [x] Add nil check for pattern before using it - [x] Remove duplicate comment line
252 lines
6.9 KiB
Go
252 lines
6.9 KiB
Go
// Package worker provides update and restart HTTP handlers.
|
|
package worker
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// handleUpdateCheck checks for available updates.
|
|
func (s *Service) handleUpdateCheck(w http.ResponseWriter, r *http.Request) {
|
|
info, err := s.updater.CheckForUpdate(r.Context())
|
|
if err != nil {
|
|
// 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)
|
|
}
|
|
|
|
// handleUpdateApply downloads and applies an available update.
|
|
func (s *Service) handleUpdateApply(w http.ResponseWriter, r *http.Request) {
|
|
// First check for update
|
|
info, err := s.updater.CheckForUpdate(r.Context())
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if !info.Available {
|
|
writeJSON(w, map[string]any{
|
|
"success": false,
|
|
"message": "No update available",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Apply update in background with tracking for graceful shutdown
|
|
s.wg.Go(func() {
|
|
if err := s.updater.ApplyUpdate(s.ctx, info); err != nil {
|
|
log.Error().Err(err).Msg("Update failed")
|
|
}
|
|
})
|
|
|
|
writeJSON(w, map[string]any{
|
|
"success": true,
|
|
"message": "Update started",
|
|
"version": info.LatestVersion,
|
|
})
|
|
}
|
|
|
|
// handleUpdateStatus returns the current update status.
|
|
func (s *Service) handleUpdateStatus(w http.ResponseWriter, r *http.Request) {
|
|
status := s.updater.GetStatus()
|
|
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 Vector DB (sqlite-vec)
|
|
vectorStatus := ComponentHealth{Name: "Vector DB", Status: "healthy"}
|
|
if s.vectorClient == nil {
|
|
vectorStatus.Status = "degraded"
|
|
vectorStatus.Message = "Not configured"
|
|
if overall == "healthy" {
|
|
overall = "degraded"
|
|
}
|
|
} else if !s.vectorClient.IsConnected() {
|
|
vectorStatus.Status = "degraded"
|
|
vectorStatus.Message = "Not connected"
|
|
if overall == "healthy" {
|
|
overall = "degraded"
|
|
}
|
|
}
|
|
components = append(components, vectorStatus)
|
|
|
|
// 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)
|
|
|
|
// Check Cross-Encoder Reranker
|
|
rerankerStatus := ComponentHealth{Name: "Cross-Encoder Reranker", Status: "healthy"}
|
|
if !s.config.RerankingEnabled {
|
|
rerankerStatus.Status = "degraded"
|
|
rerankerStatus.Message = "Disabled in config"
|
|
if overall == "healthy" {
|
|
overall = "degraded"
|
|
}
|
|
} else if s.reranker == nil {
|
|
rerankerStatus.Status = "degraded"
|
|
rerankerStatus.Message = "Not initialized"
|
|
if overall == "healthy" {
|
|
overall = "degraded"
|
|
}
|
|
} else {
|
|
// Verify reranker is functional using Score
|
|
_, normalizedScore, err := s.reranker.Score("test query", "test document")
|
|
if err != nil {
|
|
rerankerStatus.Status = "unhealthy"
|
|
rerankerStatus.Message = fmt.Sprintf("Score check failed: %v", err)
|
|
if overall == "healthy" {
|
|
overall = "degraded"
|
|
}
|
|
} else {
|
|
rerankerStatus.Message = fmt.Sprintf("Score check passed (%.4f)", normalizedScore)
|
|
}
|
|
}
|
|
components = append(components, rerankerStatus)
|
|
|
|
// 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 (after update).
|
|
func (s *Service) handleUpdateRestart(w http.ResponseWriter, r *http.Request) {
|
|
status := s.updater.GetStatus()
|
|
if status.State != "done" {
|
|
http.Error(w, "no update has been applied", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Send response before restarting
|
|
writeJSON(w, map[string]any{
|
|
"success": true,
|
|
"message": "Restarting worker...",
|
|
})
|
|
|
|
// Flush the response
|
|
if f, ok := w.(http.Flusher); ok {
|
|
f.Flush()
|
|
}
|
|
|
|
// Restart in background after response is sent
|
|
go func() {
|
|
if err := s.updater.Restart(); err != nil {
|
|
log.Error().Err(err).Msg("Failed to restart worker")
|
|
}
|
|
}()
|
|
}
|
|
|
|
// handleRestart restarts the worker process (general restart, not tied to update).
|
|
func (s *Service) handleRestart(w http.ResponseWriter, r *http.Request) {
|
|
log.Info().Msg("Manual restart requested via API")
|
|
|
|
// Send response before restarting
|
|
writeJSON(w, map[string]any{
|
|
"success": true,
|
|
"message": "Restarting worker...",
|
|
"version": s.version,
|
|
})
|
|
|
|
// Flush the response
|
|
if f, ok := w.(http.Flusher); ok {
|
|
f.Flush()
|
|
}
|
|
|
|
// Restart in background after response is sent
|
|
go func() {
|
|
// Small delay to ensure response is sent
|
|
time.Sleep(100 * time.Millisecond)
|
|
if err := s.updater.Restart(); err != nil {
|
|
log.Error().Err(err).Msg("Failed to restart worker")
|
|
}
|
|
}()
|
|
}
|