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>
- 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>
- api_client.dart: connectTimeout 10s → 60s — Flutter was cancelling
requests to /recommendations after 10s while the server waited for
OpenAI, causing 'context canceled' on the server side
- product/repository.go ListForPrompt: expires_at is computed via
(added_at + storage_days * INTERVAL '1 day'), not a stored column;
wrap in CTE to reference it by alias in SELECT/ORDER BY
- home/handler.go getExpiringSoon: same fix — use CTE to compute
expires_at before filtering/ordering by it
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- internal/home: GET /home/summary endpoint
- Today's meal plan from menu_plans/menu_items
- Logged calories sum from meal_diary
- Daily goal from user profile (default 2000)
- Expiring products within 3 days
- Last 3 saved recommendations (no AI call on home load)
- Wire homeHandler in server.go and main.go
Flutter:
- shared/models/home_summary.dart: HomeSummary, TodaySummary,
TodayMealPlan, ExpiringSoon, HomeRecipe
- features/home/home_service.dart + home_provider.dart
- features/home/home_screen.dart: greeting, calorie progress bar,
today's meals card, expiring banner, quick actions row,
recommendations horizontal list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pgx v5 cannot scan a PostgreSQL DATE column directly into a Go string.
The WHERE clause already used week_start::text but the SELECT did not,
causing GetByWeek to return a scan error that the GenerateMenu handler
swallowed without logging (just returned 500).
- repository.go: SELECT mp.week_start::text instead of mp.week_start
- handler.go: add slog.Error before the silent 500 branch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Requesting 21 full recipes in one prompt exceeds the token budget and
returns 403 Forbidden. Replace the single-call approach with three
concurrent GenerateRecipes calls (breakfast ×7, lunch ×7, dinner ×7),
then assemble the 7-day plan from the results.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace decommissioned llama-3.2-11b-vision-preview with
meta-llama/llama-4-scout-17b-16e-instruct (Groq deprecation)
- Use XFile.readAsBytes() instead of File(path).readAsBytes() so
Android content URIs (from gallery picks) are read correctly
- Add maxWidth/maxHeight constraints to image picker calls to reduce
payload size
- Increase receiveTimeout from 30s to 120s to accommodate slow vision AI
- Log recognition errors via debugPrint instead of swallowing them
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TIMESTAMPTZ + INTERVAL is STABLE (depends on timezone), not IMMUTABLE,
so PostgreSQL rejects it in GENERATED ALWAYS AS STORED columns.
Fix: remove generated column and compute expires_at inline in each query
as (added_at + storage_days * INTERVAL '1 day').
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend (Go):
- Project structure with chi router, pgxpool, goose migrations
- JWT auth (access/refresh tokens) with Firebase token verification
- NoopTokenVerifier for local dev without Firebase credentials
- PostgreSQL user repository with atomic profile updates (transactions)
- Mifflin-St Jeor calorie calculation based on profile data
- REST API: POST /auth/login, /auth/refresh, /auth/logout, GET/PUT /profile, GET /health
- Middleware: auth, CORS (localhost wildcard), logging, recovery, request_id
- Unit tests (51 passing) and integration tests (testcontainers)
- Docker Compose setup with postgres healthcheck and graceful shutdown
Flutter client:
- Riverpod state management with GoRouter navigation
- Firebase Auth (email/password + Google sign-in with web popup support)
- Platform-aware API URLs (web/Android/iOS)
- Dio HTTP client with JWT auth interceptor and concurrent refresh handling
- Secure token storage
- Screens: Login, Register, Home (tabs: Menu, Recipes, Products, Profile)
- Unit tests (17 passing)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>