Files
OnBudget/docs/notification_parsing_errors_visibility_plan.md
2026-05-31 01:12:52 +03:00

7.9 KiB
Raw Permalink Blame History

План: видимость ошибок парсинга уведомлений

Контекст

Phase 1 парсинга push-уведомлений реализована, но при отладке через debug-инжектор непонятно, что пайплайн сделал с сообщением:

  • Сообщения со статусом ignored (сработал looksLikeNonTransaction, ignore-правило или AI сказал «не транзакция») и applied (авто-применено) не показываются ни на одном экране — молча исчезают.
  • failed-сообщения попадают в Inbox (watchInbox включает failed), но inbox_card.dart рендерит только _RecognizedBody (draft декодировался) или _UnrecognizedBody («Неопознано»). Поле lastParseError нигде не выводится — причина ошибки не видна, кнопки повтора нет.

Решение: (A) полноценный экран-журнал всех raw_messages в настройках парсинга (покрывает и «показать игнорированные»), (B) человеческий вид failed-карточки в Inbox с текстом ошибки и кнопкой «Попробовать снова». Оба — часть продукта (не только debug), локализованы.

Это закрывает «хвосты Phase 1»: «Архив raw_messages / показать игнорированные» и non-transaction карточки из notification_parsing_status.md.


A. Экран «Журнал парсинга» (все raw_messages)

Данные (data/domain)

Добавить реактивный стрим «все сообщения пользователя» + сброс на повтор. Зеркалить существующий watchInbox.

  • 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 (интерфейс) + raw_messages_repository_impl.dart: добавить watchAll (через тот же row→entity mapper, что и watchInbox) и resetForRetry.

Провайдер (application)

В inbox_controller.dart рядом с inboxMessages:

@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: failednegative, appliedpositive, ignoredink2, прочее→accent),
    • время receivedAt, packageName, сниппет body,
    • распознанное: декодировать decodeDraftBundle(message.draftJson) (draft_codec.dart) → мерчант + сумма (MoneyText),
    • при lastParseError != null — строка ошибки красным,
    • для failed/ignored — кнопка «Попробовать снова» → inboxController.retry(message).
  • Цвета только через context.palette / Theme.extension<Palette>() (без хардкода).

Навигация

  • app_routes.dart: static const parsingLog = '/settings/parsing/log';
  • app_router.dart: GoRoute(path: AppRoutes.parsingLog, builder: (_, __) => const ParsingLogScreen()) (рядом с остальными parsing-роутами, ~строка 133).
  • parsing_settings_screen.dart: _NavTile(icon: Icons.receipt_long_outlined, title: l10n.parsingLogTile, onTap: → AppRoutes.parsingLog) — отдельной _Card, всегда видна (не под kDebugMode).

B. Человеческий failed в Inbox

В 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 и app_ru.arb, затем flutter gen-l10n: parsingLogTile, parsingLogTitle, parsingLogEmpty, parsingLogError, inboxRetry, inboxParseErrorTitle, статус-лейблы parsingStatusPending/Inbox/Applied/Ignored/Failed/PendingAi/ParsedPartial, фильтр-лейблы parsingLogFilterAll/Inbox/Applied/Ignored/Failed.


Команды после правок

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).