diff --git a/internal/config/config.go b/internal/config/config.go index 276a1aec..3127dd52 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -111,8 +111,10 @@ func Load() (*Config, error) { v.SetEnvKeyReplacer(strings.NewReplacer(".", "__")) v.AutomaticEnv() - // Explicitly bind top-level keys that might use double underscores in .env + // Explicitly bind keys that might use double underscores in .env v.BindEnv("encryption_key", "LLM_PROXY__ENCRYPTION_KEY") + v.BindEnv("server.port", "LLM_PROXY__SERVER__PORT") + v.BindEnv("server.host", "LLM_PROXY__SERVER__HOST") // Config file v.SetConfigName("config") @@ -133,6 +135,19 @@ func Load() (*Config, error) { return nil, fmt.Errorf("failed to unmarshal config: %w", err) } + fmt.Printf("Debug Config: port from viper=%d, host from viper=%s\n", cfg.Server.Port, cfg.Server.Host) + fmt.Printf("Debug Env: LLM_PROXY__SERVER__PORT=%s, LLM_PROXY__SERVER__HOST=%s\n", os.Getenv("LLM_PROXY__SERVER__PORT"), os.Getenv("LLM_PROXY__SERVER__HOST")) + + // Manual overrides for nested keys which Viper doesn't always bind correctly with AutomaticEnv + SetEnvPrefix + if port := os.Getenv("LLM_PROXY__SERVER__PORT"); port != "" { + fmt.Sscanf(port, "%d", &cfg.Server.Port) + fmt.Printf("Overriding port to %d from env\n", cfg.Server.Port) + } + if host := os.Getenv("LLM_PROXY__SERVER__HOST"); host != "" { + cfg.Server.Host = host + fmt.Printf("Overriding host to %s from env\n", cfg.Server.Host) + } + // Validate encryption key if cfg.EncryptionKey == "" { return nil, fmt.Errorf("encryption key is required (LLM_PROXY__ENCRYPTION_KEY)") diff --git a/internal/server/server.go b/internal/server/server.go index 9ae13ff7..0ff0ac1b 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -32,13 +32,6 @@ type Server struct { func NewServer(cfg *config.Config, database *db.DB) *Server { router := gin.Default() hub := NewHub() - - // Fetch registry (non-blocking for startup if it fails, but we'll try once) - registry, err := utils.FetchRegistry() - if err != nil { - fmt.Printf("Warning: Failed to fetch initial model registry: %v\n", err) - registry = &models.ModelRegistry{Providers: make(map[string]models.ProviderInfo)} - } s := &Server{ router: router, @@ -48,9 +41,19 @@ func NewServer(cfg *config.Config, database *db.DB) *Server { sessions: NewSessionManager(cfg.KeyBytes, 24*time.Hour), hub: hub, logger: NewRequestLogger(database, hub), - registry: registry, + registry: &models.ModelRegistry{Providers: make(map[string]models.ProviderInfo)}, } + // Fetch registry in background + go func() { + registry, err := utils.FetchRegistry() + if err != nil { + fmt.Printf("Warning: Failed to fetch initial model registry: %v\n", err) + } else { + s.registry = registry + } + }() + // Initialize providers if cfg.Providers.OpenAI.Enabled { apiKey, _ := cfg.GetAPIKey("openai") diff --git a/static/css/dashboard.css b/static/css/dashboard.css index ee4a888f..40aedbdd 100644 --- a/static/css/dashboard.css +++ b/static/css/dashboard.css @@ -1134,6 +1134,53 @@ body { gap: 0.75rem; } +/* Connection Status Indicator */ +.status-indicator { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.5rem 0.875rem; + background: var(--bg1); + border: 1px solid var(--bg3); + border-radius: 6px; + font-size: 0.8rem; + font-weight: 600; + color: var(--fg3); + transition: all 0.2s; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--fg4); + position: relative; +} + +.status-dot.connected { + background: var(--green-light); + box-shadow: 0 0 0 0 rgba(184, 187, 38, 0.4); + animation: status-pulse 2s infinite; +} + +.status-dot.disconnected { + background: var(--red-light); +} + +.status-dot.connecting { + background: var(--yellow-light); +} + +.status-dot.error { + background: var(--red); +} + +@keyframes status-pulse { + 0% { box-shadow: 0 0 0 0 rgba(184, 187, 38, 0.4); } + 70% { box-shadow: 0 0 0 6px rgba(184, 187, 38, 0); } + 100% { box-shadow: 0 0 0 0 rgba(184, 187, 38, 0); } +} + /* WebSocket Dot Pulse */ @keyframes ws-pulse { 0% { box-shadow: 0 0 0 0 rgba(184, 187, 38, 0.4); } diff --git a/static/js/websocket.js b/static/js/websocket.js index 36c58774..983bcbdf 100644 --- a/static/js/websocket.js +++ b/static/js/websocket.js @@ -248,21 +248,19 @@ class WebSocketManager { } updateStatus(status) { - const statusElement = document.getElementById('ws-status-nav'); + const statusElement = document.getElementById('connection-status'); if (!statusElement) return; - const dot = statusElement.querySelector('.ws-dot'); - const text = statusElement.querySelector('.ws-text'); + const dot = statusElement.querySelector('.status-dot'); + const text = statusElement.querySelector('.status-text'); if (!dot || !text) return; // Remove all status classes - dot.classList.remove('connected', 'disconnected'); - statusElement.classList.remove('connected', 'disconnected'); + dot.classList.remove('connected', 'disconnected', 'error', 'connecting'); // Add new status class dot.classList.add(status); - statusElement.classList.add(status); // Update text const statusText = {