136 lines
4.0 KiB
Go
136 lines
4.0 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
|
|
}
|
|
// Likewise preserve ambient fields when a caller omits them, so a partial
|
|
// POST never clobbers a configured ambient dial.
|
|
if req.AmbientMode == "" {
|
|
req.AmbientMode = s.settings.AmbientMode
|
|
}
|
|
if req.AmbientCadenceSecs <= 0 {
|
|
req.AmbientCadenceSecs = s.settings.AmbientCadenceSecs
|
|
}
|
|
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 "/"
|
|
}
|