- 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>
- Add units + unit_translations tables with FK constraints on products and ingredient_mappings
- Normalize products.unit from Russian strings (г, кг) to English codes (g, kg)
- Load units at startup (in-memory registry) and serve via GET /units (language-aware)
- Replace hardcoded _units lists and _mapUnit() functions in Flutter with unitsProvider FutureProvider
- Re-fetches automatically when language changes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- migration 013: create languages table (code PK, native_name, english_name,
is_active, sort_order) with all 12 existing languages seeded
- locale: add Language struct, Languages []Language, LoadFromDB() — queries
languages table at startup and populates both Supported map and Languages
slice; existing Parse/FromContext/FromRequest unchanged
- main.go: call locale.LoadFromDB after pool is ready
- gemini/recipe.go: remove hardcoded langNames map, use locale.Languages
linear lookup for English name in prompt
- language/handler.go: new package with GET /languages handler returning
active languages list (no auth required)
- server.go: register GET /languages as public route
- Flutter: add LanguageRepository + languageRepositoryProvider that fetches
/languages from backend
- language_provider.dart: replace const supportedLanguages map with
supportedLanguagesProvider (FutureProvider) backed by LanguageRepository
- profile_provider.dart: remove supportedLanguages.containsKey validation —
backend is source of truth; sync any non-empty language from preferences
- profile_screen.dart: use supportedLanguagesProvider for display name and
dropdown (async with loading/error states)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Store date_of_birth (DATE) instead of a static age integer so that age
is always computed dynamically from the stored date of birth.
- Migration 011: adds date_of_birth, backfills from age, drops age
- AgeFromDOB helper computes current age from YYYY-MM-DD string
- User model, repository SQL, and service validation updated
- Flutter: User.age becomes a computed getter; profile edit screen
uses a date picker bounded to [today-120y, today-10y]
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Passing the outer widget context to Navigator.pop() inside a dialog or
bottom sheet builder caused GoRouter to pop a page route instead of the
modal, triggering the "no pages left to show" assertion.
Affects _showChangeDialog and _confirmGenerate in menu_screen.dart,
and _showAddMenu bottom sheet in products_screen.dart.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add languageProvider (StateProvider<String>, default 'ru') with
supportedLanguages map matching backend locale.Supported
- Wire Accept-Language header into AuthInterceptor via languageGetter
callback; all API requests now carry the current language
- Sync language from user profile preferences into languageProvider
on every ProfileNotifier load/update
- Add language field to UpdateProfileRequest, serialized as
preferences.language in PUT /profile
- Profile screen: НАСТРОЙКИ section displays current language;
edit sheet adds DropdownButtonFormField for language selection
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add internal/locale package: Parse(Accept-Language), FromContext/WithLang helpers, 12 supported languages
- Add Language middleware that reads Accept-Language header and stores lang in context
- Register Language middleware globally in server router (after CORS)
Database migrations:
- 009: create recipe_translations, saved_recipe_translations, ingredient_translations tables; migrate existing _ru data
- 010: drop legacy _ru columns (title_ru, description_ru, canonical_name_ru); update FTS index
Models: remove all _ru fields (TitleRu, DescriptionRu, NameRu, UnitRu, CanonicalNameRu)
Repositories:
- recipe: Upsert drops _ru params; GetByID does LEFT JOIN COALESCE on recipe_translations; ListMissingTranslation(lang); UpsertTranslation
- ingredient: same pattern with ingredient_translations; Search now queries translated names/aliases
- savedrecipe: List/GetByID LEFT JOIN COALESCE on saved_recipe_translations; UpsertTranslation
Gemini:
- RecipeRequest/MenuRequest gain Lang field
- buildRecipePrompt rewritten in English with target-language content instruction; image_query always in English
- GenerateMenu propagates Lang to GenerateRecipes
Handlers:
- recommendation/menu: pass locale.FromContext(ctx) as Lang
- recognition: saveClassification stores Russian translation via UpsertTranslation instead of _ru column
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Define uuid_generate_v7() in the first migration and switch all table
primary key defaults to it. Remove the uuid-ossp extension dependency.
Update refresh token and request ID generation in Go code to use
uuid.NewV7() from the existing google/uuid v1.6.0 library.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
routerProvider was watching authProvider and returning a new GoRouter
on every status transition (unknown → authenticated). This caused
MaterialApp.router to rebuild the entire navigation tree, which
triggered all data providers to start loading before auth was confirmed.
Switch to refreshListenable pattern: GoRouter is created once,
_RouterNotifier fires notifyListeners() when auth changes, and GoRouter
re-runs redirect using ref.read(authProvider). Add /loading splash route
shown during AuthStatus.unknown so no authenticated screen (and no API
call) is initiated until the stored-token check completes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Navigator.pop(context) with the outer tile context caused go_router to
pop the main navigation stack instead of closing the dialog. Use the
builder's ctx parameter instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add ProfileService (GET/PUT /profile), ProfileNotifier provider,
and full ProfileScreen with body-params, goal/activity, daily-calories
sections and logout confirmation. EditProfileSheet lets user update
name, height, weight, age, gender, goal and activity; backend
auto-recalculates daily_calories via Mifflin-St Jeor.
HomeScreen greeting now shows the user's real name from profileProvider.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switch AppColors to iOS system palette (007AFF blue, F2F2F7 grouped
background, separator, label hierarchy) and rewrite AppTheme with
iOS-inspired Material 3 tokens (no elevation, negative letter-spacing,
50px buttons, 12px radii). Replace removed primaryLight/accent references
in recipe screens with primary.withValues(alpha:0.15) and primary.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>