Android · Kotlin · Jetpack Compose

CRONOS.

Мобильная платформа управления молодёжными мероприятиями. Включает систему кадрового резерва, AI-аналитику на базе Qwen 3.5, реалтайм-мониторинг безопасности и ролевую модель доступа.

4 Роли пользователей
21 Экранов
6 AI-функций
500 Пользователей в мониторинге
Платформа
Android 7.0+ (API 24)
Язык
Kotlin 1.9 / JVM 17
UI-фреймворк
Jetpack Compose + Material 3
Backend
Supabase (PostgreSQL)
AI-модель
Qwen 3.5 via GenAPI
Архитектура
MVVM + Clean Architecture

Технологический стек

Проект построен на современном Android-стеке с декларативным UI и реактивным управлением состоянием через StateFlow.

Core

Android / Kotlin

minSdk 24, targetSdk 34, Kotlin 1.9, JVM 17, AGP 8.3.2, Gradle 8.5

UI

Jetpack Compose

Material 3, Navigation Compose 2.7.6, Hilt Navigation Compose 1.1.0, Compose BOM 2024.02.00

DI

Hilt

Dagger Hilt 2.51, KSP 1.9.20-1.0.14, ViewModel injection через @HiltViewModel

Backend

Supabase

BOM 1.4.7 — postgrest-kt, gotrue-kt, realtime-kt. Таблица profiles в PostgreSQL

Network

Ktor Client

Android engine 2.3.7, ContentNegotiation, kotlinx.serialization, OkHttp под капотом

Charts

Vico 1.13.1

papanicolas/vico compose-m3, столбчатые графики активности, entryModelOf() API

Images

Coil 2.5.0

coil-compose + coil-gif для анимированных GIF-фонов на экране авторизации

Camera

CameraX + ZXing

androidx.camera 1.3.1, ZXing core 3.5.2 (isTransitive=false), QR-сканер участников

Serialization

kotlinx.serialization

1.6.2, encodeDefaults=true, ignoreUnknownKeys=true глобально на Supabase-клиенте

Coroutines

Kotlin Coroutines

1.7.3, StateFlow для UI-состояния, viewModelScope для запросов, Flow для стриминга

PDF

Android PdfDocument

Встроенный android.graphics.pdf.PdfDocument, MediaStore API для Android 10+, папка Downloads

Build

Gradle KTS

build.gradle.kts, --add-opens JVM args для совместимости AGP 8.3.2 + Gradle 8.5

app/build.gradle.kts — ключевые зависимости
// Supabase BOM
implementation(platform("io.github.jan-tennert.supabase:bom:1.4.7"))
implementation("io.github.jan-tennert.supabase:postgrest-kt")
implementation("io.github.jan-tennert.supabase:gotrue-kt")
implementation("io.github.jan-tennert.supabase:realtime-kt")

// Ktor HTTP Client
implementation("io.ktor:ktor-client-android:2.3.7")
implementation("io.ktor:ktor-client-content-negotiation:2.3.7")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.7")

// Vico Charts
implementation("com.papanicolas.vico:compose-m3:1.13.1")

// Coil + GIF
implementation("io.coil-kt:coil-compose:2.5.0")
implementation("io.coil-kt:coil-gif:2.5.0")

// ZXing (без транзитивных зависимостей)
implementation("com.google.zxing:core:3.5.2") { isTransitive = false }

// Hilt
implementation("com.google.dagger:hilt-android:2.51")
ksp("com.google.dagger:hilt-compiler:2.51")

Архитектура

Паттерн MVVM + Clean Architecture. Однонаправленный поток данных через StateFlow. Dependency Injection через Hilt. Каждый экран имеет собственный ViewModel с изолированным UiState.

