fix: resolve dashboard websocket 'disconnected' status
Some checks failed
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Build (push) Has been cancelled

- Fixed status indicator UI mapping in websocket.js and index.html.
- Added missing CSS for connection status indicator and pulse animation.
- Made initial model registry fetch asynchronous to prevent blocking server startup.
- Improved configuration loading to correctly handle LLM_PROXY__SERVER__PORT from environment.
This commit is contained in:
2026-03-19 14:32:34 -04:00
parent 08cf5cc1d9
commit 9380580504
4 changed files with 78 additions and 15 deletions

View File

@@ -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)")

View File

@@ -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")

View File

@@ -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); }

View File

@@ -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 = {