feat: dish recognition UX, background mode, and backend bug fixes

Flutter client:
- Progress dialog: redesigned with pulsing animated icon, info hint about
  background mode, full-width Minimize button; dismiss signal via ValueNotifier
  so the dialog always closes regardless of widget lifecycle
- Background recognition: when user taps Minimize, wasMinimizedByUser flag is
  set; on completion a snackbar is shown instead of opening DishResultSheet
  directly; snackbar action opens the sheet on demand
- Fix dialog spinning forever: finally block guarantees dismissSignal=true on
  all exit paths including early returns from context.mounted checks
- Fix DishResultSheet not appearing: add ValueKey to _DailyMealsSection and
  meal card Padding so Flutter reuses elements when _TodayJobsWidget is
  inserted/removed from the SliverChildListDelegate list
- todayJobsProvider refresh: added refresh() method; called after job submit
  and on DishJobDone; all ref.read() calls guarded with context.mounted checks
- food_search_sheet: scan buttons replaced with full-width stacked OutlinedButtons
- app.dart: WidgetsBindingObserver refreshes scan providers on app resume
- L10n: added dishRecognitionHint and minimize keys to all 12 locales

Backend:
- migrations/003: ALTER TYPE recipe_source ADD VALUE 'recommendation' to fix
  22P02 error in GET /home/summary -> getRecommendations()
- item_enricher: normalizeProductCategory() validates AI-returned category
  against known slugs, falls back to "other" — fixes products_category_fkey
  FK violation during receipt recognition
- recognition prompt: enumerate valid categories so AI returns correct values

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dbastrikin
2026-03-28 00:03:17 +02:00
parent 5c5ed25e5b
commit 180c741424
32 changed files with 416 additions and 64 deletions

View File

@@ -231,5 +231,7 @@
}
},
"recognitionReplaceProduct": "استبدال المنتج",
"scanJobCloseHint": "يمكنك إغلاق التطبيق — سيظهر هذا المسح في عمليات المسح الأخيرة في شاشة المنتجات"
"scanJobCloseHint": "يمكنك إغلاق التطبيق — سيظهر هذا المسح في عمليات المسح الأخيرة في شاشة المنتجات",
"minimize": "تصغير",
"dishRecognitionHint": "يمكنك التصغير — ستظهر النتيجة في الشاشة الرئيسية عند الانتهاء"
}

View File

@@ -231,5 +231,7 @@
}
},
"recognitionReplaceProduct": "Produkt ersetzen",
"scanJobCloseHint": "Du kannst die App schließen — dieser Scan erscheint in Letzte Scans auf dem Produktbildschirm"
"scanJobCloseHint": "Du kannst die App schließen — dieser Scan erscheint in Letzte Scans auf dem Produktbildschirm",
"minimize": "Minimieren",
"dishRecognitionHint": "Du kannst minimieren — das Ergebnis erscheint auf dem Startbildschirm, wenn fertig"
}

View File

@@ -231,5 +231,7 @@
}
},
"recognitionReplaceProduct": "Replace product",
"scanJobCloseHint": "You can close the app — this scan will appear in Recent Scans on the Products screen"
"scanJobCloseHint": "You can close the app — this scan will appear in Recent Scans on the Products screen",
"minimize": "Minimize",
"dishRecognitionHint": "You can minimize — the result will appear on the Home screen when done"
}

View File

@@ -231,5 +231,7 @@
}
},
"recognitionReplaceProduct": "Reemplazar producto",
"scanJobCloseHint": "Puedes cerrar la app — este escaneo aparecerá en Escaneos recientes en la pantalla de Productos"
"scanJobCloseHint": "Puedes cerrar la app — este escaneo aparecerá en Escaneos recientes en la pantalla de Productos",
"minimize": "Minimizar",
"dishRecognitionHint": "Puedes minimizar — el resultado aparecerá en la pantalla principal cuando esté listo"
}

View File

@@ -231,5 +231,7 @@
}
},
"recognitionReplaceProduct": "Remplacer le produit",
"scanJobCloseHint": "Vous pouvez fermer l'app — ce scan apparaîtra dans Scans récents sur l'écran Produits"
"scanJobCloseHint": "Vous pouvez fermer l'app — ce scan apparaîtra dans Scans récents sur l'écran Produits",
"minimize": "Réduire",
"dishRecognitionHint": "Vous pouvez réduire — le résultat apparaîtra sur l'écran d'accueil quand c'est terminé"
}