UI Layer
Screens (Composables) ViewModels UiState (data class) collectAsState()
Data Layer
ProfileRepository AiRepository AppStateRepository EventRepository
Source Layer
Supabase PostgreSQL GenAPI / Qwen 3.5 Android PdfDocument CameraX / ZXing
Структура пакетов
com.cronos.app/
├── data/
│   ├── model/           # Profile, PortfolioItem, EventApplication, AnticheatUser
│   └── repository/      # ProfileRepository, AiRepository, AppStateRepository, EventRepository
├── di/
│   └── AppModule.kt     # Hilt-модуль: Supabase-клиент, Ktor HttpClient, репозитории
├── ui/
│   ├── components/      # CityDropdown, переиспользуемые Composable-компоненты
│   ├── navigation/      # CronosNavigation.kt — граф навигации NavHost
│   ├── screens/
│   │   ├── auth/        # LoginScreen, LoginViewModel
│   │   ├── onboarding/  # Participant/Organizer/ObserverOnboardingScreen
│   │   ├── dashboard/   # DashboardScreen (4 роли в одном файле)
│   │   ├── profile/     # ProfileScreen, ProfileViewModel
│   │   ├── events/      # EventsScreen, CreateEventScreen
│   │   ├── leaderboard/ # LeaderboardScreen
│   │   ├── messenger/   # MessengerScreen (ChatList + ChatDetail)
│   │   ├── stats/       # StatsScreen
│   │   ├── ai/          # AiHubScreen, AiHubViewModel
│   │   ├── inspector/   # InspectorScreen, InspectorViewModel
│   │   ├── organizer/   # OrganizerProfileScreen
│   │   ├── qr/          # QrScannerScreen
│   │   ├── rating/      # RatingScreen
│   │   └── admin/       # AdminProfileScreen, AdminMessengerScreen, AnticheatScreen+VM
│   └── theme/           # Color.kt, Theme.kt, Typography.kt
└── MainActivity.kt
Паттерн ViewModel + UiState
@HiltViewModel
class InspectorViewModel @Inject constructor(
    private val aiRepository: AiRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow(InspectorUiState())
    val uiState: StateFlow<InspectorUiState> = _uiState.asStateFlow()

    fun scoreCandidate(participant: Participant) {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true) }
            val result = aiRepository.scoreCandidate(participant)
            _uiState.update { it.copy(isLoading = false, scoreResult = result) }
        }
    }
}

data class InspectorUiState(
    val candidates: List<Participant> = emptyList(),
    val isLoading: Boolean = false,
    val scoreResult: String? = null,
    val compareResult: String? = null,
    val reserveRefreshSeconds: Long = 0L
)
AppModule.kt — инициализация Supabase
@Provides @Singleton
fun provideSupabaseClient(): SupabaseClient = createSupabaseClient(
    supabaseUrl = "https://<project>.supabase.co",
    supabaseKey = "<anon-key>"
) {
    install(GoTrue)
    install(Postgrest)
    install(Realtime)
    defaultSerializer = KotlinXSerializer(Json {
        ignoreUnknownKeys = true
        encodeDefaults = true
    })
}

Роли пользователей

Система поддерживает четыре роли. Роль определяется при регистрации, сохраняется в поле role таблицы profiles и определяет доступный дашборд и функциональность.

participant

Участник

  • Профиль с hero-блоком, вертикальным градиентом и белой рамкой аватара
  • Уровни: Bronze / Silver / Gold / Reserve — цветовая маркировка бейджа
  • Статистика: мероприятия, баллы, рейтинг, прогресс-бар до следующего уровня
  • График активности по месяцам (Vico ColumnChart)
  • Достижения с количеством очков за каждое
  • Портфолио проектов с описанием и ссылками
  • Лидерборд и персональный рейтинг
  • Запись на мероприятия с фильтрами по направлению и формату
  • AI-резюме, план роста до Reserve, подбор событий
  • Мессенджер с организаторами
organizer

Организатор

  • Публичный профиль с рейтингом доверия (прогресс-бар)
  • Чипы типичных призов и наград
  • Создание мероприятий: название, дата, формат, направление, лимит участников
  • QR-сканер для регистрации участников на входе (CameraX + ZXing)
  • Панель модерации заявок: одобрить / отклонить
  • Настройки мероприятия и управление призовым фондом
  • Дашборд с двумя вкладками: Профиль и Панель управления
observer

