package api import ( "encoding/json" "net/http" "strconv" "strings" fleetai "crypto-miner-server/internal/ai" "crypto-miner-server/internal/db" ) // SeerHandler serves read-only Seer stream and notes APIs. type SeerHandler struct { db interface { ListSeerEvents(limit int) ([]db.SeerEventRecord, error) ListSeerNotes(limit int) ([]db.SeerNoteRecord, error) InsertSeerNote(agentID, note, source string) error } } func NewSeerHandler(database interface { ListSeerEvents(limit int) ([]db.SeerEventRecord, error) ListSeerNotes(limit int) ([]db.SeerNoteRecord, error) InsertSeerNote(agentID, note, source string) error }) *SeerHandler { return &SeerHandler{db: database} } // GET /api/v1/seer/stream — LLM event stream + persisted notes snapshot. func (h *SeerHandler) GetStream(w http.ResponseWriter, r *http.Request) { limit := parseLimit(r, 100) if h.db == nil { writeJSON(w, map[string]interface{}{ "events": []db.SeerEventRecord{}, "notes": []db.SeerNoteRecord{}, }) return } events, err := h.db.ListSeerEvents(limit) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } notes, err := h.db.ListSeerNotes(limit) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if events == nil { events = []db.SeerEventRecord{} } if notes == nil { notes = []db.SeerNoteRecord{} } writeJSON(w, map[string]interface{}{"events": events, "notes": notes}) } // POST /api/v1/seer/stream — append a note (read-only UI; API for scheduler/tests). func (h *SeerHandler) PostStream(w http.ResponseWriter, r *http.Request) { h.PostNote(w, r) } func (h *SeerHandler) GetNotes(w http.ResponseWriter, r *http.Request) { if h.db == nil { writeJSON(w, []db.SeerNoteRecord{}) return } limit := parseLimit(r, 100) rows, err := h.db.ListSeerNotes(limit) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if rows == nil { rows = []db.SeerNoteRecord{} } writeJSON(w, rows) } func (h *SeerHandler) PostNote(w http.ResponseWriter, r *http.Request) { if h.db == nil { http.Error(w, "database unavailable", http.StatusServiceUnavailable) return } var body struct { AgentID string `json:"agent_id"` Note string `json:"note"` Source string `json:"source"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { http.Error(w, "invalid JSON", http.StatusBadRequest) return } note := strings.TrimSpace(body.Note) if note == "" { http.Error(w, "note required", http.StatusBadRequest) return } source := strings.TrimSpace(body.Source) if source == "" { source = "manual" } if err := h.db.InsertSeerNote(body.AgentID, note, source); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, map[string]bool{"ok": true}) } // GET /api/v1/seer/tools — documented AI tool-call endpoints. func (h *SeerHandler) GetTools(w http.ResponseWriter, _ *http.Request) { writeJSON(w, map[string]interface{}{ "tools": []map[string]string{ {"name": "spread_route", "method": "POST", "path": "/api/v1/seer/tools/spread_route", "description": "BGP-style minimum-clearance spread routing"}, {"name": "graft_strain", "method": "POST", "path": "/api/v1/seer/tools/graft_strain", "description": "Clone winning spread strain onto sibling subnet"}, {"name": "fork_onion", "method": "POST", "path": "/api/v1/seer/tools/fork_onion", "description": "Split LOTL tier chain for A/B recovery"}, }, "catalog": fleetai.SeerToolCatalog, }) } func (h *SeerHandler) ToolSpreadRoute(w http.ResponseWriter, r *http.Request) { seerToolStub(w, r, "spread_route") } func (h *SeerHandler) ToolGraftStrain(w http.ResponseWriter, r *http.Request) { seerToolStub(w, r, "graft_strain") } func (h *SeerHandler) ToolForkOnion(w http.ResponseWriter, r *http.Request) { seerToolStub(w, r, "fork_onion") } func seerToolStub(w http.ResponseWriter, r *http.Request, tool string) { var body json.RawMessage if r.Body != nil { _ = json.NewDecoder(r.Body).Decode(&body) } if body == nil { body = json.RawMessage(`{}`) } writeJSON(w, map[string]interface{}{ "ok": true, "stub": true, "tool": tool, "received": json.RawMessage(body), }) } func parseLimit(r *http.Request, fallback int) int { limit := fallback if raw := r.URL.Query().Get("limit"); raw != "" { if n, err := strconv.Atoi(raw); err == nil && n > 0 { limit = n } } return limit }