Исследовательский отчёт

Аудит продукта Nukl

Дата: 28 марта 2026 Версия: 1.0.0+14 Стек: Flutter 3.8+ (Dart) + Supabase (PostgreSQL) + Firebase (FCM) Ветка: develop


1. Общая структура проекта

1.1 Описание

NUKL — мобильное приложение для любительского футбола. Основная идея: пользователь выбирает город, видит матчи на ближайших площадках, присоединяется к игре, выбирает команду и позицию на поле.

1.2 Структура каталогов

lib/
  config/           -- конфигурация формаций (расстановки на поле)
  l10n/             -- локализация (EN, RU, ES, FR)
  models/           -- 2 модели: AppNotification, NewsItem
  pages/            -- 7 разделов (pre_auth, navigation, matches, profile, news, notifications, settings, friends)
  services/         -- 9 сервисов (auth, notifications, king_of_match, locale, theme, location, maps, user_device, notification_router)
  ui/theme/         -- тема (цвета, типографика, стили кнопок, пикеры)
  widgets/          -- переиспользуемые виджеты (16 подкаталогов: app_bar, avatar, bottom_nav, checkbox, common, dialog, error, feedback, inputs, locations, matches, news, notifications, pickers, skeletons, settings_tile)

supabase/
  schema/           -- дамп схемы v0802, news_posts, cron_jobs (пустой)
  migrations/       -- 4 миграции (i18n, news_translations, global_location, king_of_match_votes)
  functions/        -- 3 Edge Functions (notifications-worker, translate-news, translate-text)

assets/             -- шрифт Inter (3 начертания), SVG-иконки, фоновые изображения
plugins/            -- локальный плагин flutter_app_badger

1.3 Ключевые зависимости (pubspec.yaml)

  • supabase_flutter 2.10.3 — основной бэкенд
  • firebase_core + firebase_messaging — push-уведомления через FCM v1
  • go_router 17.0.0 — объявлен, но не используется (навигация ручная через Navigator.push)
  • sign_in_with_apple + crypto — Apple Sign-In
  • geolocator — геолокация для автоопределения города
  • cached_network_image — кеширование изображений
  • shimmer — скелетоны
  • flutter_svg — векторные иконки
  • shared_preferences — хранение темы и языка
  • intl — форматирование дат

Файл CLAUDE.md отсутствует — нет задокументированных правил проекта для AI-ассистентов.


2. Полный список реализованных фичей

2.1 Аутентификация и онбординг

КомпонентОписаниеЗавершённость
Welcome ScreenСтартовый экран с кнопкой "Log in / Sign in"100%
Login PageEmail magic link, Google OAuth, Apple Sign-In100%
OnboardingЗаполнение имени, принятие Terms of Use; специальная логика для Apple (не требует имени)95%
AuthPostLoginFlowПост-авторизация: определение провайдера, проверка профиля, авто-заполнение имени из metadata, redirect на онбординг или Main100%
Terms of UseЗагрузка из таблицы user_agreements, отображение, принятие100%
Account DeletionМягкое удаление (pending_deletion на 30 дней), авто-восстановление при re-login100%

2.2 Матчи (ядро продукта)

КомпонентОписаниеЗавершённость
Matches PageСписок матчей по городу: "Joined" + "Others" сгруппированные по датам100%
Match DetailsДетальная страница матча: информация о площадке, время, цена, организатор, команды, расстановка100%
Match RulesПравила матча100%
Create MatchЗаглушка "Coming soon"5%
Join / Leave MatchПрисоединение к команде с выбором позиции на поле, выход из матча100%
Formation FieldВизуализация расстановки на поле (5x5, 6x6, 7x7, 8x8, 9x9, 11x11) с drag-like взаимодействием100%
Match Status Lifecyclescheduled -> in_progress -> finished / cancelled (cron через update_match_statuses)100%
Match RatingsОценка матча (1-5) после завершения90%
King of the MatchГолосование за лучшего игрока (48ч после завершения), модальный промпт при открытии приложения80%

2.3 Профиль и социальные функции