Наблюдатель

  • Инспектор кадрового резерва с поиском и фильтрами по городу и направлению
  • Карточка кандидата: уровень, баллы, возраст, достижения, прогноз
  • AI-скоринг кандидата: оценка 1–10, риски, рекомендация
  • Сравнение двух кандидатов: 9-секционный отчёт 400+ слов
  • Экспорт PDF-отчётов с AI-заключением в папку Downloads
  • Таймер обновления резерва (12-часовой цикл, обратный отсчёт)
  • Просмотр мероприятий и статистики платформы
admin

Администратор

  • Локальный вход без обращения к Supabase
  • Отдельный Scaffold с 3-вкладочным нижним навбаром: Главная, Чат, Профиль
  • Нет доступа к соревновательным функциям (Events, Leaderboard)
  • Назначение модераторов по @username с валидацией дубликатов
  • Чат исключительно с модераторами (значок верификации)
  • Система AI-античита с реалтайм-мониторингом 500 пользователей
  • Переключение AI-режима / ручной проверки в топбаре
Уровни участников
Bronze   →  0   – 199  баллов   // начальный уровень
Silver   →  200 – 499  баллов   // активный участник
Gold     →  500 – 999  баллов   // опытный участник
Reserve  →  1000+      баллов   // кадровый резерв

Экраны приложения

Навигация реализована через Jetpack Navigation Compose. Граф маршрутов определён в CronosNavigation.kt. Каждый экран получает зависимости через Hilt.

ЭкранМаршрутРольViewModelОписание
LoginScreenloginВсеLoginViewModelGIF-фон (Coil), тёмный оверлей, вход через Supabase или admin-bypass
RoleSelectionScreenrole_selectionНовыеВыбор роли при первом входе, 4 карточки с описанием
ParticipantOnboardingScreenparticipant_onboardingУчастникИмя, город (CityDropdown), дата рождения DD.MM.YYYY, направление
OrganizerOnboardingScreenorganizer_onboardingОрганизаторНазвание организации, направление деятельности, описание
ObserverOnboardingScreenobserver_onboardingНаблюдательДолжность, организация, цель наблюдения
DashboardScreendashboardВсеDashboardViewModel4 независимых дашборда по роли, нижний навбар с иконками
ProfileScreenprofileУчастникProfileViewModelHero-блок с градиентом, статистика с иконками, Vico-график, достижения, портфолио
EventsScreeneventsУчастникСписок мероприятий, фильтры по направлению и формату (онлайн/офлайн)
LeaderboardScreenleaderboardУчастникРейтинг участников по баллам, топ-3 с подсветкой
MessengerScreenmessengerУчастникСписок чатов с организаторами, счётчики непрочитанных, кнопка назад
StatsScreenstatsУчастникДетальная статистика: мероприятия по месяцам, направления, прогресс
AiHubScreenai_hubУчастникAiHubViewModel3 AI-функции: резюме, подбор событий, план роста. Результат — только PDF
RatingScreenratingУчастникЛичный рейтинг, прогресс до следующего уровня, история изменений
InspectorScreeninspectorНаблюдательInspectorViewModelКандидаты резерва, AI-скоринг, сравнение двух кандидатов, PDF-экспорт, таймер
CreateEventScreencreate_eventОрганизаторФорма создания мероприятия: название, дата, формат, лимит, призы
QrScannerScreenqr_scannerОрганизаторCameraX Preview + ZXing BarcodeScanner, сканирование QR-кодов участников
OrganizerProfileScreenorganizer_profileОрганизаторПубличный профиль, рейтинг доверия, типичные призы, история мероприятий
AdminProfileScreenadmin_profileАдминПрофиль администратора, форма назначения модераторов, список с кнопкой удаления
AdminMessengerScreenadmin_messengerАдминЧат только с модераторами, значок щита верификации, счётчики непрочитанных
AnticheatScreenanticheatАдминAnticheatViewModelLive-мониторинг пользователей, AI-анализ, ручная проверка, PDF-отчёт безопасности
""

AI-модуль

Интеграция с Qwen 3.5 через GenAPI (OpenAI-совместимый прокси). Реализована в AiRepository.kt на базе Ktor HTTP Client. AI-текст никогда не отображается в UI — результат доступен только через PDF-экспорт.

