Files
food-ai/docs/Flow.md
dbastrikin e57ff8e06c feat: implement Iteration 1 — AI recipe recommendations
Backend:
- Add Groq LLM client (llama-3.3-70b) for recipe generation with JSON
  retry strategy (retries only on parse errors, not API errors)
- Add Pexels client for parallel photo search per recipe
- Add saved_recipes table (migration 004) with JSONB fields
- Add GET /recommendations endpoint (profile-aware prompt building)
- Add POST/GET/GET{id}/DELETE /saved-recipes CRUD endpoints
- Wire gemini, pexels, recommendation, savedrecipe packages in main.go

Flutter:
- Add Recipe, SavedRecipe models with json_serializable
- Add RecipeService (getRecommendations, getSavedRecipes, save, delete)
- Add RecommendationsNotifier and SavedRecipesNotifier (Riverpod)
- Add RecommendationsScreen with skeleton loading and refresh FAB
- Add RecipeDetailScreen (SliverAppBar, nutrition tooltip, steps with timer)
- Add SavedRecipesScreen with Dismissible swipe-to-delete and empty state
- Update RecipesScreen to TabBar (Recommendations / Saved)
- Add /recipe-detail route outside ShellRoute (no bottom nav)
- Extend ApiClient with getList() and deleteVoid()

Project:
- Add CLAUDE.md with English-only rule for comments and commit messages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 22:43:29 +02:00