КомпонентОписаниеЗавершённость
Profile PageАватар, имя, позиция, город, количество завершённых матчей, уведомления, друзья, активность100%
Edit ProfileРедактирование имени, фамилии, аватара, позиции100%
Player ProfileПросмотр профиля другого игрока100%
Activity PageИстория матчей пользователя100%
Friends ListСписок друзей с поиском, глобальный поиск игроков, realtime-подписки на изменения100%
Friend RequestsВходящие запросы дружбы, принятие/отклонение, массовые действия100%

2.4 Уведомления

КомпонентОписаниеЗавершённость
Push NotificationsFCM v1 API через Edge Function (notifications-worker), планирование через триггеры PostgreSQL100%
In-App BannerOverlay-баннер для foreground-пушей с подавлением дубликатов100%
Notifications PageСписок уведомлений с mark-as-read, навигация по типам100%
Badge ServiceСинглтон-сервис: realtime-подписка, кеширование unread count, sync с app icon badge100%
Notification PreferencesOpt-out модель по категориям (в worker, таблица notification_preferences)90%
Notification RouterУниверсальный роутер уведомлений (route-based + category fallback)100%
Localised NotificationsШаблоны на 4 языках, resolution в worker через title_key/body_key100%

2.5 Новости

КомпонентОписаниеЗавершённость
News FeedЛента новостей с пагинацией (20 на страницу), скелетоны100%
News DetailДетальная страница новости100%
News TranslationsEdge Function translate-news (Google Translate API), хранение в news_translations100%

2.6 Настройки

КомпонентОписаниеЗавершённость
PreferencesЯзык (4 локали), тёмная/светлая тема100%
LocationГлобальная система: 200 стран, 33K+ городов, PostGIS, автоопределение GPS100%
Preferred Maps AppВыбор картографического приложения (Apple, Google, Waze, 2GIS, Yandex), deep links100%
FeedbackОтправка отзывов (bug/idea/other) с rate-limiting (20/день)100%
Log Out / Delete AccountВыход, удаление аккаунта с grace period100%

2.7 Локализация (i18n)

КомпонентОписаниеЗавершённость
Flutter l10nARB-файлы для EN, RU, ES, FR; класс S95%
Server-side i18nПереводы стран, городов, позиций, уведомлений90%
Auto-detect localeОпределение языка системы, fallback на EN100%

3. Техническое качество

3.1 Архитектура

  • Паттерн: Stateful виджеты с прямыми вызовами Supabase. Нет выделенного слоя Repository / UseCase.
  • Сервисы: Синглтоны (ThemeService, LocaleService, LocationService, NotificationsBadgeService) — разумно для текущего масштаба, но связанность высокая.
  • Бизнес-логика матчей: Полностью на стороне сервера (RPC, триггеры, RLS) — это хорошее архитектурное решение.
  • Глобальный supabase клиент: Через top-level final supabase = Supabase.instance.client — работает, но затрудняет тестирование.

3.2 State Management

  • ValueNotifier для темы и локали — минимально, но достаточно.
  • ChangeNotifier для NotificationsBadgeService — корректно.
  • setState повсюду в Pages — типичный Flutter-подход без DI. Для 15-20 экранов это терпимо, но при росте потребуется Riverpod/Bloc.
  • AutomaticKeepAliveClientMixin на ключевых табах (News, Matches, Profile, Settings) — правильно для PageView.

3.3 Навигация

  • MaterialApp + Navigator.push/pushAndRemoveUntil — ручная навигация.
  • go_router объявлен в pubspec.yaml, но нигде не используется — мёртвая зависимость.
  • NavigatorKey для deep linking из push-уведомлений — работоспособно, но хрупко.
  • Отсутствует декларативный роутинг; добавление новых deep links потребует ручной правки NotificationRouter.

3.4 Локализация

  • Полноценная система: ARB-файлы, S.of(context), параметризованные строки.
  • Серверная локализация уведомлений через шаблоны в translations.ts.
  • Динамическая локализация городов/стран/позиций.
  • Проблема: формат даты захардкожен как 'en_US' в MatchesPage (DateFormat('h', 'en_US')) — не адаптируется под локаль.

