feat: dish recognition job context, diary linkage, home widget, history page
Backend: - Rename recognition_jobs → dish_recognition_jobs; add target_date and target_meal_type columns to capture scan context at submission time - Add job_id FK on meal_diary so entries are linked to their origin job - New GET /ai/jobs endpoint returns today's unlinked jobs for the current user - diary.Entry and CreateRequest gain job_id field; repository reads/writes it - CORS middleware: allow Accept-Language and Cache-Control headers - Logging middleware: implement http.Flusher on responseWriter (needed for SSE) - Consolidate migrations into a single 001_initial_schema.sql Flutter: - POST /ai/recognize-dish now sends target_date and target_meal_type - DishResultSheet accepts jobId; _addToDiary includes it in the diary payload, saves last-used meal type to SharedPreferences, invalidates todayJobsProvider - TodayJobsNotifier + todayJobsProvider: loads unlinked jobs via GET /ai/jobs - Home screen shows _TodayJobsWidget (up to 3 tiles) between macros and meals; tapping a done tile reopens DishResultSheet with the stored result - Quick Actions row: third button "История" → /scan/history - New RecognitionHistoryScreen: full-screen list of today's unlinked jobs - LocalPreferences wrapper over SharedPreferences (last_used_meal_type) - app_theme: apply Google Fonts Roboto as default font family Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -84,6 +84,14 @@ type imageRequest struct {
|
||||
MimeType string `json:"mime_type"`
|
||||
}
|
||||
|
||||
// recognizeDishRequest is the body for POST /ai/recognize-dish.
|
||||
type recognizeDishRequest struct {
|
||||
ImageBase64 string `json:"image_base64"`
|
||||
MimeType string `json:"mime_type"`
|
||||
TargetDate *string `json:"target_date"`
|
||||
TargetMealType *string `json:"target_meal_type"`
|
||||
}
|
||||
|
||||
// imagesRequest is the request body for multi-image endpoints.
|
||||
type imagesRequest struct {
|
||||
Images []imageRequest `json:"images"`
|
||||
@@ -173,9 +181,9 @@ func (handler *Handler) RecognizeProducts(responseWriter http.ResponseWriter, re
|
||||
|
||||
// RecognizeDish handles POST /ai/recognize-dish (async).
|
||||
// Enqueues the image for AI processing and returns 202 Accepted with a job_id.
|
||||
// Body: {"image_base64": "...", "mime_type": "image/jpeg"}
|
||||
// Body: {"image_base64": "...", "mime_type": "image/jpeg", "target_date": "2006-01-02", "target_meal_type": "lunch"}
|
||||
func (handler *Handler) RecognizeDish(responseWriter http.ResponseWriter, request *http.Request) {
|
||||
var req imageRequest
|
||||
var req recognizeDishRequest
|
||||
if decodeError := json.NewDecoder(request.Body).Decode(&req); decodeError != nil || req.ImageBase64 == "" {
|
||||
writeErrorJSON(responseWriter, http.StatusBadRequest, "image_base64 is required")
|
||||
return
|
||||
@@ -186,11 +194,13 @@ func (handler *Handler) RecognizeDish(responseWriter http.ResponseWriter, reques
|
||||
lang := locale.FromContext(request.Context())
|
||||
|
||||
job := &Job{
|
||||
UserID: userID,
|
||||
UserPlan: userPlan,
|
||||
ImageBase64: req.ImageBase64,
|
||||
MimeType: req.MimeType,
|
||||
Lang: lang,
|
||||
UserID: userID,
|
||||
UserPlan: userPlan,
|
||||
ImageBase64: req.ImageBase64,
|
||||
MimeType: req.MimeType,
|
||||
Lang: lang,
|
||||
TargetDate: req.TargetDate,
|
||||
TargetMealType: req.TargetMealType,
|
||||
}
|
||||
if insertError := handler.jobRepo.InsertJob(request.Context(), job); insertError != nil {
|
||||
slog.Error("insert recognition job", "err", insertError)
|
||||
@@ -221,6 +231,24 @@ func (handler *Handler) RecognizeDish(responseWriter http.ResponseWriter, reques
|
||||
})
|
||||
}
|
||||
|
||||
// ListTodayJobs handles GET /ai/jobs — returns today's unlinked jobs for the current user.
|
||||
func (handler *Handler) ListTodayJobs(responseWriter http.ResponseWriter, request *http.Request) {
|
||||
userID := middleware.UserIDFromCtx(request.Context())
|
||||
|
||||
summaries, listError := handler.jobRepo.ListTodayUnlinked(request.Context(), userID)
|
||||
if listError != nil {
|
||||
slog.Error("list today unlinked jobs", "err", listError)
|
||||
writeErrorJSON(responseWriter, http.StatusInternalServerError, "failed to list jobs")
|
||||
return
|
||||
}
|
||||
|
||||
// Return an empty array instead of null when there are no results.
|
||||
if summaries == nil {
|
||||
summaries = []*JobSummary{}
|
||||
}
|
||||
writeJSON(responseWriter, http.StatusOK, summaries)
|
||||
}
|
||||
|
||||
// GetJobStream handles GET /ai/jobs/{id}/stream — SSE endpoint for job updates.
|
||||
func (handler *Handler) GetJobStream(responseWriter http.ResponseWriter, request *http.Request) {
|
||||
handler.sseBroker.ServeSSE(responseWriter, request)
|
||||
|
||||
Reference in New Issue
Block a user