caltrack/api/handlers.go
Bastian Gruber 6b6778508f
Some checks are pending
Build and Push / build (push) Waiting to run
fix: store entries for the day I am currently seeing
2026-02-24 12:19:54 +00:00

309 lines
7.6 KiB
Go

package api
import (
"caltrack/ai"
"caltrack/calc"
"caltrack/config"
"caltrack/db"
"caltrack/types"
"encoding/json"
"log"
"net/http"
"strconv"
)
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, h.cfg.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
}
now := h.cfg.Now()
entryDate := now.Format("2006-01-02")
entryTime := now.Format("15:04")
if req.Date != "" {
entryDate = req.Date
if req.Date != now.Format("2006-01-02") {
entryTime = "12:00"
}
}
var entries []types.Entry
for _, item := range estimation.Items {
desc := item.Name
if item.Brand != "" {
desc = item.Name + " (" + item.Brand + ")"
}
entry := types.Entry{
Date: entryDate,
Time: entryTime,
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(h.cfg.Now().Format("2006-01-02"), 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)
}