Аудит продукта 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 Page | Email magic link, Google OAuth, Apple Sign-In | 100% |
| Onboarding | Заполнение имени, принятие Terms of Use; специальная логика для Apple (не требует имени) | 95% |
| AuthPostLoginFlow | Пост-авторизация: определение провайдера, проверка профиля, авто-заполнение имени из metadata, redirect на онбординг или Main | 100% |
| Terms of Use | Загрузка из таблицы user_agreements, отображение, принятие | 100% |
| Account Deletion | Мягкое удаление (pending_deletion на 30 дней), авто-восстановление при re-login | 100% |
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 Lifecycle | scheduled -> 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 Notifications | FCM v1 API через Edge Function (notifications-worker), планирование через триггеры PostgreSQL | 100% |
| In-App Banner | Overlay-баннер для foreground-пушей с подавлением дубликатов | 100% |
| Notifications Page | Список уведомлений с mark-as-read, навигация по типам | 100% |
| Badge Service | Синглтон-сервис: realtime-подписка, кеширование unread count, sync с app icon badge | 100% |
| Notification Preferences | Opt-out модель по категориям (в worker, таблица notification_preferences) | 90% |
| Notification Router | Универсальный роутер уведомлений (route-based + category fallback) | 100% |
| Localised Notifications | Шаблоны на 4 языках, resolution в worker через title_key/body_key | 100% |
2.5 Новости
| Компонент | Описание | Завершённость |
|---|
| News Feed | Лента новостей с пагинацией (20 на страницу), скелетоны | 100% |
| News Detail | Детальная страница новости | 100% |
| News Translations | Edge Function translate-news (Google Translate API), хранение в news_translations | 100% |
2.6 Настройки
| Компонент | Описание | Завершённость |
|---|
| Preferences | Язык (4 локали), тёмная/светлая тема | 100% |
| Location | Глобальная система: 200 стран, 33K+ городов, PostGIS, автоопределение GPS | 100% |
| Preferred Maps App | Выбор картографического приложения (Apple, Google, Waze, 2GIS, Yandex), deep links | 100% |
| Feedback | Отправка отзывов (bug/idea/other) с rate-limiting (20/день) | 100% |
| Log Out / Delete Account | Выход, удаление аккаунта с grace period | 100% |
2.7 Локализация (i18n)
| Компонент | Описание | Завершённость |
|---|
| Flutter l10n | ARB-файлы для EN, RU, ES, FR; класс S | 95% |
| Server-side i18n | Переводы стран, городов, позиций, уведомлений | 90% |
| Auto-detect locale | Определение языка системы, fallback на EN | 100% |
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)
Включён на всех основных таблицах:
| Таблица | SELECT | INSERT | UPDATE | DELETE |
|---|
| profiles | authenticated (все) | own | own | — |
| matches | active + own history | — | — | — |
| match_players | authenticated (все) | own | own | own |
| match_ratings | own | own+finished | own | — |
| notifications | own | own | own | — |
| user_devices | own | own | own | — |
| app_feedback | own | own | — | — |
| fields | public read | — | — | — |
| news_posts | authenticated + published | — | — | — |
| king_of_match_votes | own | own+conditions | — | — |
Замечания по RLS:
cities и countries — RLS не включён (видимо, public read intended, но лучше явно).
player_positions и positions — RLS не включён.
user_agreements — RLS не включён (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 Критический
go_router подключён, но не используется — мёртвая зависимость, увеличивает размер бандла.
- Тест-файл — шаблонный Counter test — не имеет отношения к проекту, тесты фактически отсутствуют (0% покрытие).
.env включён в assets Flutter (pubspec.yaml, строка 115) — потенциальная утечка секретов в бандл.
- Таблица
friendships не в дампе схемы — риск рассинхронизации между окружениями.
- Таблица
notification_preferences не в миграциях — worker на неё ссылается, но без миграции новое окружение не запустится.
5.2 Значительный
- Create Match — заглушка. Матчи можно создавать только из Supabase Studio. Это блокирует пользовательский рост.
- Нет пагинации в списках матчей — при росте данных экран будет подгружать всё сразу.
refreshUnreadCount() делает SELECT id и считает length на клиенте — неэффективно; нужно count: 'exact', head: true.
- Формат даты захардкожен на
en_US — несмотря на 4 локали.
- Placeholder-изображение для поля — внешний URL (
dhresource.com) — внешняя зависимость, может сломаться.
5.3 Средний
- Нет offline-поддержки — при отсутствии сети показывается просто ошибка.
- Нет аналитики — ни Firebase Analytics, ни Amplitude, ни Posthog.
- Нет crash reporting — ни Sentry, ни Crashlytics.
- Нет CI/CD конфигурации в репозитории.
- Нет pagination в Friends/News (News имеет пагинацию, но Friends — нет).
- King of the Match: DELETE policy отсутствует —
cancelVote() на клиенте вызывает delete, но в RLS нет DELETE policy на king_of_match_votes.
- Нет валидации длины имени на клиенте при онбординге (только
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_devices | FCM-токены устройств | Да |
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)
- notifications-worker — основной воркер уведомлений. Cron-based (через pg_cron -> HTTP). FCM v1 API, локализация, opt-out, dead token cleanup. Качественная реализация.
- translate-news — перевод новостей через Google Translate API при публикации.
- 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 проект готов к публичному запуску.