mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 19:06:42 +02:00
feat(apps): create Hono compute servers for Traces, Planta, NutriPhi
Add lightweight Hono + Bun servers for server-only compute endpoints. CRUD is handled by mana-sync, these handle AI + file upload only. Traces: AI guide generation, location sync (Port 3026) Planta: Photo upload (S3), AI plant analysis (Port 3022) NutriPhi: AI meal analysis (photo+text), recommendations (Port 3023) Each uses @manacore/shared-hono for auth/health/errors. ~100-200 LOC. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4d26196590
commit
d3d11e661d
30 changed files with 1161 additions and 221 deletions
|
|
@ -1,24 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, data any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, status int, message string) {
|
||||
writeJSON(w, status, map[string]any{
|
||||
"success": false,
|
||||
"error": map[string]any{
|
||||
"statusCode": status,
|
||||
"message": message,
|
||||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@ package handler
|
|||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/manacore/shared-go/httputil"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
|
|
@ -34,27 +36,27 @@ func (h *ExtractHandler) Extract(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
var req extract.ExtractRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if req.URL == "" {
|
||||
writeError(w, http.StatusBadRequest, "url is required")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "url is required")
|
||||
return
|
||||
}
|
||||
if _, err := url.ParseRequestURI(req.URL); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "url must be a valid URL")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "url must be a valid URL")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate options
|
||||
if req.Options != nil {
|
||||
if req.Options.MaxLength > 0 && (req.Options.MaxLength < 100 || req.Options.MaxLength > 100000) {
|
||||
writeError(w, http.StatusBadRequest, "maxLength must be between 100 and 100000")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "maxLength must be between 100 and 100000")
|
||||
return
|
||||
}
|
||||
if req.Options.Timeout > 0 && (req.Options.Timeout < 1000 || req.Options.Timeout > 30000) {
|
||||
writeError(w, http.StatusBadRequest, "timeout must be between 1000 and 30000")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "timeout must be between 1000 and 30000")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -68,7 +70,7 @@ func (h *ExtractHandler) Extract(w http.ResponseWriter, r *http.Request) {
|
|||
cached.Meta.Cached = true
|
||||
duration := time.Since(start).Seconds()
|
||||
h.metrics.RecordRequest("extract", "200", duration)
|
||||
writeJSON(w, http.StatusOK, cached)
|
||||
httputil.WriteJSON(w, http.StatusOK, cached)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -89,7 +91,7 @@ func (h *ExtractHandler) Extract(w http.ResponseWriter, r *http.Request) {
|
|||
duration := time.Since(start).Seconds()
|
||||
h.metrics.RecordRequest("extract", status, duration)
|
||||
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
httputil.WriteJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// BulkExtract handles POST /api/v1/extract/bulk
|
||||
|
|
@ -98,22 +100,22 @@ func (h *ExtractHandler) BulkExtract(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
var req extract.BulkExtractRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.URLs) == 0 {
|
||||
writeError(w, http.StatusBadRequest, "urls is required")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "urls is required")
|
||||
return
|
||||
}
|
||||
if len(req.URLs) > 20 {
|
||||
writeError(w, http.StatusBadRequest, "maximum 20 URLs allowed")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "maximum 20 URLs allowed")
|
||||
return
|
||||
}
|
||||
|
||||
for _, u := range req.URLs {
|
||||
if _, err := url.ParseRequestURI(u); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid URL: "+u)
|
||||
httputil.WriteError(w, http.StatusBadRequest, "invalid URL: "+u)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -123,5 +125,5 @@ func (h *ExtractHandler) BulkExtract(w http.ResponseWriter, r *http.Request) {
|
|||
duration := time.Since(start).Seconds()
|
||||
h.metrics.RecordRequest("extract_bulk", "200", duration)
|
||||
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
httputil.WriteJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package handler
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/manacore/shared-go/httputil"
|
||||
"time"
|
||||
|
||||
"github.com/manacore/mana-search/internal/cache"
|
||||
|
|
@ -34,7 +36,7 @@ func (h *HealthHandler) Health(w http.ResponseWriter, r *http.Request) {
|
|||
overall = "degraded"
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
httputil.WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"status": overall,
|
||||
"service": "mana-search",
|
||||
"version": "1.0.0",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import (
|
|||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/manacore/shared-go/httputil"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
|
|
@ -37,23 +39,23 @@ func (h *SearchHandler) Search(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
var req search.SearchRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Query == "" {
|
||||
writeError(w, http.StatusBadRequest, "query is required")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "query is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate options
|
||||
if req.Options != nil {
|
||||
if req.Options.Limit < 0 || req.Options.Limit > 50 {
|
||||
writeError(w, http.StatusBadRequest, "limit must be between 1 and 50")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "limit must be between 1 and 50")
|
||||
return
|
||||
}
|
||||
if req.Options.SafeSearch < 0 || req.Options.SafeSearch > 2 {
|
||||
writeError(w, http.StatusBadRequest, "safeSearch must be 0, 1, or 2")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "safeSearch must be 0, 1, or 2")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -69,7 +71,7 @@ func (h *SearchHandler) Search(w http.ResponseWriter, r *http.Request) {
|
|||
cached.Meta.CacheKey = cacheKey
|
||||
duration := time.Since(start).Seconds()
|
||||
h.metrics.RecordRequest("search", "200", duration)
|
||||
writeJSON(w, http.StatusOK, cached)
|
||||
httputil.WriteJSON(w, http.StatusOK, cached)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -81,7 +83,7 @@ func (h *SearchHandler) Search(w http.ResponseWriter, r *http.Request) {
|
|||
slog.Error("search failed", "error", err, "query", req.Query)
|
||||
duration := time.Since(start).Seconds()
|
||||
h.metrics.RecordRequest("search", "502", duration)
|
||||
writeError(w, http.StatusBadGateway, err.Error())
|
||||
httputil.WriteError(w, http.StatusBadGateway, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -119,13 +121,13 @@ func (h *SearchHandler) Search(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
duration := time.Since(start).Seconds()
|
||||
h.metrics.RecordRequest("search", "200", duration)
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
httputil.WriteJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// Engines handles GET /api/v1/search/engines
|
||||
func (h *SearchHandler) Engines(w http.ResponseWriter, r *http.Request) {
|
||||
engines := h.provider.GetEngines(r.Context())
|
||||
writeJSON(w, http.StatusOK, map[string]any{"engines": engines})
|
||||
httputil.WriteJSON(w, http.StatusOK, map[string]any{"engines": engines})
|
||||
}
|
||||
|
||||
// Health handles GET /api/v1/search/health
|
||||
|
|
@ -134,7 +136,7 @@ func (h *SearchHandler) Health(w http.ResponseWriter, r *http.Request) {
|
|||
redisHealth := h.cache.HealthCheck(r.Context())
|
||||
cacheStats := h.cache.Stats()
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
httputil.WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"searxng": map[string]any{
|
||||
"status": sxStatus,
|
||||
"latency": sxLatency,
|
||||
|
|
@ -148,10 +150,10 @@ func (h *SearchHandler) Health(w http.ResponseWriter, r *http.Request) {
|
|||
func (h *SearchHandler) ClearCache(w http.ResponseWriter, r *http.Request) {
|
||||
deleted, err := h.cache.Clear(r.Context())
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to clear cache")
|
||||
httputil.WriteError(w, http.StatusInternalServerError, "failed to clear cache")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
httputil.WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"cleared": true,
|
||||
"keysRemoved": deleted,
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue