mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-01 21:10:13 +00:00
Add AI monitoring enhancements and host metadata features
- Add host metadata API for custom URL editing on hosts page - Enhance AI routing with unified resource provider lookup - Add encryption key watcher script for debugging key issues - Improve AI service with better command timeout handling - Update dev environment workflow with key monitoring docs - Fix resource store deduplication logic
This commit is contained in:
parent
927ac76bad
commit
c8adbb7ae5
17 changed files with 1101 additions and 110 deletions
|
|
@ -43,7 +43,7 @@ const (
|
|||
DefaultAIModelAnthropic = "claude-opus-4-5-20251101"
|
||||
DefaultAIModelOpenAI = "gpt-4o"
|
||||
DefaultAIModelOllama = "llama3"
|
||||
DefaultAIModelDeepSeek = "deepseek-reasoner"
|
||||
DefaultAIModelDeepSeek = "deepseek-chat" // V3.2 with tool-use support
|
||||
DefaultOllamaBaseURL = "http://localhost:11434"
|
||||
DefaultDeepSeekBaseURL = "https://api.deepseek.com/chat/completions"
|
||||
)
|
||||
|
|
|
|||
179
internal/config/host_metadata.go
Normal file
179
internal/config/host_metadata.go
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// HostMetadata holds additional metadata for a host
|
||||
type HostMetadata struct {
|
||||
ID string `json:"id"` // Host ID
|
||||
CustomURL string `json:"customUrl"` // Custom URL for the host
|
||||
Description string `json:"description"` // Optional description
|
||||
Tags []string `json:"tags"` // Optional tags for categorization
|
||||
Notes []string `json:"notes"` // User annotations for AI context
|
||||
}
|
||||
|
||||
// HostMetadataStore manages host metadata
|
||||
type HostMetadataStore struct {
|
||||
mu sync.RWMutex
|
||||
metadata map[string]*HostMetadata // keyed by host ID
|
||||
dataPath string
|
||||
}
|
||||
|
||||
// NewHostMetadataStore creates a new host metadata store
|
||||
func NewHostMetadataStore(dataPath string) *HostMetadataStore {
|
||||
store := &HostMetadataStore{
|
||||
metadata: make(map[string]*HostMetadata),
|
||||
dataPath: dataPath,
|
||||
}
|
||||
|
||||
// Load existing metadata
|
||||
if err := store.Load(); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to load host metadata")
|
||||
}
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
// Get retrieves metadata for a host
|
||||
func (s *HostMetadataStore) Get(hostID string) *HostMetadata {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if meta, exists := s.metadata[hostID]; exists {
|
||||
return meta
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAll retrieves all host metadata
|
||||
func (s *HostMetadataStore) GetAll() map[string]*HostMetadata {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
// Return a copy to prevent external modifications
|
||||
result := make(map[string]*HostMetadata)
|
||||
for k, v := range s.metadata {
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Set updates or creates metadata for a host
|
||||
func (s *HostMetadataStore) Set(hostID string, meta *HostMetadata) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if meta == nil {
|
||||
return fmt.Errorf("metadata cannot be nil")
|
||||
}
|
||||
|
||||
meta.ID = hostID
|
||||
s.metadata[hostID] = meta
|
||||
|
||||
// Save to disk
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// Delete removes metadata for a host
|
||||
func (s *HostMetadataStore) Delete(hostID string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
delete(s.metadata, hostID)
|
||||
|
||||
// Save to disk
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// ReplaceAll replaces all metadata entries and persists them to disk.
|
||||
func (s *HostMetadataStore) ReplaceAll(metadata map[string]*HostMetadata) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.metadata = make(map[string]*HostMetadata)
|
||||
|
||||
for hostID, meta := range metadata {
|
||||
if meta == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
clone := *meta
|
||||
clone.ID = hostID
|
||||
// Ensure slice copy is not nil to allow JSON marshalling of empty tags
|
||||
if clone.Tags == nil {
|
||||
clone.Tags = []string{}
|
||||
}
|
||||
s.metadata[hostID] = &clone
|
||||
}
|
||||
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// Load reads metadata from disk
|
||||
func (s *HostMetadataStore) Load() error {
|
||||
filePath := filepath.Join(s.dataPath, "host_metadata.json")
|
||||
|
||||
log.Debug().Str("path", filePath).Msg("Loading host metadata from disk")
|
||||
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// File doesn't exist yet, not an error
|
||||
log.Debug().Str("path", filePath).Msg("Host metadata file does not exist yet")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to read metadata file: %w", err)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if err := json.Unmarshal(data, &s.metadata); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal metadata: %w", err)
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Int("hostCount", len(s.metadata)).
|
||||
Msg("Loaded host metadata")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// save writes metadata to disk (must be called with lock held)
|
||||
func (s *HostMetadataStore) save() error {
|
||||
filePath := filepath.Join(s.dataPath, "host_metadata.json")
|
||||
|
||||
log.Debug().Str("path", filePath).Msg("Saving host metadata to disk")
|
||||
|
||||
data, err := json.Marshal(s.metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal metadata: %w", err)
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
if err := os.MkdirAll(s.dataPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create data directory: %w", err)
|
||||
}
|
||||
|
||||
// Write to temp file first for atomic operation
|
||||
tempFile := filePath + ".tmp"
|
||||
if err := os.WriteFile(tempFile, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write metadata file: %w", err)
|
||||
}
|
||||
|
||||
// Rename temp file to actual file (atomic on most systems)
|
||||
if err := os.Rename(tempFile, filePath); err != nil {
|
||||
return fmt.Errorf("failed to rename metadata file: %w", err)
|
||||
}
|
||||
|
||||
log.Debug().Str("path", filePath).Int("hosts", len(s.metadata)).Msg("Host metadata saved successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue