299 lines
7.3 KiB
Go
299 lines
7.3 KiB
Go
|
|
package api
|
||
|
|
|
||
|
|
import (
|
||
|
|
"caltrack/ai"
|
||
|
|
"caltrack/calc"
|
||
|
|
"caltrack/config"
|
||
|
|
"caltrack/db"
|
||
|
|
"caltrack/types"
|
||
|
|
"encoding/json"
|
||
|
|
"log"
|
||
|
|
"net/http"
|
||
|
|
"strconv"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
type Handler struct {
|
||
|
|
db *db.DB
|
||
|
|
ai *Client
|
||
|
|
cfg *types.Config
|
||
|
|
}
|
||
|
|
|
||
|
|
type Client = ai.Client
|
||
|
|
|
||
|
|
func New(database *db.DB, aiClient *ai.Client, cfg *types.Config) *Handler {
|
||
|
|
return &Handler{
|
||
|
|
db: database,
|
||
|
|
ai: aiClient,
|
||
|
|
cfg: cfg,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
|
||
|
|
mux.HandleFunc("GET /api/today", h.handleGetToday)
|
||
|
|
mux.HandleFunc("GET /api/day/{date}", h.handleGetDay)
|
||
|
|
mux.HandleFunc("POST /api/entry", h.handleAddEntry)
|
||
|
|
mux.HandleFunc("DELETE /api/entry/{id}", h.handleDeleteEntry)
|
||
|
|
mux.HandleFunc("DELETE /api/day/{date}", h.handleClearDay)
|
||
|
|
mux.HandleFunc("POST /api/weighin", h.handleWeighIn)
|
||
|
|
mux.HandleFunc("GET /api/autocomplete", h.handleAutocomplete)
|
||
|
|
mux.HandleFunc("GET /api/history", h.handleHistory)
|
||
|
|
mux.HandleFunc("GET /api/weight-history", h.handleWeightHistory)
|
||
|
|
mux.HandleFunc("GET /api/settings", h.handleGetSettings)
|
||
|
|
mux.HandleFunc("POST /api/settings", h.handleUpdateSettings)
|
||
|
|
}
|
||
|
|
|
||
|
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
w.WriteHeader(status)
|
||
|
|
json.NewEncoder(w).Encode(v)
|
||
|
|
}
|
||
|
|
|
||
|
|
func writeError(w http.ResponseWriter, status int, msg string) {
|
||
|
|
writeJSON(w, status, map[string]string{"error": msg})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *Handler) handleGetToday(w http.ResponseWriter, r *http.Request) {
|
||
|
|
h.handleGetDayData(w, time.Now().Format("2006-01-02"))
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *Handler) handleGetDay(w http.ResponseWriter, r *http.Request) {
|
||
|
|
date := r.PathValue("date")
|
||
|
|
if date == "" {
|
||
|
|
writeError(w, http.StatusBadRequest, "date is required")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
h.handleGetDayData(w, date)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *Handler) handleGetDayData(w http.ResponseWriter, date string) {
|
||
|
|
entries, err := h.db.GetEntriesByDate(date)
|
||
|
|
if err != nil {
|
||
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if entries == nil {
|
||
|
|
entries = []types.Entry{}
|
||
|
|
}
|
||
|
|
|
||
|
|
targetKcal := calc.DailyTarget(*h.cfg)
|
||
|
|
summary, err := h.db.GetDailySummary(date, targetKcal)
|
||
|
|
if err != nil {
|
||
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
weight, _ := h.db.GetLatestWeight()
|
||
|
|
if weight == 0 {
|
||
|
|
weight = h.cfg.CurrentWeightKg
|
||
|
|
}
|
||
|
|
|
||
|
|
h.cfg.HasAPIKey = h.cfg.AnthropicAPIKey != ""
|
||
|
|
writeJSON(w, http.StatusOK, types.TodayResponse{
|
||
|
|
Date: date,
|
||
|
|
Entries: entries,
|
||
|
|
Summary: summary,
|
||
|
|
TargetKcal: targetKcal,
|
||
|
|
Deficit: calc.Deficit(*h.cfg),
|
||
|
|
Weight: weight,
|
||
|
|
Config: *h.cfg,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *Handler) handleAddEntry(w http.ResponseWriter, r *http.Request) {
|
||
|
|
var req types.AddEntryRequest
|
||
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if req.Description == "" {
|
||
|
|
writeError(w, http.StatusBadRequest, "description is required")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if req.Type == "" {
|
||
|
|
req.Type = types.EntryFood
|
||
|
|
}
|
||
|
|
|
||
|
|
// Use AI to estimate calories
|
||
|
|
estimation, err := h.ai.EstimateCalories(r.Context(), req.Description, req.Type)
|
||
|
|
if err != nil {
|
||
|
|
log.Printf("AI estimation error: %v", err)
|
||
|
|
writeError(w, http.StatusInternalServerError, "failed to estimate calories: "+err.Error())
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
var entries []types.Entry
|
||
|
|
for _, item := range estimation.Items {
|
||
|
|
desc := item.Name
|
||
|
|
if item.Brand != "" {
|
||
|
|
desc = item.Name + " (" + item.Brand + ")"
|
||
|
|
}
|
||
|
|
|
||
|
|
entry := types.Entry{
|
||
|
|
Type: req.Type,
|
||
|
|
Description: desc,
|
||
|
|
Kcal: item.Kcal,
|
||
|
|
}
|
||
|
|
|
||
|
|
entry, err = h.db.AddEntry(entry)
|
||
|
|
if err != nil {
|
||
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||
|
|
return
|
||
|
|
}
|
||
|
|
entries = append(entries, entry)
|
||
|
|
}
|
||
|
|
|
||
|
|
writeJSON(w, http.StatusCreated, types.AddEntryResponse{
|
||
|
|
Entries: entries,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *Handler) handleDeleteEntry(w http.ResponseWriter, r *http.Request) {
|
||
|
|
idStr := r.PathValue("id")
|
||
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
||
|
|
if err != nil {
|
||
|
|
writeError(w, http.StatusBadRequest, "invalid entry id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := h.db.DeleteEntry(id); err != nil {
|
||
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *Handler) handleClearDay(w http.ResponseWriter, r *http.Request) {
|
||
|
|
date := r.PathValue("date")
|
||
|
|
if date == "" {
|
||
|
|
writeError(w, http.StatusBadRequest, "date is required")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := h.db.ClearDay(date); err != nil {
|
||
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "cleared"})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *Handler) handleWeighIn(w http.ResponseWriter, r *http.Request) {
|
||
|
|
var req types.WeighInRequest
|
||
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if req.WeightKg <= 0 {
|
||
|
|
writeError(w, http.StatusBadRequest, "weight must be positive")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
weighIn, err := h.db.AddWeighIn("", req.WeightKg)
|
||
|
|
if err != nil {
|
||
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update config with new weight
|
||
|
|
h.cfg.CurrentWeightKg = req.WeightKg
|
||
|
|
config.Save(*h.cfg)
|
||
|
|
|
||
|
|
writeJSON(w, http.StatusCreated, weighIn)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *Handler) handleAutocomplete(w http.ResponseWriter, r *http.Request) {
|
||
|
|
query := r.URL.Query().Get("q")
|
||
|
|
if len(query) < 2 {
|
||
|
|
writeJSON(w, http.StatusOK, []types.AutocompleteResult{})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
results, err := h.db.SearchProducts(query)
|
||
|
|
if err != nil {
|
||
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if results == nil {
|
||
|
|
results = []types.AutocompleteResult{}
|
||
|
|
}
|
||
|
|
|
||
|
|
writeJSON(w, http.StatusOK, results)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *Handler) handleHistory(w http.ResponseWriter, r *http.Request) {
|
||
|
|
summaries, err := h.db.GetHistory(30)
|
||
|
|
if err != nil {
|
||
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if summaries == nil {
|
||
|
|
summaries = []types.DailySummary{}
|
||
|
|
}
|
||
|
|
|
||
|
|
writeJSON(w, http.StatusOK, summaries)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *Handler) handleWeightHistory(w http.ResponseWriter, r *http.Request) {
|
||
|
|
weighIns, err := h.db.GetWeightHistory()
|
||
|
|
if err != nil {
|
||
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if weighIns == nil {
|
||
|
|
weighIns = []types.WeighIn{}
|
||
|
|
}
|
||
|
|
|
||
|
|
writeJSON(w, http.StatusOK, weighIns)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *Handler) handleGetSettings(w http.ResponseWriter, r *http.Request) {
|
||
|
|
h.cfg.HasAPIKey = h.cfg.AnthropicAPIKey != ""
|
||
|
|
writeJSON(w, http.StatusOK, h.cfg)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (h *Handler) handleUpdateSettings(w http.ResponseWriter, r *http.Request) {
|
||
|
|
var req types.SettingsRequest
|
||
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if req.CurrentWeightKg != nil {
|
||
|
|
h.cfg.CurrentWeightKg = *req.CurrentWeightKg
|
||
|
|
}
|
||
|
|
if req.TargetWeightKg != nil {
|
||
|
|
h.cfg.TargetWeightKg = *req.TargetWeightKg
|
||
|
|
}
|
||
|
|
if req.HeightCm != nil {
|
||
|
|
h.cfg.HeightCm = *req.HeightCm
|
||
|
|
}
|
||
|
|
if req.Age != nil {
|
||
|
|
h.cfg.Age = *req.Age
|
||
|
|
}
|
||
|
|
if req.Sex != nil {
|
||
|
|
h.cfg.Sex = *req.Sex
|
||
|
|
}
|
||
|
|
if req.ActivityLevel != nil {
|
||
|
|
h.cfg.ActivityLevel = *req.ActivityLevel
|
||
|
|
}
|
||
|
|
if req.WeeklyLossKg != nil {
|
||
|
|
h.cfg.WeeklyLossKg = *req.WeeklyLossKg
|
||
|
|
}
|
||
|
|
if req.AnthropicAPIKey != nil {
|
||
|
|
h.cfg.AnthropicAPIKey = *req.AnthropicAPIKey
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := config.Save(*h.cfg); err != nil {
|
||
|
|
writeError(w, http.StatusInternalServerError, "failed to save config: "+err.Error())
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
h.cfg.HasAPIKey = h.cfg.AnthropicAPIKey != ""
|
||
|
|
writeJSON(w, http.StatusOK, h.cfg)
|
||
|
|
}
|