fix: resolve dashboard websocket 'disconnected' status
- 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:
@@ -111,8 +111,10 @@ func Load() (*Config, error) {
|
|||||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "__"))
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "__"))
|
||||||
v.AutomaticEnv()
|
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("encryption_key", "LLM_PROXY__ENCRYPTION_KEY")
|
||||||
|
v.BindEnv("server.port", "LLM_PROXY__SERVER__PORT")
|
||||||
|
v.BindEnv("server.host", "LLM_PROXY__SERVER__HOST")
|
||||||
|
|
||||||
// Config file
|
// Config file
|
||||||
v.SetConfigName("config")
|
v.SetConfigName("config")
|
||||||
@@ -133,6 +135,19 @@ func Load() (*Config, error) {
|
|||||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
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
|
// Validate encryption key
|
||||||
if cfg.EncryptionKey == "" {
|
if cfg.EncryptionKey == "" {
|
||||||
return nil, fmt.Errorf("encryption key is required (LLM_PROXY__ENCRYPTION_KEY)")
|
return nil, fmt.Errorf("encryption key is required (LLM_PROXY__ENCRYPTION_KEY)")
|
||||||
|
|||||||
@@ -33,13 +33,6 @@ func NewServer(cfg *config.Config, database *db.DB) *Server {
|
|||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
hub := NewHub()
|
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{
|
s := &Server{
|
||||||
router: router,
|
router: router,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
@@ -48,9 +41,19 @@ func NewServer(cfg *config.Config, database *db.DB) *Server {
|
|||||||
sessions: NewSessionManager(cfg.KeyBytes, 24*time.Hour),
|
sessions: NewSessionManager(cfg.KeyBytes, 24*time.Hour),
|
||||||
hub: hub,
|
hub: hub,
|
||||||
logger: NewRequestLogger(database, 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
|
// Initialize providers
|
||||||
if cfg.Providers.OpenAI.Enabled {
|
if cfg.Providers.OpenAI.Enabled {
|
||||||
apiKey, _ := cfg.GetAPIKey("openai")
|
apiKey, _ := cfg.GetAPIKey("openai")
|
||||||
|
|||||||
@@ -1134,6 +1134,53 @@ body {
|
|||||||
gap: 0.75rem;
|
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 */
|
/* WebSocket Dot Pulse */
|
||||||
@keyframes ws-pulse {
|
@keyframes ws-pulse {
|
||||||
0% { box-shadow: 0 0 0 0 rgba(184, 187, 38, 0.4); }
|
0% { box-shadow: 0 0 0 0 rgba(184, 187, 38, 0.4); }
|
||||||
|
|||||||
@@ -248,21 +248,19 @@ class WebSocketManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateStatus(status) {
|
updateStatus(status) {
|
||||||
const statusElement = document.getElementById('ws-status-nav');
|
const statusElement = document.getElementById('connection-status');
|
||||||
if (!statusElement) return;
|
if (!statusElement) return;
|
||||||
|
|
||||||
const dot = statusElement.querySelector('.ws-dot');
|
const dot = statusElement.querySelector('.status-dot');
|
||||||
const text = statusElement.querySelector('.ws-text');
|
const text = statusElement.querySelector('.status-text');
|
||||||
|
|
||||||
if (!dot || !text) return;
|
if (!dot || !text) return;
|
||||||
|
|
||||||
// Remove all status classes
|
// Remove all status classes
|
||||||
dot.classList.remove('connected', 'disconnected');
|
dot.classList.remove('connected', 'disconnected', 'error', 'connecting');
|
||||||
statusElement.classList.remove('connected', 'disconnected');
|
|
||||||
|
|
||||||
// Add new status class
|
// Add new status class
|
||||||
dot.classList.add(status);
|
dot.classList.add(status);
|
||||||
statusElement.classList.add(status);
|
|
||||||
|
|
||||||
// Update text
|
// Update text
|
||||||
const statusText = {
|
const statusText = {
|
||||||
|
|||||||
Reference in New Issue
Block a user