// ingestion/internal/api/handler.go package api import ( "encoding/json" "fmt" "log/slog" "net/http" "os" "path/filepath" "strings" "time" "github.com/mathiasbq/hyperguild/ingestion/internal/search" ) // Handler serves the ingestion HTTP API. type Handler struct { brainDir string logger *slog.Logger } // NewHandler constructs a Handler. brainDir is the absolute path to brain/. func NewHandler(brainDir string, logger *slog.Logger) *Handler { return &Handler{brainDir: brainDir, logger: logger} } type queryRequest struct { Query string `json:"query"` Limit int `json:"limit,omitempty"` } type writeRequest struct { Content string `json:"content"` Filename string `json:"filename,omitempty"` Type string `json:"type,omitempty"` Domain string `json:"domain,omitempty"` } // Query handles POST /query — full-text search across the brain wiki. func (h *Handler) Query(w http.ResponseWriter, r *http.Request) { var req queryRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid JSON", http.StatusBadRequest) return } if strings.TrimSpace(req.Query) == "" { http.Error(w, "query is required", http.StatusBadRequest) return } if req.Limit == 0 { req.Limit = 5 } results, err := search.Query(h.brainDir, req.Query, req.Limit) if err != nil { h.logger.Error("query failed", "err", err) http.Error(w, "search error", http.StatusInternalServerError) return } writeJSON(w, map[string]any{"results": results}) } // Write handles POST /write — write raw content to brain/raw/. func (h *Handler) Write(w http.ResponseWriter, r *http.Request) { var req writeRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid JSON", http.StatusBadRequest) return } if req.Content == "" { http.Error(w, "content is required", http.StatusBadRequest) return } filename := req.Filename if filename == "" { filename = fmt.Sprintf("%s-auto.md", time.Now().UTC().Format("2006-01-02-150405")) } rawDir := filepath.Join(h.brainDir, "knowledge") if err := os.MkdirAll(rawDir, 0o755); err != nil { http.Error(w, "failed to create raw dir", http.StatusInternalServerError) return } finalContent := req.Content if req.Type != "" || req.Domain != "" { var fm strings.Builder fm.WriteString("---\n") if req.Type != "" { fmt.Fprintf(&fm, "type: %s\n", req.Type) } if req.Domain != "" { fmt.Fprintf(&fm, "domain: %s\n", req.Domain) } fm.WriteString("---\n") finalContent = fm.String() + req.Content } base := filepath.Base(filename) if !strings.HasSuffix(base, ".md") { base += ".md" } dest := filepath.Join(rawDir, base) if err := os.WriteFile(dest, []byte(finalContent), 0o644); err != nil { h.logger.Error("write failed", "err", err) http.Error(w, "write error", http.StatusInternalServerError) return } rel, _ := filepath.Rel(h.brainDir, dest) writeJSON(w, map[string]string{"path": filepath.ToSlash(rel)}) } func writeJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(v) //nolint:errcheck }