645 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Flow: взаимодействие пользователя → бэкенда → сторонних API
## Содержание
1. [Архитектура системы](#1-архитектура-системы)
2. [Ключевой принцип: сторонние API только там где нужны](#2-ключевой-принцип)
3. [Flow 1: Аутентификация](#3-flow-1-аутентификация)
4. [Flow 2: Обновление токена](#4-flow-2-обновление-токена)
5. [Flow 3: Профиль пользователя](#5-flow-3-профиль-пользователя)
6. [Flow 4: Рекомендации рецептов](#6-flow-4-рекомендации-рецептов)
7. [Flow 5: Сохранённые рецепты](#7-flow-5-сохранённые-рецепты)
8. [Flow 6: Управление продуктами (Итерация 2)](#8-flow-6-управление-продуктами-итерация-2)
9. [Flow 7: Распознавание продуктов (Итерация 3)](#9-flow-7-распознавание-продуктов-итерация-3)
10. [Flow 8: Планирование меню (Итерация 4)](#10-flow-8-планирование-меню-итерация-4)
11. [Анализ потребления сторонних API](#11-анализ-потребления-сторонних-api)
12. [Количество запросов к бэкенду по сценариям](#12-количество-запросов-к-бэкенду-по-сценариям)
13. [Сводная таблица](#13-сводная-таблица)
---
## 1. Архитектура системы
```
┌────────────────────────────────────────────────────────────────────┐
│ Flutter Client │
│ (Android / iOS / Web) │
│ - Firebase Auth (Google / Apple / Email) │
│ - Dio HTTP Client + Auth Interceptor │
│ - FlutterSecureStorage (токены) │
│ - Riverpod (состояние) │
└────────────────────────┬───────────────────────────────────────────┘
│ HTTPS, Bearer JWT
┌────────────────────────▼───────────────────────────────────────────┐
│ Go Backend (chi v5) │
│ - JWT middleware (HS256, верификация без Firebase в рантайме) │
│ - Gemini API (генерация рекомендаций рецептов) │
│ - Pexels API (подбор фотографий к рецептам) │
│ - Firebase Admin SDK (только при логине) │
│ - Калькулятор КБЖУ (Mifflin-St Jeor, локально) │
└──────┬─────────────────────────────────────────────────────────────┘
┌──────▼──────────────────────────────────────────────────────────────┐
│ PostgreSQL 15 │
│ users · saved_recipes · products (Iter.2) · ingredient_mappings │
└─────────────────────────────────────────────────────────────────────┘
Сторонние API:
┌────────────────────┐ ┌─────────────────────────────────────┐
│ Firebase Auth │ │ Google Gemini 2.0 Flash │
│ только логин │ │ генерация рецептов-рекомендаций │
└────────────────────┘ └─────────────────────────────────────┘
┌────────────────────┐
│ Pexels API │
│ фото к рецептам │
└────────────────────┘
```
---
## 2. Ключевой принцип
**Сторонние API вызываются только при конкретных пользовательских действиях**, а не фоновых задачах:
| API | Когда вызывается | Частота |
|-----|-----------------|---------|
| **Firebase Auth** | Только `POST /auth/login` | 1 раз за сессию |
| **Gemini** | `GET /recommendations`, `POST /ai/recognize-*`, `POST /ai/generate-menu` | По запросу пользователя |
| **Pexels** | Внутри рекомендаций и генерации меню | 1 вызов на рецепт |
Все токены, профили и сохранённые рецепты хранятся в PostgreSQL и раздаются **без внешних вызовов**.
---
## 3. Flow 1: Аутентификация
### 3.1 Первый вход (Google OAuth)
```
Пользователь Flutter Firebase Go Backend PostgreSQL
│ │ │ │ │
│── тапает "Войти" ─►│ │ │ │
│ │── signInWithGoogle()►│ │ │
│ │◄── idToken ─────────│ │ │
│ │ │ │ │
│ │── POST /auth/login ─────────────────────►│ │
│ │ { firebase_token: idToken } │ │
│ │ │◄── VerifyIDToken() ─│ │
│ │ │──► { uid, email } ──►│ │
│ │ │ │── UPSERT users ──►│
│ │ │ │◄── User{id, plan}─│
│ │ │ │── генерирует JWT │
│ │ │ │── UPDATE refresh ─►│
│ │◄── { access_token, refresh_token, user } ─│ │
│ │── сохраняет в SecureStorage │ │
│◄── Home Screen ───│ │ │ │
```
**Запросы на бэкенд:** 1
**Вызовов Firebase:** 1 (VerifyIDToken — серверная верификация)
**SQL:** 2 (UPSERT users + UPDATE refresh_token)
### 3.2 Последующие запросы (с JWT)
```
Flutter ── GET /profile ──► middleware.Auth
└── ValidateAccessToken() — локально, HMAC HS256
Firebase НЕ вызывается
──► handler ──► SELECT users WHERE id=$1
```
**Вызовов Firebase:** 0 (JWT верифицируется локально по секрету)
---
## 4. Flow 2: Обновление токена
Происходит автоматически в `AuthInterceptor` при 401 или истечении токена (15 мин TTL).
```
Flutter (AuthInterceptor) Go Backend PostgreSQL
│ │ │
│── POST /auth/refresh ──────►│ │
│ { refresh_token } │── SELECT users WHERE ──►│
│ │ refresh_token=$1 AND │
│ │ token_expires_at>now()│
│ │◄── User ───────────────│
│ │── новый JWT + UUID │
│ │── UPDATE users ────────►│
│◄── { access_token, │ │
│ refresh_token } │ │
│── повторяет исходный запрос│ │
```
**Запросов к Firebase:** 0
**SQL:** 2 (SELECT + UPDATE)
---
## 5. Flow 3: Профиль пользователя
### Просмотр
```
Flutter → GET /profile → SELECT * FROM users WHERE id=$1
```
**Запросов на бэкенд:** 1 | **SQL:** 1
### Обновление (онбординг)
```
Flutter → PUT /profile → Go Backend
{ height_cm, weight_kg, │
age, gender, │── Mifflin-St Jeor (локально):
activity, goal } │ BMR = 10W + 6.25H - 5A + offset
│ TDEE = BMR × activity_factor
│ Calories = TDEE ± goal_delta
│── UPDATE users SET daily_calories=...
```
**Запросов к сторонним API:** 0 (всё считается на Go)
**Запросов на бэкенд:** 1 | **SQL:** 1
---
## 6. Flow 4: Рекомендации рецептов
Центральный flow приложения. Вызывается когда пользователь открывает экран рекомендаций.
### 6.1 Полный flow
```
Пользователь Flutter Go Backend Gemini Pexels PostgreSQL
│ │ │ │ │ │
│── открывает │ │ │ │ │
│ экран ───►│ │ │ │ │
│ │── GET /recommendations ─────────►│ │ │
│ │ ?count=5 │ │ │
│ │ │ │ │
│ │ │── SELECT user ──────────────────────────────── ►│
│ │ │ profile + products (Iter.2) ◄────────────────│
│ │ │ │ │ │
│ │ │── GenerateContent(prompt) ────►│ │
│ │ │ prompt содержит: │ │
│ │ │ - цель пользователя │ │
│ │ │ - дневные калории │ │
│ │ │ - список продуктов (Iter.2) │ │
│ │ │ - N=5 рецептов │ │
│ │ │◄── JSON: [Recipe×5] ───────────│ │
│ │ │ каждый с image_query │ │
│ │ │ │ │ │
│ │ │ для каждого рецепта: │ │
│ │ │── GET /v1/search?query=... ─────────────────── ►│
│ │ │ Authorization: Pexels key │ │
│ │ │◄── { photos[0].src.medium } ───────────────────│
│ │ │ │ │ │
│◄── [Recipe×5 c image_url] ─│ │ │ │
```
### 6.2 Структура промпта для Gemini
```
Ты — диетолог-повар. Предложи {N} рецептов на русском языке.
Профиль пользователя:
- Цель: похудение
- Дневная норма калорий: 1800 ккал
- Ограничения: без глютена (если есть в preferences)
[Итерация 2+] Доступные продукты:
- куриная грудка (500г)
- помидоры (3 шт)
- ...
Требования к каждому рецепту:
- Калорийность на порцию: не более 600 ккал
- Время приготовления: до 40 минут
- Укажи КБЖУ на порцию (приблизительно)
Верни ТОЛЬКО валидный JSON массив без markdown:
[{
"title": "Название рецепта",
"description": "Краткое описание (2-3 предложения)",
"cuisine": "mediterranean",
"difficulty": "easy|medium|hard",
"prep_time_min": 10,
"cook_time_min": 20,
"servings": 2,
"image_query": "grilled chicken breast vegetables mediterranean",
"ingredients": [
{ "name": "Куриная грудка", "amount": 300, "unit": "г" }
],
"steps": [
{ "number": 1, "description": "Нарежьте курицу...", "timer_seconds": null }
],
"tags": ["без глютена", "высокий белок"],
"nutrition_per_serving": {
"calories": 420,
"protein_g": 48,
"fat_g": 12,
"carbs_g": 18
}
}]
```
### 6.3 Кэширование рекомендаций
Рекомендации **не сохраняются** автоматически. При каждом открытии экрана генерируются заново. Это дает:
- Свежесть контента
- Учёт изменившихся продуктов (Итерация 2)
Возможная оптимизация в будущем: кэшировать последний набор рекомендаций в Redis/памяти на 30 минут, инвалидировать при обновлении продуктов.
---
## 7. Flow 5: Сохранённые рецепты
### 7.1 Сохранить рекомендацию
```
Пользователь тапает ❤️ на рецепте
Flutter → POST /saved-recipes → PostgreSQL
{ полный JSON рецепта } └── INSERT INTO saved_recipes
(user_id, title, steps,
ingredients, nutrition,
image_url, ...)
← { id, saved_at }
```
**Запросов на бэкенд:** 1
**Вызовов Gemini/Pexels:** 0 (данные уже есть в клиенте)
**SQL:** 1
### 7.2 Список сохранённых
```
Flutter → GET /saved-recipes → SELECT * FROM saved_recipes
WHERE user_id=$1
ORDER BY saved_at DESC
```
**Запросов на бэкенд:** 1 | **SQL:** 1
### 7.3 Удалить из сохранённых
```
Flutter → DELETE /saved-recipes/{id} → DELETE FROM saved_recipes
WHERE id=$1 AND user_id=$2
```
**Запросов на бэкенд:** 1 | **SQL:** 1
---
## 8. Flow 6: Управление продуктами (Итерация 2)
### 8.1 Открытие списка продуктов
```
Flutter → GET /products → SELECT * FROM products
WHERE user_id=$1
ORDER BY expires_at ASC
```
**Запросов на бэкенд:** 1
### 8.2 Добавление продукта с автодополнением
```
Пользователь вводит "кур" (debounce 300мс)
Flutter → GET /ingredients/search?q=кур → PostgreSQL
├── ILIKE на canonical_name_ru
├── GIN на aliases
└── pg_trgm similarity
Пользователь выбирает "Куриная грудка"
│ поля автозаполняются локально
Flutter → POST /products → INSERT INTO products
{ mapping_id, expires_at GENERATED ALWAYS AS
name, quantity, (added_at + storage_days days)
unit, category,
storage_days }
```
**Запросов на бэкенд:** 35 (поиск, debounce) + 1 (создание)
**Вызовов сторонних API:** 0
### 8.3 Связь продуктов с рекомендациями (Итерация 2+)
После того как у пользователя есть продукты, `GET /recommendations` включает их в промпт для Gemini. Рекомендации становятся персонализированными: "что приготовить из того, что есть".
---
## 9. Flow 7: Распознавание продуктов (Итерация 3)
Пользователь фотографирует чек, холодильник или готовое блюдо — Gemini Vision распознаёт содержимое и заполняет список продуктов.
### 9.1 Распознавание чека
```
Пользователь Flutter Go Backend Gemini PostgreSQL
│ │ │ │ │
│─ фото чека►│ │ │ │
│ │── POST /ai/recognize-receipt ─────────►│ │
│ │ multipart/form-data: image │ │
│ │ │── GenerateContent ►│ │
│ │ │ prompt: OCR чека │ │
│ │ │◄── JSON: [{name, │ │
│ │ │ qty, unit, │ │
│ │ │ category, │ │
│ │ │ confidence}] │ │
│ │ │ │ │
│ │ │ fuzzy match по ingredient_mappings►│
│ │ │◄── mapping_id ─────────────────────│
│ │ │ │ │
│◄── [{name, mapping_id, qty, │ │ │
│ unit, storage_days}] ──────│ │ │
│ │ │ │ │
│─ подтверждает список ─────────►│ │ │
│ POST /products/batch │── INSERT products ►│ │
```
**Запросов на бэкенд:** 2 (recognize + batch insert)
**Gemini:** 1 (vision)
**SQL:** 1 SELECT (fuzzy match) + 1 INSERT batch
### 9.2 Распознавание фото продуктов (холодильник/стол)
Аналогичен чеку, но без цены. Поддерживает несколько фото:
```
Пользователь делает 13 фото холодильника
Flutter → POST /ai/recognize-products → Gemini Vision
(multipart, несколько фото) └── анализирует каждое фото
└── объединяет результаты
(дедупликация по canonical_name)
Backend:
1. Для каждого фото → 1 Gemini-запрос (параллельно)
2. Объединение списков, дедупликация (суммирование количества)
3. Fuzzy match по ingredient_mappings
4. Возврат клиенту для подтверждения
5. POST /products/batch → INSERT
```
**Gemini:** 13 (по числу фото, параллельно)
### 9.3 Распознавание блюда (фото → калории)
```
Пользователь Flutter Go Backend Gemini PostgreSQL
│ │ │ │ │
│─ фото блюда►│ │ │ │
│ │── POST /ai/recognize-dish ────────────►│ │
│ │ │── GenerateContent ►│ │
│ │ │ prompt: распознай│ │
│ │ │ блюдо, КБЖУ │ │
│ │ │◄── {dish_name, │ │
│ │ │ weight_g, │ │
│ │ │ calories, │ │
│ │ │ protein, fat, │ │
│ │ │ carbs, │ │
│ │ │ confidence} │ │
│ │ │ │ │
│ │ │ Опционально: поиск│ │
│ │ │ в saved_recipes │ │
│ │ │ по dish_name │ │
│ │ │ │ │
│◄── {dish, calories, КБЖУ≈, │ │ │
│ matched_recipe?} ──────────│ │ │
│ │ │ │ │
│─ добавить в дневник? ─────────►│ │ │
│ POST /diary │── INSERT meal_diary►│ │
```
**Запросов на бэкенд:** 2 (recognize + diary)
**Gemini:** 1 (vision)
---
## 10. Flow 8: Планирование меню (Итерация 4)
Пользователь запрашивает меню на неделю — Gemini генерирует полный план питания с рецептами на основе продуктов, профиля и целей пользователя.
### 10.1 Генерация меню
```
Пользователь Flutter Go Backend Gemini Pexels PostgreSQL
│ │ │ │ │ │
│─ «Составить │ │ │ │ │
│ меню» ──►│ │ │ │ │
│ │── POST /ai/generate-menu ─────────────►│ │ │
│ │ { period: "week", │ │ │
│ │ meals_per_day: 3 } │ │ │
│ │ │ │ │ │
│ │ SELECT user profile + products ────────────────────────►│
│ │ ◄── {goal, КБЖУ, products list} ───────────────────────│
│ │ │ │ │ │
│ │ │── GenerateContent ►│ │ │
│ │ │ промпт: │ │ │
│ │ │ - профиль юзера │ │ │
│ │ │ - продукты │ │ │
│ │ │ - период 7 дней │ │ │
│ │ │ - 3 приёма/день │ │ │
│ │ │◄── JSON: 21 recipe │ │ │
│ │ │ каждый с │ │ │
│ │ │ image_query │ │ │
│ │ │ │ │ │
│ │ │ для каждого рецепта (параллельно): │
│ │ │── GET /v1/search?query=... ──────────────────────►│
│ │ │◄── photo_url ────────────────────────────────────│
│ │ │ │ │ │
│ │ │── INSERT menu_plans + menu_items ───────────────►│
│ │ │── INSERT saved_recipes (рецепты меню) ──────────►│
│ │ │ │ │ │
│◄── {menu_plan_id, │ │ │ │
│ days: [{day, meals: [ │ │ │ │
│ {meal_type, recipe} │ │ │ │
│ ]}]} ──────────────────────│ │ │ │
```
**Gemini:** 1 (большой промпт, ~21 рецепт)
**Pexels:** до 21 (параллельно; на практике повторяющиеся query кешируются)
**SQL:** 1 SELECT + batch INSERT menu_plans/items + batch INSERT saved_recipes
### 10.2 Просмотр и редактирование меню
```
Flutter → GET /menu?week=2026-W08 → SELECT menu_plans, menu_items WHERE user_id=$1 AND week_start=$2
LEFT JOIN saved_recipes ON menu_items.recipe_id
Flutter → PUT /menu/items/{id} → UPDATE menu_items SET recipe_id=$1
Flutter → DELETE /menu/items/{id} → DELETE menu_items WHERE id=$1 AND user_id=$2
```
**Запросов к бэкенду:** 13 | **Gemini:** 0 | **SQL:** 12
### 10.3 Список покупок из меню
```
Flutter → POST /shopping-list/generate → Go Backend
{ menu_plan_id } ├── SELECT menu_items JOIN saved_recipes
│ WHERE menu_plan_id=$1
├── Агрегация ингредиентов:
│ суммирование по canonical_name
│ вычитание того, что уже есть в products
└── INSERT/UPDATE shopping_lists
Gemini НЕ участвует. Чистая SQL-агрегация.
```
**Gemini:** 0 | **SQL:** 3
---
## 11. Анализ потребления сторонних API
### 9.1 Firebase Auth
| Сценарий | Вызовов |
|----------|---------|
| Логин | 1 (VerifyIDToken) |
| Обычный запрос с JWT | **0** |
| Refresh токена | **0** |
### 9.2 Gemini (Google Gemini 2.0 Flash)
**Тарифы Free tier:**
| Параметр | Значение |
|----------|----------|
| RPM | 15 (Flash) / 30 (Flash-Lite) |
| Запросов/день | 1 500 |
| Токенов/минуту | 1 000 000 |
**Расход на запрос рекомендаций (5 рецептов):**
| Метрика | Значение |
|---------|----------|
| Input токены (промпт + продукты) | ~500800 |
| Output токены (5 рецептов JSON) | ~1 5002 500 |
| Gemini-запросов | **1** |
| Стоимость (Flash, платный) | ~$0.0003 |
**Дневное потребление при 100 активных пользователях (каждый открывает рекомендации 3 раза/день):**
- 100 × 3 = 300 Gemini-запросов/день
- Free tier: 1 500/день → **хватает на ~5× текущую нагрузку**
- Платный Flash: ~$0.09/день = ~$2.7/мес
### 9.3 Pexels API
**Тарифы:**
| Тариф | Запросов/час | Запросов/мес |
|-------|-------------|-------------|
| Free | 200 | 20 000 |
**Расход:**
- 1 рекомендация = 5 рецептов = **5 Pexels-запросов**
- 100 пользователей × 3 рекомендации = 300 запросов/час пик
- ⚠️ **200 req/hour лимит может стать узким местом при пиковой нагрузке**
**Стратегия:** кэшировать image_url в `saved_recipes`, для несохранённых рекомендаций — запрашивать при генерации. При росте нагрузки — кэшировать по `image_query` в Redis (большинство запросов повторяются: "grilled chicken", "pasta carbonara", etc.).
---
## 12. Количество запросов к бэкенду по сценариям
### Первый запуск (новый пользователь)
| Шаг | Endpoint | Сторонний API |
|-----|----------|---------------|
| Вход через Google | POST /auth/login | Firebase (1×) |
| Загрузка профиля | GET /profile | — |
| Онбординг | PUT /profile | — |
| Первые рекомендации | GET /recommendations | Gemini (1×) + Pexels (5×) |
| **Итого** | **4** | **Firebase×1, Gemini×1, Pexels×5** |
### Обычная сессия
| Шаг | Endpoint | Кол-во |
|-----|----------|--------|
| Refresh токена (если истёк) | POST /auth/refresh | 01 |
| Открыть рекомендации | GET /recommendations | 1 |
| Сохранить рецепт | POST /saved-recipes | 1 |
| Открыть сохранённые | GET /saved-recipes | 1 |
| **Итого** | **34** | **Gemini×1, Pexels×5** |
### Сценарий: пользователь не взаимодействует с рекомендациями
```
Открывает приложение → просматривает сохранённые рецепты
Запросов: 1 GET /saved-recipes
Сторонних API: 0
SQL: 1
```
### Детальный breakdown: GET /recommendations
```
1. SELECT users WHERE id=$1 → 1 SQL
2. [Iter.2+] SELECT products WHERE user_id=$1 → 1 SQL
3. Gemini.GenerateContent(prompt) → 1 Gemini req (~13 сек)
4. Pexels.Search(image_query) × 5 (параллельно) → 5 Pexels req (параллельно)
5. Формирование ответа и отдача → 0 SQL
Итого: 12 SQL + 1 Gemini + 5 Pexels
Время ответа: ~24 секунды (доминирует Gemini latency)
```
---
## 13. Сводная таблица
### Сторонние API в рантайме
| API | Trigger | Вызовов на запрос | Free tier (день) |
|-----|---------|------------------|-----------------|
| Firebase Auth | POST /auth/login | 1 | Без ограничений |
| Gemini Flash | GET /recommendations | 1 | 1 500 |
| Gemini Flash | POST /ai/recognize-receipt | 1 | — |
| Gemini Flash | POST /ai/recognize-products | 13 (фото) | — |
| Gemini Flash | POST /ai/recognize-dish | 1 | — |
| Gemini Flash | POST /ai/generate-menu | 1 | — |
| Pexels | GET /recommendations | 5 (параллельно) | ~667 рекомендаций |
| Pexels | POST /ai/generate-menu | до 21 (параллельно) | — |
### Запросы к бэкенду
| Сценарий | Бэкенд | Firebase | Gemini | Pexels |
|----------|--------|----------|--------|--------|
| Первый вход | 1 | 1 | 0 | 0 |
| Просмотр профиля | 1 | 0 | 0 | 0 |
| Обновление профиля | 1 | 0 | 0 | 0 |
| Рекомендации | 1 | 0 | 1 | 5 |
| Сохранить рецепт | 1 | 0 | 0 | 0 |
| Список сохранённых | 1 | 0 | 0 | 0 |
| Удалить из сохранённых | 1 | 0 | 0 | 0 |
| Refresh токена | 1 | 0 | 0 | 0 |
| Распознавание чека | 2 | 0 | 1 | 0 |
| Распознавание фото продуктов | 2 | 0 | 13 | 0 |
| Распознавание блюда | 2 | 0 | 1 | 0 |
| Генерация меню (неделя) | 1 | 0 | 1 | до 21 |
| Просмотр меню | 1 | 0 | 0 | 0 |
| Список покупок из меню | 1 | 0 | 0 | 0 |
### Ключевые выводы
1. **Критические пути требуют Gemini + Pexels:** рекомендации (24 сек), распознавание продуктов (13 сек), генерация меню (510 сек). Во всех случаях нужна skeleton-загрузка в UI.
2. **Pexels — потенциальный bottleneck** при масштабировании (200 req/hour). Особенно при генерации меню (до 21 вызова). Решается кэшированием image_url по query-строке в Redis.
3. **Всё остальное работает без внешних зависимостей** — отказ Gemini/Pexels не роняет авторизацию, профиль, сохранённые рецепты, меню (просмотр/редактирование).
4. **КБЖУ приблизительные** — Gemini генерирует оценочные значения. Для MVP этого достаточно; точные данные требуют интеграции с верифицированной базой (USDA FoodData Central, см. TODO.md).
5. **Gemini Free tier (1 500 req/day):** распознавание продуктов (3 AI-операции) + рекомендации (1) + меню (1) = ~5 Gemini-запросов на активного пользователя. Free tier хватает на 300 DAU.