Параметры HTTP-запроса
endpoint     : https://proxy.gen-api.ru/v1/chat/completions
model        : qwen-3-5
max_tokens   : 1500
temperature  : 0.7
content-type : application/json
auth         : Bearer <API_KEY>
01

AI-резюме достижений

Генерирует профессиональную самопрезентацию (3–4 предложения) на основе уровня, баллов, направления и достижений участника. Промпт включает все достижения с очками. Доступно в AiHubScreen. Результат — кнопка скачать PDF.

02

AI-подбор событий

Анализирует профиль участника (уровень, направление, город, интересы) и список доступных мероприятий. Возвращает топ-3 с обоснованием релевантности каждого события и рекомендацией по приоритету участия.

03

AI-план роста

Строит конкретный план достижения уровня Reserve: типы мероприятий, рекомендуемое количество в неделю, расчётные сроки в неделях, ключевые направления для прокачки. Учитывает текущий уровень и дефицит баллов.

04

AI-скоринг кандидата

В InspectorScreen — оценка потенциала по шкале 1–10, выявление рисков, рекомендация о включении в кадровый резерв. Промпт содержит полный профиль: уровень, баллы, возраст, город, направление, все достижения, прогноз. Результат — только PDF-отчёт.

05

AI-сравнение кандидатов

Подробный сравнительный отчёт из 9 разделов (400+ слов): общая активность, специализация, достижения, потенциал роста, риски, географический фактор, коммуникативные навыки, рекомендация по приоритету, итоговое заключение. Многостраничный PDF с автоматическим переносом страниц.

06

AI-сводка античита

Анализирует топ нарушителей из системы мониторинга: паттерны аномалий, статистику по платформе, рекомендации по улучшению системы безопасности. Результат — PDF-отчёт безопасности с заголовком, статистикой и заключением.

AiRepository.kt — структура запроса и постобработка
@Serializable
data class ChatRequest(
    val model: String,
    val messages: List<Message>,
    val max_tokens: Int = 1500,
    val temperature: Float = 0.7f
)

suspend fun scoreCandidate(participant: Participant): String {
    val response = client.post(ENDPOINT) {
        header("Authorization", "Bearer $API_KEY")
        contentType(ContentType.Application.Json)
        setBody(ChatRequest(model = MODEL, messages = listOf(
            Message(role = "user", content = buildPrompt(participant))
        )))
    }
    return extractContent(response.bodyAsText()).stripMarkdown()
}

// Удаление markdown из ответа AI
fun String.stripMarkdown() = this
    .replace(Regex("\\*\\*(.+?)\\*\\*"), "$1")
    .replace(Regex("#+\\s"), "")
    .replace(Regex("\\*(.+?)\\*"), "$1")
    .trim()
extractContent() — поддерживаемые форматы ответа
// 1. OpenAI standard string
choices[0].message.content  →  String

// 2. OpenAI content-array
choices[0].message.content  →  [{type: "text", text: "..."}]

// 3. Qwen thinking mode
choices[0].message.reasoning_content  →  String

// 4. GenAPI native format
output    →  String
response  →  String

Система античита

Реалтайм-мониторинг активности пользователей с AI-анализом аномалий. Реализована в AnticheatScreen + AnticheatViewModel. Поддерживает два режима: автоматический (AI) и ручной.

BanStatus.BANNED

Автоматический бан

Аномальное начисление баллов (более 80 в день), множественные аккаунты с одного IP, рост уровня менее чем за 3 дня.

BanStatus.SUSPICIOUS

Предупреждение

Подозрительная активность: более 5 мероприятий за 24 часа. Требует ручной проверки модератором.

BanStatus.CLEAN

Чистый статус

Активность в пределах нормы. Модератор может вручную изменить статус через контекстное меню на карточке пользователя.

