buildRecipePrompt ignored req.Lang entirely and hardcoded two
English instructions in the prompt. Added targetLang resolution
using the existing langNames map (from recognition.go) and
threaded it into both "Generate N recipes in …" and "All text
fields … MUST be in …" instructions. The image_query field
remains English since it is passed to the Pexels photo API.
Fixes recommendations and menu recipes being returned in English
regardless of the Accept-Language header.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Inserts a new PlanProductsSheet as step 1 of the planning flow.
Users see their current products as a multi-select checklist (all
selected by default) before choosing the planning mode and dates.
- Empty state explains the benefit and offers "Add products" CTA
while always allowing "Plan without products" to skip
- Selected product IDs flow through PlanMenuSheet →
PlanDatePickerSheet → MenuService.generateForDates → backend
- Backend: added ProductIDs field to generate-menu request body;
uses ListForPromptByIDs when set, ListForPrompt otherwise
- Backend: added Repository.ListForPromptByIDs (filtered SQL query)
- All 12 ARB locale files updated with planProducts* keys
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The recipes tab (recommendations + saved recipes + detail screen) is
removed from the app UI and all feature files are deleted. The section
will be redesigned from scratch.
- Remove /recipes and /recipe-detail routes from app_router.dart
- Remove Recipes tab from BottomNavigationBar
- Delete client/lib/features/recipes/ entirely
- Keep shared/models/recipe.dart and saved_recipe.dart for later reuse
- Add "Раздел рецептов (переработка с нуля)" section to docs/TODO.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On first open the ListView started at offset 0 (future end), leaving
today's pill out of view. Also, navigation always scrolled the pill to
the left edge instead of centering it.
Added _centeredOffset() helper and a post-frame jumpTo in initState()
so the initially selected date is centered immediately. Updated
_jumpToToday() and didUpdateWidget() to use the same centering formula.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix _isoWeek: correct Sunday shift (-3 instead of +4), use floor+1 formula,
match jan1 timezone to input — planned meals now appear correctly for UTC+ users
- Add kPlanningHorizonDays=28 / kMenuPastWeeks=8 constants; apply to home date
strip, plan picker (strip + calendar), and menu screen prev/next navigation
- Menu screen week nav: disable arrows at min/max limits using compareTo
- Home screen: replace _GenerateActionCard/_WeekPlannedChip conditional with
always-visible _FutureDayPlanButton(dateString); show _DayPlannedChip only
when the specific day has planned meals; remove standalone _PlanMenuButton
- _FutureDayPlanButton uses selected date as defaultStart instead of lastPlanned+1
- Rename weekPlannedLabel -> dayPlannedLabel across all 12 locales
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fold all incremental schema changes into the baseline migration so that
a fresh database only needs to run one file:
- recipe_source enum now includes 'menu'
- recipe_products/recipe_product_translations renamed to
recipe_ingredients/recipe_ingredient_translations; product_id → ingredient_id
- menu_items.meal_type CHECK expanded to all 6 types
(breakfast, second_breakfast, lunch, afternoon_snack, dinner, snack)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend fixes:
- migration 003: add 'menu' value to recipe_source enum (was causing SQLSTATE 22P02)
- migration 004: rename recipe_products→recipe_ingredients, product_id→ingredient_id (was causing SQLSTATE 42P01)
- dish/repository.go: fix INSERT INTO tags using $1/$1 for two columns → $1/$2 (was causing SQLSTATE 42P08)
- home/handler.go: replace non-existent saved_recipes table with correct joins (recipes→dishes→dish_translations, user_saved_recipes) so today's plan and recommendations load correctly
- reqlog: new slog.Handler wrapper that adds request_id and stack trace to ERROR-level logs
- all handlers: slog.Error→slog.ErrorContext so error logs include request context; writeError includes request_id in response body
Client:
- home_screen.dart: extend home screen to future dates, show planned meals as ghost entries
- l10n: add new localisation keys for home screen date navigation and planned meal UI
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 1: date strip now covers today + 7 future days; right chevron enabled;
future pills rendered in lighter style.
Phase 2: home screen shows DateContext (past/today/future):
- future dates: hide calorie ring + macros, show PlanningBanner
- plannedMealsProvider derives from cached menuProvider (no extra API call)
- _MealCard shows ghost PlannedSlotTile for unconfirmed menu slots
- "Mark as eaten" creates a diary entry (source: menu_plan) via existing API
New l10n keys (12 locales): planningForDate, markAsEaten, plannedMealLabel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
VARCHAR(255) was too narrow for some OpenFoodFacts product names.
Switching to TEXT removes the length constraint entirely.
Also trim whitespace from resolved canonical names during import.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- GET /dishes/search — hybrid FTS (english + simple) + trgm + ILIKE search
- GET /diary/recent — recently used dishes and products for the current user
- product search upgraded: FTS on canonical_name and product_aliases, ranked by GREATEST(ts_rank, similarity)
- importoff: collect product_name_ru/de/fr/... as product_aliases for multilingual search (e.g. "сникерс" → "Snickers")
- migrations: FTS + trgm indexes merged into 001_initial_schema.sql (002 removed)
Flutter:
- FoodSearchSheet: debounced search field, recently-used section, product/dish results, scan-photo and barcode chips
- DishPortionSheet: quick ½/1/1½/2 buttons + custom input
- + button in meal card now opens FoodSearchSheet instead of going directly to AI scan
- 7 new l10n keys across all 12 languages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- import-off: show limit/batch/min-scans params in make command examples
- CLAUDE.md: clarify that parameter changes to make targets also require README update
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rename catalog: ingredient/* → product/* (canonical_name, barcode, nutrition per 100g)
- Rename pantry: product/* → userproduct/* (user-owned items with expiry)
- Squash migrations into single 001_initial_schema.sql (clean-db baseline)
- product_categories: add English canonical name column; fix COALESCE in queries
- Remove product_translations: product names are stored in their original language
- Add default_unit_name to product API responses via unit_translations JOIN
- Add cmd/importoff: bulk import from OpenFoodFacts JSONL dump (COPY + ON CONFLICT)
- Diary: support product_id entries alongside dish_id (CHECK num_nonnulls = 1)
- Home: getLoggedCalories joins both recipes and catalog products
- Flutter: rename models/providers/services to match backend rename
- Flutter: add barcode scan flow for diary (mobile_scanner, product_portion_sheet)
- Flutter: localise 6 new keys across 12 languages (barcode scan, portion weight)
- Routes: GET /products/search, GET /products/barcode/{barcode}, /user-products
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs caused a FK constraint violation on products.unit:
1. RecognizedItem.fromJson fell back to 'шт' (Cyrillic, not a valid
units.code) when the AI returned a null unit — changed to 'pcs'.
2. The unit dropdown in RecognitionConfirmScreen displayed units.keys.first
for invalid units but never updated item.unit, so the invalid value was
still submitted. Added a reconcile step in build() that syncs item.unit
to units.keys.first whenever the stored value is not in the valid set.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace all hardcoded Russian strings in ScanScreen and _LoadingDialog
with AppLocalizations keys; add addFromReceiptOrPhoto, chooseMethod,
photoReceipt, photoReceiptSubtitle, photoProducts, photoProductsSubtitle,
recognizing keys to all 12 ARB files and regenerate AppLocalizations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add flutter_localizations + intl, 12 ARB files (en/ru/es/de/fr/it/pt/zh/ja/ko/ar/hi),
replace all hardcoded Russian UI strings with AppLocalizations, detect system locale
on first launch, localise bottom nav bar labels, document rule in CLAUDE.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Replace dual-consumer priority WorkerPool with a single consumer per
worker process. WORKER_PLAN=paid|free selects the Kafka topic and
consumer group ID (dish-recognition-paid / dish-recognition-free).
docker-compose now runs worker-paid and worker-free as separate services
for independent scaling. Makefile dev target launches both workers locally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add EXTERNAL listener on port 9093 to Kafka so local processes can connect
- Add KAFKA_BROKERS=localhost:9093 to .env.example
- Add dev/dev-infra-up/dev-infra-down targets to Makefile
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Kafka consumers and WorkerPool are moved out of the server process into
a dedicated worker binary. Server now handles HTTP + SSE only; worker
handles Kafka consumption and AI processing.
- cmd/worker/main.go + init.go: new binary with minimal config
(DATABASE_URL, OPENAI_API_KEY, KAFKA_BROKERS)
- cmd/server: remove WorkerPool, paidConsumer, freeConsumer
- Dockerfile: builds both server and worker binaries
- docker-compose.yml: add worker service
- Makefile: add run-worker and docker-logs-worker targets
- README.md: document worker startup and env vars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
RecognizeDish() and buildRecipePrompt() were generating text in the
user's language and storing it in base tables, violating the project
rule that base tables always hold English canonical text.
- RecognizeDish(): hardcode English for dish_name; enrichDishInBackground()
now correctly translates FROM English into all other languages
- buildRecipePrompt(): remove langName lookup, hardcode English for all
text fields; drop unused locale import
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove denormalized columns (name, calories, protein_g, fat_g, carbs_g)
from meal_diary. Name is now resolved via JOIN with dishes/dish_translations;
macros are computed as recipe.*_per_serving * portions at query time.
- Add dish.Repository.FindOrCreateRecipe: finds or creates a minimal recipe
stub seeded with AI-estimated macros
- recognition/handler: resolve recipe_id synchronously per candidate;
simplify enrichDishInBackground to translations-only
- diary/handler: accept dish_id OR name; always resolve recipe_id via
FindOrCreateRecipe before INSERT
- diary/entity: DishID is now non-nullable string; CreateRequest drops macros
- diary/repository: ListByDate and Create use JOIN to return computed macros
- ai/types: add RecipeID field to DishCandidate
- Update tests and wire_gen accordingly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove "Определить блюдо" from ScanScreen and the /scan/dish route.
The + button on each meal card now triggers dish recognition inline —
picks image, shows loading dialog, then presents DishResultSheet as a
modal bottom sheet. After adding to diary the sheet closes and the user
stays on home.
Also fix Navigator.pop crash: showDialog uses the root navigator by
default, so capture Navigator.of(context, rootNavigator: true) before
the async gap and use it to close the loading dialog.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace static 7-day ListView with a scrollable strip (365 days back,
today at the right edge) and a header row with:
- chevron buttons to step one day at a time
- selected date label ("Вт, 18 марта")
- "Сегодня" shortcut that appears when a past date is selected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Avoids conflict with other local Postgres instances.
Container internal port stays 5432; only the host-side mapping changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- Translate all recognition prompts (receipt, products, dish) from Russian to English
- Add lang parameter to Recognizer interface and pass locale.FromContext in handlers
- DishResult type uses candidates array for multi-candidate responses
Client:
- Add meal tracking: diary provider, date selector, meal type model
- DishResult parser: backward-compatible with legacy flat format and new candidates format
- DishResultScreen: sticky bottom button, full-width portion/meal-type inputs,
КБЖУ disclaimer moved under nutrition card, add date field to diary POST body
- Recognition prompts now return dish/product names in user's preferred language
- Onboarding, profile, home screen visual updates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The home screen CaloriesCard now uses a circular ring (CustomPainter)
instead of a LinearProgressIndicator. Ring colour is determined by the
user's goal type (lose / maintain / gain) with inverted semantics for
the gain goal — red means undereating, not overeating.
Overflow beyond 100% is shown as a thinner second-lap arc in the same
warning colour. Numbers (logged kcal, goal, remaining/overage) are
displayed both inside the ring and in a stat column to the right.
Adds docs/calorie_ring_color_spec.md with the full colour threshold
specification per goal type.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduce 6-step onboarding screen (Goal → Gender → DOB → Height+Weight
→ Activity → Calories) with per-step accent colors, hero illustration
area (concentric circles + icon), and white card content panel.
Backend user entity and service updated to support onboarding fields
(goal, activity, height, weight, DOB, dailyCalories). Router guards
unauthenticated and onboarding-incomplete users. Profile service and
screen updated to expose language and onboarding preferences.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
goose splits SQL on semicolons and misinterprets dollar-quoted strings
($$...$$) as statement boundaries, causing a parse error on the PL/pgSQL
function body. StatementBegin/End annotations tell goose to treat the
block as a single statement.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The examples/ directory contains sample images used for manual testing only
and should not be versioned. Added rule to CLAUDE.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Document snake_case for file names and Go convention (no separators)
for package directory names. All existing names are already compliant.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The file no longer contains Firebase-specific code — only the TokenVerifier
interface. Rename to reflect its actual purpose.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Create internal/adapters/firebase/auth.go with Auth, noopAuth, NewAuthOrNoop
(renamed from FirebaseAuth, noopTokenVerifier, NewFirebaseAuthOrNoop)
- Reduce internal/auth/firebase.go to TokenVerifier interface only
- Remove PhotoSearcher interface from adapters/pexels (belongs to consumers)
- Update wire.go and wire_gen.go to use firebase.NewAuthOrNoop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add internal/adapters/ai/types.go with neutral shared types
(Recipe, DayPlan, RecognizedItem, IngredientClassification, etc.)
- Remove types from internal/adapters/openai/ — adapter now uses ai.*
- Define Recognizer interface in recognition package
- Define MenuGenerator interface in menu package
- Define RecipeGenerator interface in recommendation package
- Handler structs now hold interfaces, not *openai.Client
- Add wire.Bind entries for the three new interface bindings
To swap OpenAI for another provider: implement the three interfaces
using ai.* types and change the wire.Bind lines in cmd/server/wire.go.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All test files relocated from internal/X/ to tests/X/ and converted
to package X_test, using only the public API of each package.
- tests/auth/: jwt, service, handler integration tests
- tests/middleware/: auth, request_id, recovery tests
- tests/user/: calories, service, repository integration tests
- tests/locale/: locale tests (already package locale_test, just moved)
- tests/ingredient/: repository integration tests
- tests/recipe/: repository integration tests
mockUserRepo in tests/user/service_test.go redefined locally with
fully-qualified user.* types. Unexported auth.refreshRequest replaced
with a local testRefreshRequest struct in the integration test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add google/wire; generate wire_gen.go from wire.go injector
- Move all constructor wiring out of main.go into providers.go / wire.go
- Export recognition.IngredientRepository (was unexported, blocked Wire binding)
- Delete units/cuisine/tag registry.go files (global state + unused NameFor helpers)
- Replace List handlers with NewListHandler(pool) using COALESCE SQL queries
- Remove units/cuisine/tag LoadFromDB from server startup; locale.LoadFromDB kept
(locale.Supported is used by language middleware on every request)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>