Files
jbergner 9030c2efb7
All checks were successful
release-tag / release-image (push) Successful in 6m17s
1.5.3
2026-08-27 10:43:07 +02:00

182 lines
5.0 KiB
Go

package httpapi
import (
"context"
"errors"
"net/http"
"strconv"
"time"
"neuroforge/internal/brain"
"neuroforge/internal/core"
"neuroforge/internal/store"
)
func (s *Server) goalsList(w http.ResponseWriter, r *http.Request) {
s.json(w, http.StatusOK, s.store.GoalsSnapshot())
}
func (s *Server) goalsCreate(w http.ResponseWriter, r *http.Request) {
var g core.Goal
if err := decode(r, &g); err != nil {
s.err(w, 400, err)
return
}
if err := s.store.UpsertGoal(&g); err != nil {
if errors.Is(err, store.ErrDuplicateGoal) {
s.err(w, http.StatusConflict, err)
return
}
s.err(w, 400, err)
return
}
s.json(w, http.StatusCreated, g)
}
func (s *Server) goalsGet(w http.ResponseWriter, r *http.Request) {
g, ok := s.store.GetGoal(r.PathValue("id"))
if !ok {
s.err(w, 404, errors.New("goal not found"))
return
}
s.json(w, 200, g)
}
func (s *Server) goalsPut(w http.ResponseWriter, r *http.Request) {
var g core.Goal
if err := decode(r, &g); err != nil {
s.err(w, 400, err)
return
}
g.ID = r.PathValue("id")
if old, ok := s.store.GetGoal(g.ID); ok && g.CreatedAt.IsZero() {
g.CreatedAt = old.CreatedAt
}
if err := s.store.UpsertGoal(&g); err != nil {
s.err(w, 400, err)
return
}
s.json(w, 200, g)
}
func (s *Server) goalsDelete(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
goal, _ := s.store.GetGoal(id)
if err := s.store.DeleteGoal(id); err != nil {
s.err(w, 404, err)
return
}
meta := map[string]string{"goal_id": id}
if goal != nil {
meta["title"] = goal.Title
}
_ = s.store.AddKnowledgeEvent(core.KnowledgeEvent{Type: "goal.deleted", Summary: "Goal deleted", Reason: "administrator/user deleted goal", Actor: "goal-control", Metadata: meta})
s.json(w, 200, map[string]bool{"ok": true})
}
func (s *Server) goalPause(w http.ResponseWriter, r *http.Request) {
g, err := s.store.PauseGoal(r.PathValue("id"))
if err != nil {
s.err(w, 400, err)
return
}
_ = s.store.AddKnowledgeEvent(core.KnowledgeEvent{Type: "goal.paused", Summary: "Goal paused: " + g.Title, Reason: "manual pause", Actor: "goal-control", Metadata: map[string]string{"goal_id": g.ID}})
s.json(w, 200, g)
}
func (s *Server) goalResume(w http.ResponseWriter, r *http.Request) {
g, err := s.store.ResumeGoal(r.PathValue("id"))
if err != nil {
s.err(w, 400, err)
return
}
_ = s.store.AddKnowledgeEvent(core.KnowledgeEvent{Type: "goal.resumed", Summary: "Goal resumed: " + g.Title, Reason: "manual resume", Actor: "goal-control", Metadata: map[string]string{"goal_id": g.ID}})
s.json(w, 200, g)
}
func (s *Server) goalCycle(w http.ResponseWriter, r *http.Request) {
cycle, err := s.brain.RunGoalCycle(r.Context(), r.PathValue("id"))
if err != nil {
if errors.Is(err, brain.ErrGoalCycleInProgress) {
s.err(w, http.StatusConflict, err)
return
}
s.err(w, http.StatusBadRequest, err)
return
}
s.json(w, 200, cycle)
}
func (s *Server) learningCycles(w http.ResponseWriter, r *http.Request) {
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
if limit <= 0 {
limit = 100
}
s.json(w, 200, s.store.RecentLearningCycles(limit))
}
func (s *Server) conflicts(w http.ResponseWriter, r *http.Request) {
s.json(w, 200, s.store.ConflictsSnapshot())
}
func (s *Server) adminRetention(w http.ResponseWriter, r *http.Request) {
out, err := s.store.RunRetention(time.Now().UTC())
if err != nil {
s.err(w, 500, err)
return
}
s.json(w, 200, out)
}
func (s *Server) adminAutonomy(w http.ResponseWriter, r *http.Request) {
s.json(w, 200, s.brain.RunAutonomy(r.Context()))
}
func (s *Server) adminRebalance(w http.ResponseWriter, r *http.Request) {
var q struct {
DryRun bool `json:"dry_run"`
}
if r.ContentLength != 0 {
if err := decode(r, &q); err != nil {
s.err(w, 400, err)
return
}
}
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Minute)
defer cancel()
s.json(w, 200, s.brain.RebalanceShards(ctx, q.DryRun))
}
func (s *Server) adminCheckpoint(w http.ResponseWriter, r *http.Request) {
if err := s.store.ForceCheckpoint(); err != nil {
s.err(w, 500, err)
return
}
s.json(w, 200, map[string]any{"ok": true, "wal": s.store.WALStatus()})
}
func (s *Server) adminWAL(w http.ResponseWriter, r *http.Request) {
s.json(w, 200, s.store.WALStatus())
}
func (s *Server) adminResolveConflict(w http.ResponseWriter, r *http.Request) {
var q struct {
TruthKey string `json:"truth_key"`
WinnerID string `json:"winner_id"`
}
if err := decode(r, &q); err != nil {
s.err(w, 400, err)
return
}
if q.TruthKey == "" || q.WinnerID == "" {
s.err(w, 400, errors.New("truth_key and winner_id are required"))
return
}
if err := s.store.ResolveConflict(q.TruthKey, q.WinnerID); err != nil {
s.err(w, 400, err)
return
}
_ = s.store.AddKnowledgeEvent(core.KnowledgeEvent{Type: "conflict.resolved", MemoryID: q.WinnerID, Summary: "Knowledge conflict resolved by administrator", Reason: "winner selected for truth key", Actor: "admin", Metadata: map[string]string{"truth_key": q.TruthKey}})
s.json(w, 200, map[string]bool{"ok": true})
}