AnticheatViewModel.kt — стриминг и переключение режима
fun startStreaming() {
    streamingJob = viewModelScope.launch {
        var index = _uiState.value.streamIndex
        while (isActive && _uiState.value.aiEnabled) {
            if (index < allUsers.size) {
                _uiState.update { state ->
                    state.copy(
                        visibleUsers = state.visibleUsers + allUsers[index],
                        streamIndex = index + 1,
                        isLive = true
                    )
                }
                index++
                if (index % 10 == 0) delay(2000L) else delay(800L)
            } else {
                _uiState.update { it.copy(isLive = false) }
                break
            }
        }
    }
}

fun toggleAi(enabled: Boolean) {
    if (!enabled) {
        streamingJob?.cancel()
        _uiState.update { it.copy(aiEnabled = false, isLive = false) }
    } else {
        _uiState.update { it.copy(aiEnabled = true) }
        startStreaming()
    }
}
LIVE-индикатор в топбаре — мигающий красный кружок, пока идёт стриминг
Переключатель AI вкл/выкл — при отключении стриминг останавливается, появляется баннер ручной проверки
Ручной режим: контекстное меню на каждой карточке — Забанить / Предупреждение / Снять статус
Фильтрация по статусу: Все / Забанены / Подозрительные / Чистые — через ChipGroup
AI-сводка — кнопка видна только при включённом AI, генерирует PDF-отчёт безопасности
500 пользователей: ~8% забанено (~40 чел.), ~14% подозрительных (~70 чел.), остальные чистые
Каждый пользователь: id, имя, @username, pointsPerDay, eventsIn24h, duplicateIp, rapidLevelUp, rating, banReason

Авторизация

Поддерживаются два режима входа: через Supabase Auth и локальный admin-bypass. После входа профиль загружается из таблицы profiles, поле role определяет дашборд.

Supabase Auth

Email + Password через gotrue-kt. После успешного входа загружается профиль из таблицы profiles. Поле role определяет дашборд. При первом входе — онбординг с выбором роли и заполнением профиля.

supabase.gotrue.signInWith(Email) { ... }

Admin Bypass

Локальная проверка без обращения к Supabase. Флаг AppStateRepository.isAdminMode активирует admin-дашборд и блокирует доступ к соревновательным функциям.

admin@gmail.com / admin123
LoginViewModel.kt — логика входа
fun login(email: String, password: String) {
    viewModelScope.launch {
        // Локальный admin-bypass
        if (email == "admin@gmail.com" && password == "admin123") {
            appStateRepository.setAdminMode(true)
            navigate("dashboard")
            return@launch
        }
        // Supabase Auth
        supabase.gotrue.signInWith(Email) {
            this.email = email
            this.password = password
        }
        val userId = supabase.gotrue.currentUserOrNull()?.id ?: return@launch
        val profile = profileRepository.getProfile(userId)
        if (profile == null) {
            navigate("role_selection")   // первый вход — онбординг
        } else {
            navigate("dashboard")        // профиль есть — сразу дашборд
        }
    }
}

Модели данных

Основные data-классы приложения. Сериализация через kotlinx.serialization с аннотацией @Serializable.

Profile.kt
@Serializable
data class Profile(
    val id: String,
    val displayName: String,
    val username: String?,
    val city: String?,
    val role: String,                    // participant | organizer | observer | admin
    val avatarUrl: String?,
    val birthDate: String?,              // YYYY-MM-DD
    val direction: String?,              // IT | Спорт | Культура | Наука | Бизнес
    val interests: List<String>?,
    val verificationStatus: String,      // approved | pending | rejected
    val rating: Int = 0,
    val eventsCount: Int = 0,
    val level: String = "Bronze"         // Bronze | Silver | Gold | Reserve
)
Participant.kt
data class Participant(
    val id: String,
    val name: String,
    val username: String,
    val city: String,
    val direction: String,
    val eventsCount: Int,
    val rating: Int,
    val level: String,
    val age: Int,
    val achievements: List<Pair<String, Int>>,  // название → очки
    val forecast: String                           // прогноз развития
)
AnticheatUser.kt
data class AnticheatUser(
    val id: String,
    val name: String,
    val username: String,
    val pointsPerDay: Float,     // аномалия если > 80
    val eventsIn24h: Int,        // аномалия если > 5
    val duplicateIp: Boolean,
    val rapidLevelUp: Boolean,
    val rating: Int,
    val status: BanStatus,       // CLEAN | SUSPICIOUS | BANNED
    val banReason: String?
)

