Stage ai for sms 1

This commit is contained in:
2026-05-31 01:12:52 +03:00
parent 50d56b91d2
commit a2d94a1451
@@ -0,0 +1,89 @@
# План: видимость ошибок парсинга уведомлений
## Контекст
Phase 1 парсинга push-уведомлений реализована, но при отладке через debug-инжектор **непонятно, что пайплайн сделал с сообщением**:
- Сообщения со статусом `ignored` (сработал `looksLikeNonTransaction`, ignore-правило или AI сказал «не транзакция») и `applied` (авто-применено) **не показываются ни на одном экране** — молча исчезают.
- `failed`-сообщения попадают в Inbox (`watchInbox` включает `failed`), но [inbox_card.dart](../lib/src/features/notification_parsing/presentation/widgets/inbox_card.dart) рендерит только `_RecognizedBody` (draft декодировался) или `_UnrecognizedBody` («Неопознано»). Поле **`lastParseError` нигде не выводится** — причина ошибки не видна, кнопки повтора нет.
Решение: **(A)** полноценный экран-журнал всех `raw_messages` в настройках парсинга (покрывает и «показать игнорированные»), **(B)** человеческий вид `failed`-карточки в Inbox с текстом ошибки и кнопкой «Попробовать снова». Оба — часть продукта (не только debug), локализованы.
Это закрывает «хвосты Phase 1»: *«Архив raw_messages / показать игнорированные»* и *non-transaction карточки* из [notification_parsing_status.md](notification_parsing_status.md).
---
## A. Экран «Журнал парсинга» (все raw_messages)
### Данные (data/domain)
Добавить реактивный стрим «все сообщения пользователя» + сброс на повтор. Зеркалить существующий `watchInbox`.
- [raw_messages_dao.dart](../lib/src/features/notification_parsing/data/drift/daos/raw_messages_dao.dart):
- `Stream<List<RawMessagesTableData>> watchAll(String userId, {int limit = 200})``select(rawMessagesTable)..where(userId)..orderBy(receivedAt desc)..limit(limit)`.
- `Future<void> resetForRetry(String id)` — companion: `status: Value(pending)`, `parseAttemptCount: Value(0)`, `lastParseError: Value(null)` (чтобы воркер переобработал и AI-лимит `>= 5` не блокировал повтор сразу).
- [raw_messages_repository.dart](../lib/src/features/notification_parsing/domain/repositories/raw_messages_repository.dart) (интерфейс) + [raw_messages_repository_impl.dart](../lib/src/features/notification_parsing/data/repositories/raw_messages_repository_impl.dart): добавить `watchAll` (через тот же row→entity mapper, что и `watchInbox`) и `resetForRetry`.
### Провайдер (application)
В [inbox_controller.dart](../lib/src/features/notification_parsing/application/inbox_controller.dart) рядом с `inboxMessages`:
```dart
@riverpod
Stream<List<RawMessage>> parsingLog(Ref ref, String userId) =>
ref.watch(rawMessagesRepositoryProvider).watchAll(userId);
```
И метод в `InboxController`: `Future<void> retry(RawMessage m) => ref.read(rawMessagesRepositoryProvider).resetForRetry(m.id);`
### Экран (presentation)
Новый `presentation/screens/parsing_log_screen.dart` (`ConsumerStatefulWidget`, локальный фильтр-стейт):
- AppBar `l10n.parsingLogTitle`, фон `p.paper` — как у `ParsingSettingsScreen`.
- **Фильтр-чипы** по статусу — зеркалить чип-паттерн `RulesListScreen` (`_RuleFilter`): `_LogFilter { all, inbox, applied, ignored, failed }`. Под `all` показывать всё (включая `pending`/`pendingAi`/`parsedPartial`).
- Список карточек-строк. Каждая строка:
- статус-бэйдж (цвет из `Palette`: `failed``negative`, `applied``positive`, `ignored``ink2`, прочее→`accent`),
- время `receivedAt`, `packageName`, сниппет `body`,
- распознанное: декодировать `decodeDraftBundle(message.draftJson)` ([draft_codec.dart](../lib/src/features/notification_parsing/data/parser/draft_codec.dart)) → мерчант + сумма (`MoneyText`),
- при `lastParseError != null` — строка ошибки красным,
- для `failed`/`ignored` — кнопка «Попробовать снова» → `inboxController.retry(message)`.
- Цвета только через `context.palette` / `Theme.extension<Palette>()` (без хардкода).
### Навигация
- [app_routes.dart](../lib/src/app/router/app_routes.dart): `static const parsingLog = '/settings/parsing/log';`
- [app_router.dart](../lib/src/app/router/app_router.dart): `GoRoute(path: AppRoutes.parsingLog, builder: (_, __) => const ParsingLogScreen())` (рядом с остальными parsing-роутами, ~строка 133).
- [parsing_settings_screen.dart](../lib/src/features/notification_parsing/presentation/screens/parsing_settings_screen.dart): `_NavTile(icon: Icons.receipt_long_outlined, title: l10n.parsingLogTile, onTap: → AppRoutes.parsingLog)` — отдельной `_Card`, всегда видна (не под `kDebugMode`).
---
## B. Человеческий `failed` в Inbox
В [inbox_card.dart](../lib/src/features/notification_parsing/presentation/widgets/inbox_card.dart):
- В `build` добавить третью ветку перед текущим тернарником:
`message.status == RawMessageStatus.failed` (или `lastParseError != null`) → `_FailedBody`.
- Новый `_FailedBody`: заголовок «Ошибка распознавания», `message.lastParseError` красным (`p.negative`), сниппет `body`, две кнопки — `_SecondaryButton` «Попробовать снова» (`inboxController.retry(message)`) и «Игнорировать» (существующий `ignore`).
---
## Локализация
Добавить ключи в [app_en.arb](../lib/l10n/app_en.arb) и [app_ru.arb](../lib/l10n/app_ru.arb), затем `flutter gen-l10n`:
`parsingLogTile`, `parsingLogTitle`, `parsingLogEmpty`, `parsingLogError`, `inboxRetry`, `inboxParseErrorTitle`,
статус-лейблы `parsingStatusPending/Inbox/Applied/Ignored/Failed/PendingAi/ParsedPartial`,
фильтр-лейблы `parsingLogFilterAll/Inbox/Applied/Ignored/Failed`.
---
## Команды после правок
```bash
dart run build_runner build --delete-conflicting-outputs # новый @riverpod parsingLog
flutter gen-l10n # новые ARB-строки
flutter analyze
```
## Проверка (e2e через debug-инжектор)
1. `flutter run`, Profile → Parsing settings → Debug Inject.
2. Инжектить **не-транзакцию** (напр. `Баланс: 5000 ₽`) → в Inbox её нет, а в **Журнале** строка со статусом `ignored`.
3. Инжектить нормальную покупку → строка `applied`/`inbox` с распознанным мерчантом и суммой.
4. Проверить `failed`: сообщение, дающее исключение → в Inbox карточка с текстом `lastParseError` и кнопкой «Попробовать снова»; тап → статус сбрасывается в `pending`, воркер переобрабатывает.
5. В Журнале фильтр-чипы корректно фильтруют по статусу; «Попробовать снова» возвращает `ignored`/`failed` в обработку.
6. `flutter analyze` чист; существующие тесты фичи зелёные (`flutter test`).