Files
aethera/backend/internal/server/server.go
Evan Reichard 74b8d43032 refactor!: move LLM configuration from in-app settings to CLI/env vars
- Remove `api_endpoint` from Settings model and settings UI
- Add `--llm-endpoint` / `AETHERA_LLM_ENDPOINT` and `--llm-key` /
  `AETHERA_LLM_KEY` CLI flags (endpoint is required)
- Update client constructor to accept API key parameter
- Update tests and documentation to reflect new configuration approach

BREAKING CHANGE: LLM endpoint and key must now be provided via
`AETHERA_LLM_ENDPOINT` and `AETHERA_LLM_KEY` environment variables or
CLI flags instead of the Settings page.
2026-05-01 23:30:34 -04:00

105 lines
3.1 KiB
Go

package server
import (
"fmt"
"io/fs"
"net/http"
"path"
"time"
"github.com/sirupsen/logrus"
"reichard.io/aethera/internal/api"
"reichard.io/aethera/internal/store"
"reichard.io/aethera/web"
)
func StartServer(settingsStore store.Store, dataDir, staticDir, listenAddress string, listenPort int, llmEndpoint, llmKey string) {
mux := http.NewServeMux()
// Create API Instance - use settingsStore as the unified store for both settings and chat
logger := logrus.New()
api := api.New(settingsStore, dataDir, logger, llmEndpoint, llmKey)
// Serve Static Assets
if staticDir != "" {
logrus.Infof("Serving static assets from directory: %s", staticDir)
mux.Handle("GET /", http.FileServer(http.Dir(staticDir)))
} else {
staticFS, err := fs.Sub(web.Assets, "static")
if err != nil {
logrus.Fatal("Failed to create static filesystem: ", err)
}
mux.Handle("GET /", http.FileServer(http.FS(staticFS)))
}
// Serve Generated Data
genFS := http.FileServer(http.Dir(path.Join(dataDir, "generated")))
mux.Handle("GET /generated/", http.StripPrefix("/generated/", genFS))
// Register API Routes
mux.HandleFunc("POST /api/images", api.PostImage)
mux.HandleFunc("GET /api/settings", api.GetSettings)
mux.HandleFunc("POST /api/settings", api.PostSettings)
mux.HandleFunc("GET /api/models", api.GetModels)
mux.HandleFunc("GET /api/images", api.GetImages)
mux.HandleFunc("DELETE /api/images/{filename}", api.DeleteImage)
// Register Chat Management Routes
mux.HandleFunc("GET /api/chats", api.GetChats)
mux.HandleFunc("POST /api/chats", api.PostChat)
mux.HandleFunc("GET /api/chats/{chatId}", api.GetChat)
mux.HandleFunc("GET /api/chats/{chatId}/stream", api.GetChatStream)
mux.HandleFunc("POST /api/chats/{chatId}", api.PostChatMessage)
mux.HandleFunc("DELETE /api/chats/{chatId}", api.DeleteChat)
// Wrap Logging
wrappedMux := loggingMiddleware(mux)
logrus.Infof("Starting server on %s:%d with data directory: %s", listenAddress, listenPort, dataDir)
logrus.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", listenAddress, listenPort), wrappedMux))
}
// loggingMiddleware wraps an http.Handler and logs requests
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
ww := &responseWriterWrapper{ResponseWriter: w}
next.ServeHTTP(ww, r)
logrus.WithFields(logrus.Fields{
"datetime": start.UTC().Format(time.RFC3339),
"method": r.Method,
"path": r.URL.Path,
"remote": r.RemoteAddr,
"status": ww.getStatusCode(),
"latency": time.Since(start),
}).Infof("%s %s", r.Method, r.URL.Path)
})
}
// responseWriterWrapper wraps http.ResponseWriter to capture status code
type responseWriterWrapper struct {
http.ResponseWriter
statusCode int
}
func (w *responseWriterWrapper) Flush() {
if f, ok := w.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
func (rw *responseWriterWrapper) getStatusCode() int {
if rw.statusCode == 0 {
return 200
}
return rw.statusCode
}
func (rw *responseWriterWrapper) WriteHeader(code int) {
if code > 0 {
rw.statusCode = code
}
rw.ResponseWriter.WriteHeader(code)
}