enum class BanStatus { CLEAN, SUSPICIOUS, BANNED }
PortfolioItem.kt
data class PortfolioItem(
    val title: String,
    val description: String,
    val year: Int,
    val url: String?,
    val tags: List<String>
)

База данных

Backend — Supabase (PostgreSQL). Приложение работает с единственной таблицей profiles. Все остальные данные платформы хранятся и обрабатываются на стороне приложения.

Таблица profiles — SQL-схема
CREATE TABLE profiles (
    id                  UUID PRIMARY KEY REFERENCES auth.users(id),
    display_name        TEXT NOT NULL,
    username            TEXT UNIQUE,
    city                TEXT,
    role                TEXT NOT NULL CHECK (role IN ('participant','organizer','observer','admin')),
    avatar_url          TEXT,
    birth_date          DATE,
    direction           TEXT,
    interests           TEXT[],
    verification_status TEXT DEFAULT 'pending' CHECK (verification_status IN ('approved','pending','rejected')),
    rating              INTEGER DEFAULT 0,
    events_count        INTEGER DEFAULT 0,
    level               TEXT DEFAULT 'Bronze',
    created_at          TIMESTAMPTZ DEFAULT NOW(),
    updated_at          TIMESTAMPTZ DEFAULT NOW()
);
ProfileRepository.kt — операции с Supabase
class ProfileRepository @Inject constructor(
    private val supabase: SupabaseClient
) {
    suspend fun getProfile(userId: String): Profile? =
        supabase.postgrest["profiles"]
            .select { filter { eq("id", userId) } }
            .decodeSingleOrNull<Profile>()

    suspend fun upsertProfile(profile: Profile) =
        supabase.postgrest["profiles"].upsert(profile)

    suspend fun updateRating(userId: String, delta: Int) =
        supabase.postgrest["profiles"]
            .update({ set("rating", "rating + $delta") }) {
                filter { eq("id", userId) }
            }
}
Row Level Security (RLS)
-- Пользователь видит только свой профиль
CREATE POLICY "profiles_select" ON profiles
    FOR SELECT USING (auth.uid() = id);

-- Пользователь редактирует только свой профиль
CREATE POLICY "profiles_update" ON profiles
    FOR UPDATE USING (auth.uid() = id);

PDF-экспорт

Все AI-отчёты экспортируются в PDF через встроенный android.graphics.pdf.PdfDocument. На Android 10+ используется MediaStore API для сохранения в папку Downloads без запроса разрешений.

InspectorScreen

Отчёт по кандидату

Имя, уровень, баллы, направление, достижения, AI-оценка 1–10, риски, рекомендация. Имя файла: Иван_Новиков.pdf

InspectorScreen

Сравнение кандидатов

Многостраничный PDF, 9 разделов, 400+ слов. Автоматический перенос страниц при превышении высоты. Имя файла: Сравнение_Иванов_Петров.pdf

AiHubScreen

AI-резюме / план роста

Профессиональная самопрезентация или план достижения уровня Reserve. Сохраняется в Downloads с именем участника.

AnticheatScreen

Отчёт безопасности

Топ нарушителей, статистика по платформе (забанено/подозрительных/чистых), AI-заключение и рекомендации.

