Files
latticelm/internal/admin/handlers.go
Anibal Angulo 7025ec746c
Some checks failed
CI / Test (pull_request) Failing after 1m33s
CI / Lint (pull_request) Failing after 13s
CI / Build (pull_request) Has been skipped
CI / Security Scan (pull_request) Failing after 4m47s
CI / Build and Push Docker Image (pull_request) Has been skipped
Add admin UI
2026-03-05 23:09:27 +00:00

253 lines
6.6 KiB
Go

package admin
import (
"fmt"
"net/http"
"runtime"
"strings"
"time"
"github.com/ajac-zero/latticelm/internal/config"
)
// SystemInfoResponse contains system information.
type SystemInfoResponse struct {
Version string `json:"version"`
BuildTime string `json:"build_time"`
GitCommit string `json:"git_commit"`
GoVersion string `json:"go_version"`
Platform string `json:"platform"`
Uptime string `json:"uptime"`
}
// HealthCheckResponse contains health check results.
type HealthCheckResponse struct {
Status string `json:"status"`
Timestamp string `json:"timestamp"`
Checks map[string]HealthCheck `json:"checks"`
}
// HealthCheck represents a single health check.
type HealthCheck struct {
Status string `json:"status"`
Message string `json:"message,omitempty"`
}
// ConfigResponse contains the sanitized configuration.
type ConfigResponse struct {
Server config.ServerConfig `json:"server"`
Providers map[string]SanitizedProvider `json:"providers"`
Models []config.ModelEntry `json:"models"`
Auth SanitizedAuthConfig `json:"auth"`
Conversations config.ConversationConfig `json:"conversations"`
Logging config.LoggingConfig `json:"logging"`
RateLimit config.RateLimitConfig `json:"rate_limit"`
Observability config.ObservabilityConfig `json:"observability"`
}
// SanitizedProvider is a provider entry with secrets masked.
type SanitizedProvider struct {
Type string `json:"type"`
APIKey string `json:"api_key"`
Endpoint string `json:"endpoint,omitempty"`
APIVersion string `json:"api_version,omitempty"`
Project string `json:"project,omitempty"`
Location string `json:"location,omitempty"`
}
// SanitizedAuthConfig is auth config with secrets masked.
type SanitizedAuthConfig struct {
Enabled bool `json:"enabled"`
Issuer string `json:"issuer"`
Audience string `json:"audience"`
}
// ProviderInfo contains provider information.
type ProviderInfo struct {
Name string `json:"name"`
Type string `json:"type"`
Models []string `json:"models"`
Status string `json:"status"`
}
// handleSystemInfo returns system information.
func (s *AdminServer) handleSystemInfo(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only GET is allowed")
return
}
uptime := time.Since(s.startTime)
info := SystemInfoResponse{
Version: s.buildInfo.Version,
BuildTime: s.buildInfo.BuildTime,
GitCommit: s.buildInfo.GitCommit,
GoVersion: s.buildInfo.GoVersion,
Platform: runtime.GOOS + "/" + runtime.GOARCH,
Uptime: formatDuration(uptime),
}
writeSuccess(w, info)
}
// handleSystemHealth returns health check results.
func (s *AdminServer) handleSystemHealth(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only GET is allowed")
return
}
checks := make(map[string]HealthCheck)
overallStatus := "healthy"
// Server check
checks["server"] = HealthCheck{
Status: "healthy",
Message: "Server is running",
}
// Provider check
models := s.registry.Models()
if len(models) > 0 {
checks["providers"] = HealthCheck{
Status: "healthy",
Message: "Providers configured",
}
} else {
checks["providers"] = HealthCheck{
Status: "unhealthy",
Message: "No providers configured",
}
overallStatus = "unhealthy"
}
// Conversation store check
checks["conversation_store"] = HealthCheck{
Status: "healthy",
Message: "Store accessible",
}
response := HealthCheckResponse{
Status: overallStatus,
Timestamp: time.Now().Format(time.RFC3339),
Checks: checks,
}
writeSuccess(w, response)
}
// handleConfig returns the sanitized configuration.
func (s *AdminServer) handleConfig(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only GET is allowed")
return
}
// Sanitize providers
sanitizedProviders := make(map[string]SanitizedProvider)
for name, provider := range s.cfg.Providers {
sanitizedProviders[name] = SanitizedProvider{
Type: provider.Type,
APIKey: maskSecret(provider.APIKey),
Endpoint: provider.Endpoint,
APIVersion: provider.APIVersion,
Project: provider.Project,
Location: provider.Location,
}
}
// Sanitize DSN in conversations config
convConfig := s.cfg.Conversations
if convConfig.DSN != "" {
convConfig.DSN = maskSecret(convConfig.DSN)
}
response := ConfigResponse{
Server: s.cfg.Server,
Providers: sanitizedProviders,
Models: s.cfg.Models,
Auth: SanitizedAuthConfig{
Enabled: s.cfg.Auth.Enabled,
Issuer: s.cfg.Auth.Issuer,
Audience: s.cfg.Auth.Audience,
},
Conversations: convConfig,
Logging: s.cfg.Logging,
RateLimit: s.cfg.RateLimit,
Observability: s.cfg.Observability,
}
writeSuccess(w, response)
}
// handleProviders returns the list of configured providers.
func (s *AdminServer) handleProviders(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only GET is allowed")
return
}
// Build provider info map
providerModels := make(map[string][]string)
models := s.registry.Models()
for _, m := range models {
providerModels[m.Provider] = append(providerModels[m.Provider], m.Model)
}
// Build provider list
var providers []ProviderInfo
for name, entry := range s.cfg.Providers {
providers = append(providers, ProviderInfo{
Name: name,
Type: entry.Type,
Models: providerModels[name],
Status: "active",
})
}
writeSuccess(w, providers)
}
// maskSecret masks a secret string for display.
func maskSecret(secret string) string {
if secret == "" {
return ""
}
if len(secret) <= 8 {
return "********"
}
// Show first 4 and last 4 characters
return secret[:4] + "..." + secret[len(secret)-4:]
}
// formatDuration formats a duration in a human-readable format.
func formatDuration(d time.Duration) string {
d = d.Round(time.Second)
h := d / time.Hour
d -= h * time.Hour
m := d / time.Minute
d -= m * time.Minute
s := d / time.Second
var parts []string
if h > 0 {
parts = append(parts, formatPart(int(h), "hour"))
}
if m > 0 {
parts = append(parts, formatPart(int(m), "minute"))
}
if s > 0 || len(parts) == 0 {
parts = append(parts, formatPart(int(s), "second"))
}
return strings.Join(parts, " ")
}
func formatPart(value int, unit string) string {
if value == 1 {
return "1 " + unit
}
return fmt.Sprintf("%d %ss", value, unit)
}