3.5 Supabase интеграция

  • RPC (server functions): joined_matches_with_counts, other_matches_with_counts_for_city, count_finished_matches, search_cities, find_nearest_city, get_pending_king_votes, request_account_deletion, restore_account_if_pending, update_match_statuses.
  • Триггеры: Автоматическое создание профиля (handle_new_user), уведомления при join/leave/time_change/cancel/finish, rate-limiting feedback, enforce single active device.
  • Realtime: Подписки на notifications (badge) и friendships (friend list/requests).
  • Storage: Bucket fields для фото площадок.
  • Edge Functions: notifications-worker (cron), translate-news, translate-text.
  • Качество серверной части — высокое. Триггеры хорошо продуманы, SECURITY DEFINER используется обоснованно.

3.6 RLS (Row Level Security)

Включён на всех основных таблицах:

ТаблицаSELECTINSERTUPDATEDELETE
profilesauthenticated (все)ownown
matchesactive + own history
match_playersauthenticated (все)ownownown
match_ratingsownown+finishedown
notificationsownownown
user_devicesownownown
app_feedbackownown
fieldspublic read
news_postsauthenticated + published
king_of_match_votesownown+conditions

Замечания по RLS:

  • cities и countriesRLS не включён (видимо, public read intended, но лучше явно).
  • player_positions и positionsRLS не включён.
  • user_agreementsRLS не включён (Terms of Use читаются без ограничений).
  • notification_preferences — таблица упоминается в worker, но нет миграции и нет RLS-политик в дампе схемы (возможно, создана отдельно).
  • friendships — таблица активно используется в коде (queries из 5+ файлов), но отсутствует в дампе схемы и миграциях — была создана вручную.
  • news_translations — аналогично, упоминается в миграции переводов, но схема не в дампе.

3.7 Качество кода

  • Положительное:

    • Комментарии на русском и английском, объясняющие "почему", а не "что"
    • Defensive coding: таймауты на запросах (8-15 секунд), null-проверки, try-catch
    • context.mounted проверки перед setState после async gaps
    • Хорошие скелетоны для каждого экрана (12 файлов skeletons)
    • Аккуратная обработка Edge Cases: Apple Sign-In, token refresh, dead FCM tokens
  • Отрицательное:

    • Смешение русского и английского в комментариях
    • Отсутствие CLAUDE.md / README с правилами проекта
    • Emoji в debugPrint — не критично, но шумно в логах
    • Некоторые виджеты очень длинные (MatchDetailsPage, MatchesPage — 500+ строк)

4. UX/UI решения