Сохранение PDF через MediaStore (Android 10+)
fun exportPdf(context: Context, content: String, fileName: String) {
    val document = PdfDocument()
    val pageInfo = PdfDocument.PageInfo.Builder(595, 842, 1).create()
    var page = document.startPage(pageInfo)
    val paint = Paint().apply { textSize = 12f; color = Color.BLACK }
    var y = 40f
    val lineHeight = 18f
    val maxWidth = 515f

    content.split("\n").forEach { line ->
        val words = line.split(" ")
        var currentLine = ""
        words.forEach { word ->
            val test = if (currentLine.isEmpty()) word else "$currentLine $word"
            if (paint.measureText(test) > maxWidth) {
                page.canvas.drawText(currentLine, 40f, y, paint)
                y += lineHeight
                if (y > 800f) {  // новая страница
                    document.finishPage(page)
                    page = document.startPage(pageInfo)
                    y = 40f
                }
                currentLine = word
            } else currentLine = test
        }
        if (currentLine.isNotEmpty()) {
            page.canvas.drawText(currentLine, 40f, y, paint)
            y += lineHeight
        }
    }
    document.finishPage(page)

    // MediaStore API (Android 10+)
    val values = ContentValues().apply {
        put(MediaStore.Downloads.DISPLAY_NAME, "$fileName.pdf")
        put(MediaStore.Downloads.MIME_TYPE, "application/pdf")
    }
    val uri = context.contentResolver.insert(
        MediaStore.Downloads.EXTERNAL_CONTENT_URI, values
    )
    uri?.let { context.contentResolver.openOutputStream(it)?.use { stream ->
        document.writeTo(stream)
    }}
    document.close()
}

Безопасность

Меры безопасности на уровне приложения, базы данных и сетевого взаимодействия.

Row Level Security (RLS) в Supabase — пользователь читает и редактирует только свой профиль
Supabase anon key не даёт доступа к данным других пользователей без RLS-политик
Admin-bypass работает только локально — флаг isAdminMode не передаётся на сервер
WRITE_EXTERNAL_STORAGE ограничен maxSdkVersion=28 — на Android 9+ не требуется
CAMERA разрешение запрашивается только при открытии QrScannerScreen
API-ключ GenAPI хранится в коде — рекомендуется вынести в BuildConfig или зашифрованное хранилище
ignoreUnknownKeys=true предотвращает краши при изменении схемы API без обновления приложения
Все сетевые запросы выполняются через HTTPS
AndroidManifest.xml — разрешения
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission
    android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    android:maxSdkVersion="28" />

UI-система

Тема приложения — чисто чёрный фон (pure black), Material 3. Все цвета определены в Color.kt, тема — в Theme.kt.

Color.kt — цветовая палитра
// Основные цвета
val Black       = Color(0xFF000000)
val Surface     = Color(0xFF0A0A0A)
val SurfaceVar  = Color(0xFF111111)
val Primary     = Color(0xFF00E5FF)   // акцент — голубой
val OnPrimary   = Color(0xFF000000)

// Уровни участников
val BronzeColor  = Color(0xFFCD7F32)
val SilverColor  = Color(0xFFC0C0C0)
val GoldColor    = Color(0xFFFFD700)
val ReserveColor = Color(0xFF00E5FF)
ProfileScreen — hero-блок с градиентом
Box(
    modifier = Modifier
        .fillMaxWidth()
        .height(280.dp)
        .background(
            Brush.verticalGradient(
                colors = listOf(
                    MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),
                    MaterialTheme.colorScheme.surface
                )
            )
        )
) {
    // Аватар с белой рамкой
    Box(
        modifier = Modifier
            .size(88.dp)
            .border(3.dp, Color.White, CircleShape)
            .clip(CircleShape)
            .background(MaterialTheme.colorScheme.surfaceVariant)
    )
    // Бейдж уровня с цветом
    val levelColor = when (profile.level) {
        "Gold"    -> GoldColor
        "Reserve" -> ReserveColor
        "Silver"  -> SilverColor
        else      -> BronzeColor
    }
}
CityDropdown

Выпадающий список городов

Поиск с фильтрацией по вводу, все города России, ExposedDropdownMenuBox из Material 3, используется в онбординге участника.

Vico Chart

График активности

ColumnChart с данными по месяцам (Янв–Дек), entryModelOf() API, тема Material 3, отображается в ProfileScreen и StatsScreen.

GIF Background

Анимированный фон

LoginScreen использует register.gif из assets через Coil AsyncImage с ImageDecoder.Factory. Поверх — тёмный полупрозрачный оверлей.

QR Scanner

Камера + ZXing

CameraX Preview в AndroidView, BarcodeScanner из ZXing, ImageAnalysis.Analyzer, результат передаётся через callback в ViewModel.