View File

@@ -231,5 +231,7 @@
}
},
"recognitionReplaceProduct": "उत्पाद बदलें",
"scanJobCloseHint": "आप ऐप बंद कर सकते हैं — यह स्कैन उत्पाद स्क्रीन पर हाल के स्कैन में दिखाई देगा"
"scanJobCloseHint": "आप ऐप बंद कर सकते हैं — यह स्कैन उत्पाद स्क्रीन पर हाल के स्कैन में दिखाई देगा",
"minimize": "छोटा करें",
"dishRecognitionHint": "आप छोटा कर सकते हैं — पूर्ण होने पर परिणाम होम स्क्रीन पर दिखेगा"
}

View File

@@ -231,5 +231,7 @@
}
},
"recognitionReplaceProduct": "Sostituisci prodotto",
"scanJobCloseHint": "Puoi chiudere l'app — questa scansione apparirà in Scansioni recenti nella schermata Prodotti"
"scanJobCloseHint": "Puoi chiudere l'app — questa scansione apparirà in Scansioni recenti nella schermata Prodotti",
"minimize": "Riduci a icona",
"dishRecognitionHint": "Puoi ridurre a icona — il risultato apparirà nella schermata principale al termine"
}

View File

@@ -231,5 +231,7 @@
}
},
"recognitionReplaceProduct": "商品を置き換える",
"scanJobCloseHint": "アプリを閉じても大丈夫です — このスキャンは製品画面の最近のスキャンに表示されます"
"scanJobCloseHint": "アプリを閉じても大丈夫です — このスキャンは製品画面の最近のスキャンに表示されます",
"minimize": "最小化",
"dishRecognitionHint": "最小化できます — 完了したら結果がホーム画面に表示されます"
}

View File

@@ -231,5 +231,7 @@
}
},
"recognitionReplaceProduct": "제품 교체",
"scanJobCloseHint": "앱을 닫아도 됩니다 — 이 스캔은 제품 화면의 최근 스캔에 표시됩니다"
"scanJobCloseHint": "앱을 닫아도 됩니다 — 이 스캔은 제품 화면의 최근 스캔에 표시됩니다",
"minimize": "최소화",
"dishRecognitionHint": "최소화할 수 있습니다 — 완료되면 홈 화면에 결과가 표시됩니다"
}

View File

@@ -1215,6 +1215,18 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'You can close the app — this scan will appear in Recent Scans on the Products screen'**
String get scanJobCloseHint;
/// No description provided for @minimize.
///
/// In en, this message translates to:
/// **'Minimize'**
String get minimize;
/// No description provided for @dishRecognitionHint.
///
/// In en, this message translates to:
/// **'You can minimize — the result will appear on the Home screen when done'**
String get dishRecognitionHint;
}
class _AppLocalizationsDelegate

View File

@@ -575,4 +575,11 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get scanJobCloseHint =>
'يمكنك إغلاق التطبيق — سيظهر هذا المسح في عمليات المسح الأخيرة في شاشة المنتجات';
@override
String get minimize => 'تصغير';
@override
String get dishRecognitionHint =>
'يمكنك التصغير — ستظهر النتيجة في الشاشة الرئيسية عند الانتهاء';
}

View File

@@ -579,4 +579,11 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get scanJobCloseHint =>
'Du kannst die App schließen — dieser Scan erscheint in Letzte Scans auf dem Produktbildschirm';
@override
String get minimize => 'Minimieren';
@override
String get dishRecognitionHint =>
'Du kannst minimieren — das Ergebnis erscheint auf dem Startbildschirm, wenn fertig';
}

View File

@@ -577,4 +577,11 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get scanJobCloseHint =>
'You can close the app — this scan will appear in Recent Scans on the Products screen';
@override
String get minimize => 'Minimize';
@override
String get dishRecognitionHint =>
'You can minimize — the result will appear on the Home screen when done';
}

View File

@@ -580,4 +580,11 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get scanJobCloseHint =>
'Puedes cerrar la app — este escaneo aparecerá en Escaneos recientes en la pantalla de Productos';
@override
String get minimize => 'Minimizar';
@override
String get dishRecognitionHint =>
'Puedes minimizar — el resultado aparecerá en la pantalla principal cuando esté listo';
}

