Stage ai for sms 1
This commit is contained in:
@@ -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`).
|
||||
Reference in New Issue
Block a user