c855731c39
A UI save was silently blanking the configured AW URL because handlePostSettings bound the whole Settings from the request and saved it as-is. Now reads s.settings.AWURL under the existing settingsMu lock and carries it forward when req.AWURL is empty, so fields not exposed in the form are never erased.
128 lines
3.7 KiB
Go
128 lines
3.7 KiB
Go
package web
|
|
|
|
import (
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"keel/internal/settings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// SetSettings injects the persisted settings, their file path, and the applier
|
|
// closure (built by main, which owns adapter construction). Mirrors the
|
|
// controller's Set* injection so web never imports ai/tasks/knowledge.
|
|
func (s *Server) SetSettings(path string, current settings.Settings, apply func(settings.Settings) error) {
|
|
s.settingsMu.Lock()
|
|
s.settingsPath = path
|
|
s.settings = current
|
|
s.applyFn = apply
|
|
s.settingsMu.Unlock()
|
|
}
|
|
|
|
func (s *Server) handleGetSettings(c *gin.Context) {
|
|
s.settingsMu.Lock()
|
|
cur := s.settings
|
|
s.settingsMu.Unlock()
|
|
c.JSON(http.StatusOK, cur)
|
|
}
|
|
|
|
// handlePostSettings applies the new settings first, then persists them. The
|
|
// applier checks the backend before mutating any state, so an invalid backend
|
|
// yields 400 with nothing saved and the prior wiring intact. If Save fails after
|
|
// a successful apply, the running adapters are already rewired but the file and
|
|
// the in-memory copy are left unchanged; that surfaces as 500.
|
|
func (s *Server) handlePostSettings(c *gin.Context) {
|
|
var req settings.Settings
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid json"})
|
|
return
|
|
}
|
|
s.applyMu.Lock()
|
|
defer s.applyMu.Unlock()
|
|
s.settingsMu.Lock()
|
|
apply := s.applyFn
|
|
path := s.settingsPath
|
|
// aw_url is not exposed in the settings form; preserve the persisted value
|
|
// so a UI save never blanks a configured AW URL.
|
|
if req.AWURL == "" {
|
|
req.AWURL = s.settings.AWURL
|
|
}
|
|
s.settingsMu.Unlock()
|
|
if apply == nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "settings not wired"})
|
|
return
|
|
}
|
|
if err := apply(req); err != nil {
|
|
// Apply validates the backend before mutating anything, so any error
|
|
// (invalid backend or otherwise) means nothing changed; surface it as 400.
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if err := settings.Save(path, req); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
s.settingsMu.Lock()
|
|
s.settings = req
|
|
s.settingsMu.Unlock()
|
|
s.broadcast()
|
|
c.JSON(http.StatusOK, req)
|
|
}
|
|
|
|
type browseEntry struct {
|
|
Name string `json:"name"`
|
|
Path string `json:"path"`
|
|
IsDir bool `json:"is_dir"`
|
|
}
|
|
|
|
type browseResponse struct {
|
|
Dir string `json:"dir"`
|
|
Parent string `json:"parent"`
|
|
Entries []browseEntry `json:"entries"`
|
|
}
|
|
|
|
// handleBrowse lists subdirectories plus .md files under dir, so the UI can build
|
|
// a file picker that returns a real server-side path. Dotfiles are NOT hidden —
|
|
// the default profile lives under ~/.keel. Localhost-only daemon; no jail
|
|
// beyond OS permissions (same trust boundary as the rest of the UI).
|
|
func (s *Server) handleBrowse(c *gin.Context) {
|
|
dir := strings.TrimSpace(c.Query("dir"))
|
|
if dir == "" {
|
|
dir = s.browseStart()
|
|
}
|
|
dir = filepath.Clean(dir)
|
|
infos, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
resp := browseResponse{Dir: dir, Parent: filepath.Dir(dir), Entries: []browseEntry{}}
|
|
for _, e := range infos {
|
|
name := e.Name()
|
|
full := filepath.Join(dir, name)
|
|
if e.IsDir() {
|
|
resp.Entries = append(resp.Entries, browseEntry{Name: name, Path: full, IsDir: true})
|
|
} else if strings.HasSuffix(name, ".md") {
|
|
resp.Entries = append(resp.Entries, browseEntry{Name: name, Path: full})
|
|
}
|
|
}
|
|
c.JSON(http.StatusOK, resp)
|
|
}
|
|
|
|
// browseStart picks the directory of the current knowledge path, else $HOME.
|
|
func (s *Server) browseStart() string {
|
|
s.settingsMu.Lock()
|
|
kp := s.settings.KnowledgePath
|
|
s.settingsMu.Unlock()
|
|
if kp != "" {
|
|
return filepath.Dir(kp)
|
|
}
|
|
if home, err := os.UserHomeDir(); err == nil {
|
|
return home
|
|
}
|
|
return "/"
|
|
}
|