View File

@@ -580,4 +580,11 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get scanJobCloseHint =>
'Vous pouvez fermer l\'app — ce scan apparaîtra dans Scans récents sur l\'écran Produits';
@override
String get minimize => 'Réduire';
@override
String get dishRecognitionHint =>
'Vous pouvez réduire — le résultat apparaîtra sur l\'écran d\'accueil quand c\'est terminé';
}

View File

@@ -578,4 +578,11 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get scanJobCloseHint =>
'आप ऐप बंद कर सकते हैं — यह स्कैन उत्पाद स्क्रीन पर हाल के स्कैन में दिखाई देगा';
@override
String get minimize => 'छोटा करें';
@override
String get dishRecognitionHint =>
'आप छोटा कर सकते हैं — पूर्ण होने पर परिणाम होम स्क्रीन पर दिखेगा';
}

View File

@@ -580,4 +580,11 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get scanJobCloseHint =>
'Puoi chiudere l\'app — questa scansione apparirà in Scansioni recenti nella schermata Prodotti';
@override
String get minimize => 'Riduci a icona';
@override
String get dishRecognitionHint =>
'Puoi ridurre a icona — il risultato apparirà nella schermata principale al termine';
}

View File

@@ -571,4 +571,10 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get scanJobCloseHint => 'アプリを閉じても大丈夫です — このスキャンは製品画面の最近のスキャンに表示されます';
@override
String get minimize => '最小化';
@override
String get dishRecognitionHint => '最小化できます — 完了したら結果がホーム画面に表示されます';
}

View File

@@ -571,4 +571,10 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get scanJobCloseHint => '앱을 닫아도 됩니다 — 이 스캔은 제품 화면의 최근 스캔에 표시됩니다';
@override
String get minimize => '최소화';
@override
String get dishRecognitionHint => '최소화할 수 있습니다 — 완료되면 홈 화면에 결과가 표시됩니다';
}

View File

@@ -579,4 +579,11 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get scanJobCloseHint =>
'Você pode fechar o app — este scan aparecerá em Scans recentes na tela de Produtos';
@override
String get minimize => 'Minimizar';
@override
String get dishRecognitionHint =>
'Você pode minimizar — o resultado aparecerá na tela inicial quando concluído';
}

View File

@@ -578,4 +578,11 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get scanJobCloseHint =>
'Можно закрыть приложение — скан появится в последних сканированиях на экране продуктов';
@override
String get minimize => 'Свернуть';
@override
String get dishRecognitionHint =>
'Можно свернуть — результат появится на главном экране по завершении';
}

View File

@@ -570,4 +570,10 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get scanJobCloseHint => '您可以关闭应用 — 此扫描将显示在产品页面的最近扫描中';
@override
String get minimize => '最小化';
@override
String get dishRecognitionHint => '可以最小化 — 完成后结果将显示在主屏幕上';
}

View File

@@ -231,5 +231,7 @@
}
},
"recognitionReplaceProduct": "Substituir produto",
"scanJobCloseHint": "Você pode fechar o app — este scan aparecerá em Scans recentes na tela de Produtos"
"scanJobCloseHint": "Você pode fechar o app — este scan aparecerá em Scans recentes na tela de Produtos",
"minimize": "Minimizar",
"dishRecognitionHint": "Você pode minimizar — o resultado aparecerá na tela inicial quando concluído"
}

View File

@@ -231,5 +231,7 @@
}
},
"recognitionReplaceProduct": "Заменить продукт",
"scanJobCloseHint": "Можно закрыть приложение — скан появится в последних сканированиях на экране продуктов"
"scanJobCloseHint": "Можно закрыть приложение — скан появится в последних сканированиях на экране продуктов",
"minimize": "Свернуть",
"dishRecognitionHint": "Можно свернуть — результат появится на главном экране по завершении"
}

View File

@@ -231,5 +231,7 @@
}
},
"recognitionReplaceProduct": "替换产品",
"scanJobCloseHint": "您可以关闭应用 — 此扫描将显示在产品页面的最近扫描中"
"scanJobCloseHint": "您可以关闭应用 — 此扫描将显示在产品页面的最近扫描中",
"minimize": "最小化",
"dishRecognitionHint": "可以最小化 — 完成后结果将显示在主屏幕上"
}