4.1 Тема

  • Полноценная light/dark тема с AppColorScheme — хорошая абстракция.
  • Единый фирменный цвет nuklBlue (#25B3D2).
  • Шрифт Inter (400, 500, 600).
  • 4 типа кнопок: Elevated (тёмная), Filled (синяя), Outlined (контурная), Text (ghost).
  • Pill-shaped (stadium border) кнопки — современный спортивный UI.
  • Extension context.appColors для удобного доступа к цветовой схеме.

4.2 Компоненты

  • NuklAppBar — кастомный AppBar.
  • BallRefreshIndicator — кастомный pull-to-refresh (мячик).
  • ShimmerLogo — брендированный спиннер.
  • Custom CheckBox, ConfirmDialog — стилизованные под бренд.
  • FormationField — интерактивная визуализация поля для выбора позиции.
  • BottomNavBar — 5 табов (News, Matches, Create, Profile, Settings) с badge.

4.3 User Flows

  • Cold start: Splash -> проверка сессии -> AuthPostLoginFlow -> (Onboarding или MainTabs).
  • Push notification: Background/terminated open -> NotificationRouter -> MoreInfoPage или NotificationsPage.
  • Выбор города: GPS auto-detect или ручной выбор (Country -> City с поиском).
  • Присоединение к матчу: MatchesPage -> MatchDetailsPage -> выбор команды и позиции на поле -> Join.
  • King of the Match: Модальное окно при открытии приложения -> страница голосования -> выбор игрока.
  • Haptic feedback при переключении табов и свайпе — приятная тактильная обратная связь.

5. Технический долг

5.1 Критический

  1. go_router подключён, но не используется — мёртвая зависимость, увеличивает размер бандла.
  2. Тест-файл — шаблонный Counter test — не имеет отношения к проекту, тесты фактически отсутствуют (0% покрытие).
  3. .env включён в assets Flutter (pubspec.yaml, строка 115) — потенциальная утечка секретов в бандл.
  4. Таблица friendships не в дампе схемы — риск рассинхронизации между окружениями.
  5. Таблица notification_preferences не в миграциях — worker на неё ссылается, но без миграции новое окружение не запустится.

5.2 Значительный

  1. Create Match — заглушка. Матчи можно создавать только из Supabase Studio. Это блокирует пользовательский рост.
  2. Нет пагинации в списках матчей — при росте данных экран будет подгружать всё сразу.
  3. refreshUnreadCount() делает SELECT id и считает length на клиенте — неэффективно; нужно count: 'exact', head: true.
  4. Формат даты захардкожен на en_US — несмотря на 4 локали.
  5. Placeholder-изображение для поля — внешний URL (dhresource.com) — внешняя зависимость, может сломаться.

5.3 Средний

  1. Нет offline-поддержки — при отсутствии сети показывается просто ошибка.
  2. Нет аналитики — ни Firebase Analytics, ни Amplitude, ни Posthog.
  3. Нет crash reporting — ни Sentry, ни Crashlytics.
  4. Нет CI/CD конфигурации в репозитории.
  5. Нет pagination в Friends/News (News имеет пагинацию, но Friends — нет).
  6. King of the Match: DELETE policy отсутствуетcancelVote() на клиенте вызывает delete, но в RLS нет DELETE policy на king_of_match_votes.
  7. Нет валидации длины имени на клиенте при онбординге (только latin letters regex).

5.4 Хардкод

  • Placeholder image URL: https://img4.dhresource.com/...
  • Legal URL: https://www.nukleball.com/legal
  • FCM retry: maxAttempts = 5, delay = 2s
  • Feedback rate limit: 20/day (в триггере)
  • Account deletion grace period: 30 days (в RPC)
  • King of Match voting window: 48 hours (в RPC и RLS)
  • Supported locales: {'en', 'ru', 'es', 'fr'} (в LocaleService, Edge Functions)

6. Supabase схема

6.1 Таблицы (14 задокументированных)

ТаблицаНазначениеRLS
profilesПрофили пользователей (auth.users trigger)Да
matchesМатчиДа
match_playersУчастники матчей (team, position)Да
match_ratingsОценки матчей (1-5)Да
fieldsПлощадкиДа
citiesГорода (33K+, PostGIS)Нет
countriesСтраны (200)Нет
positionsПозиции на поле (для formation)Нет
player_positionsПозиции игрока (для профиля)Нет
notificationsУведомления (scheduled -> sent)Да
user_devicesFCM-токены устройствДа
user_agreementsПользовательские соглашенияНет
app_feedbackОбратная связьДа
news_postsНовостиДа
king_of_match_votesГолосование за лучшего игрокаДа
friendships (не в дампе)Дружбы между игроками?
notification_preferences (не в дампе)Настройки уведомлений?
news_translations (не в дампе)Переводы новостей?

6.2 Ключевые отношения

auth.users --(trigger)--> profiles
profiles --> cities --> countries
profiles --> player_positions
matches --> fields --> cities
match_players --> matches, profiles, positions
match_ratings --> matches, profiles
notifications --> profiles, matches
user_devices --> profiles
king_of_match_votes --> matches, profiles (voter + voted_for)
friendships --> profiles (2 стороны)

6.3 RPC-функции (10)

  • handle_new_user — триггер создания профиля
  • handle_match_after_update — уведомления при изменении матча
  • handle_match_player_after_insert/update — уведомления при join/leave
  • match_players_before_insert — валидация (статус, лимит, время)
  • update_match_statuses — cron для смены статусов
  • joined_matches_with_counts, other_matches_with_counts_for_city — запросы матчей
  • request_account_deletion, restore_account_if_pending — управление аккаунтом
  • search_cities, find_nearest_city — PostGIS поиск
  • get_pending_king_votes — голосование
  • count_finished_matches — статистика
  • user_devices_enforce_single_active — один активный девайс

6.4 Edge Functions (3)

  1. notifications-worker — основной воркер уведомлений. Cron-based (через pg_cron -> HTTP). FCM v1 API, локализация, opt-out, dead token cleanup. Качественная реализация.
  2. translate-news — перевод новостей через Google Translate API при публикации.
  3. translate-text — общий перевод текста (вспомогательная).

6.5 Триггеры (7)

  • handle_fields_updated_at, handle_matches_updated_at — автообновление timestamps
  • trg_match_players_before_insert — валидация join
  • trg_match_players_after_insert — создание reminder/finish уведомлений
  • trg_match_players_after_update — отмена уведомлений при leave
  • trg_matches_after_update — реакция на изменение времени/отмену
  • trg_limit_app_feedback — rate limiting
  • trg_user_devices_single_active — один активный токен

6.6 Расширения PostgreSQL

  • pg_cron — планировщик (обновление статусов матчей, вызов worker)
  • pg_net — HTTP-запросы из PostgreSQL
  • postgis — геопространственные запросы
  • pg_trgm — fuzzy-поиск по названиям городов
  • pgcrypto, uuid-ossp — генерация UUID

7. SWOT-анализ

Strengths (Сильные стороны)

  • Серверная архитектура продумана на уровне senior backend: триггеры, RLS, SECURITY DEFINER, PostGIS, rate limiting, soft delete — всё на месте
  • Полноценная система push-уведомлений: edge function + FCM v1 + cron + realtime + in-app banners + badge sync — production-grade
  • Глобальная геосистема: 200 стран, 33K+ городов, PostGIS для nearest city, trigram для fuzzy search
  • 4-язычная локализация (EN/RU/ES/FR) сквозная: UI, уведомления, города, позиции, новости
  • Визуализация формаций: интерактивное поле с расстановками 5x5..11x11 — уникальная фича для ниши
  • Apple Review compliance: специальная логика для Apple Sign-In (не требует имени)
  • UI дизайн-система: AppColorScheme, типографика, 4 типа кнопок, dark/light тема, скелетоны на каждый экран

Weaknesses (Слабые стороны)

  • Create Match — заглушка: невозможно создавать матчи из приложения, только через Supabase Studio
  • Полное отсутствие тестов: единственный файл — шаблонный counter test
  • Нет аналитики и crash reporting: невозможно отслеживать поведение пользователей и ошибки
  • Нет offline-поддержки: при потере связи — белый экран с ошибкой
  • 3 таблицы отсутствуют в дампе схемы (friendships, notification_preferences, news_translations) — риск при развёртывании
  • Нет CI/CD: ни GitHub Actions, ни Codemagic, ни Fastlane

Opportunities (Возможности)

  • Create Match — крупнейшая возможность роста; без неё пользователи не могут организовывать матчи сами
  • Чат (код уже содержит заглушки case 'chat' в NotificationRouter) — следующий логический шаг
  • Статистика и лидерборды — King of the Match votes уже собираются, но нигде не отображаются
  • Платежи — price_cents/currency уже в схеме, можно интегрировать Stripe
  • Web-версия — Flutter Web + текущая архитектура позволяют
  • Расширение на турниры/лиги — естественное развитие для amateur football

Threats (Угрозы)

  • .env в assets — ключи Supabase могут быть извлечены из бандла
  • King of the Match DELETE policy отсутствуетcancelVote() тихо не работает, пользователь не знает
  • Зависимость от внешнего placeholder image (dhresource.com) — сломается без предупреждения
  • Масштабируемость refreshUnreadCount — SELECT всех id вместо COUNT при каждом изменении
  • Отсутствие rate limiting на API — теоретически пользователь может спамить join/leave

Резюме

Nukl — хорошо проработанный MVP для любительского футбола с сильной серверной архитектурой (Supabase triggers, RLS, PostGIS, Edge Functions) и продуманным UX (скелетоны, haptic feedback, dark/light тема, 4 языка). Серверная часть написана на уровне зрелого продукта: push-уведомления через FCM v1 API, realtime-подписки, soft delete, cron-задачи, rate limiting. Главная критическая проблема — отсутствие функции создания матча из приложения, что блокирует органический рост. Технический долг сосредоточен в трёх областях: нулевое тестовое покрытие, отсутствие аналитики/crash reporting и неполная синхронизация схемы БД (3 таблицы без миграций). При устранении этих проблем и реализации Create Match проект готов к публичному запуску.