Self-hosted мессенджер для команды: Rocket.Chat white-label 2026 | Илья Арестов
Безопасность

chat.ilia.ae: self-hosted Rocket.Chat с white-label системой сборки и Streamlit-дашбордом

Последние два года я поддерживаю chat.ilia.ae — self-hosted инстанс Rocket.Chat с кастомными white-label приложениями для iOS и Android, опубликованными в App Store и Google Play. По сути это один self-hosted мессенджер для команды в двух брендах: chat.ilia.ae для личного и консалтингового использования, и EL·Hub для корпоративной коммуникации внутри Estateliga. Оба приложения — форки официального мобильного клиента Rocket.Chat на React Native, ребрендированные с собственными иконками, splash-экранами, URL серверов, конфигами Firebase и демо-режимами, которые генерируют pixel-perfect скриншоты для публикации в сторах. Приложения бесплатные, не собирают данных пользователей и подключаются исключительно к серверам под моим контролем.

В этой статье разбирается весь стек за этими приложениями: Build UI (веб-интерфейс на Streamlit, работающий на build.ilia.ae:8503), система white-label сборки, которая компилирует оба бренда из одной кодовой базы, pipeline скриншотов и фикстур, управляющий демо-состояниями для листингов в сторах, и интеграция со всем моим тулкитом mobile-stores-cicd для публикации в Apple и Google. Здесь есть скриншоты каждой страницы Build UI, реальные CLI-команды, архитектурные решения и обоснование системы фикстур, благодаря которой мультиязычные скриншоты создаются без ручного взаимодействия с устройством.

Содержание
  1. Почему self-hosted Rocket.Chat
  2. Два бренда, одна кодовая база
  3. Система white-label сборки: build.py
  4. Переключение брендов
  5. Процесс сборки iOS
  6. Процесс сборки Android
  7. Управление версиями
  8. Build UI: операторская консоль на Streamlit
  9. Build ui home
  10. Build ui status
  11. Build ui ios
  12. Build ui android
  13. Build ui version
  14. Build ui upstream
  15. Build ui screenshots
  16. Вкладка Capture
  17. Вкладка Headlines
  18. Вкладка Fixtures
  19. Вкладка Gallery
  20. Панель синхронизации с Crowdin
  21. Build ui logs
  22. Демо-режим и система фикстур
  23. Pipeline фикстур: кодогенерация из YAML в TypeScript
  24. Изоляция брендов
  25. Интеграция с Crowdin для текстов фикстур
  26. Интеграция pipeline скриншотов с mobile-stores-cicd
  27. Архитектура
  28. Безопасность и деплой
  29. Присутствие в App Store и Google Play
  30. Почему не официальное приложение Rocket.Chat
  31. Рабочий процесс: типичный релиз
  32. Во сколько это обходится
  33. Ограничения и что я бы сделал иначе
  34. Часто задаваемые вопросы
  35. Может ли кто угодно использовать chat.ilia.ae?
  36. Почему Rocket.Chat, а не Matrix, Mattermost или Zulip?
  37. Есть Android-приложение?
  38. Чем это отличается от WhatsApp или Telegram?
  39. Как Build UI соотносится с mobile-stores-cicd?
  40. Можешь развернуть это для моей команды?
  41. Система сборки open source?
  42. Исходный код и консалтинг
  43. Нужна консультация?

Почему self-hosted Rocket.Chat

self-hosted мессенджер для команды на базе Rocket.Chat — приложение chat.ilia.ae

Короткий ответ — суверенитет данных. Моя консалтинговая работа охватывает финтех-комплаенс, блокчейн-архитектуру и стратегию кибербезопасности для клиентов в регулируемых отраслях. Сообщения об инфраструктуре клиентов, процедурах управления ключами и результатах комплаенс-проверок не могут лежать на серверах Slack в США, подпадая под повестки по CLOUD Act. Они не могут храниться на инфраструктуре Microsoft на условиях их обработки данных. И тем более не могут проходить через потребительские приложения вроде WhatsApp или Telegram, где анализ метаданных и скрапинг контактов — стандартная практика.

Rocket.Chat — это open-source, self-hosted решение без тарификации за пользователя. Я запускаю его на инфраструктуре, которой владею, за reverse proxy, который настраиваю сам, с хранилищем MongoDB, которое сам бэкаплю. Нет vendor lock-in, потому что формат данных хорошо задокументирован и инструменты экспорта работают. Нет ограничения функций по тарифному плану. А поскольку и chat.ilia.ae, и EL·Hub имеют собственные опубликованные приложения в App Store и Google Play, члены команды и клиенты устанавливают этот self-hosted мессенджер для команды так же, как любой другой. Никаких ссылок на TestFlight, никакого сайдлоадинга, никакой регистрации в MDM. Просто скачай из стора и войди.

Практическая польза проста: я получаю функциональность уровня Slack (каналы, треды, обмен файлами, push-уведомления, поиск, реакции, закреплённые сообщения) на своих условиях. Приложения бесплатны для всех. Метка «Data Not Collected» в App Store — правдивая. А когда клиент спрашивает, где хранятся его сообщения, я могу указать на конкретный сервер в конкретном дата-центре, а не на расплывчатое «наша глобальная инфраструктура».

Стоит упомянуть и сравнение по стоимости. Slack берёт $8.75 за пользователя в месяц на тарифе Pro. Microsoft Teams требует подписку Microsoft 365 Business минимум за $6 за пользователя в месяц. При 20 пользователях это $2,100/год за Slack или $1,440/год за Teams, плюс любые надбавки за комплаенс, которые требует корпоративный тариф. Self-hosted Rocket.Chat не берёт ничего за пользователя. Стоимость сервера — фиксированные инфраструктурные расходы, а мобильные приложения бесплатны. При любом размере команды больше нескольких человек экономика в пользу self-hosting, особенно если учитывать стоимость комплаенса за хранение сообщений на сторонней платформе, которые потом нужно аудитить, экспортировать и контролировать через их API, а не через свою собственную базу данных.

Два бренда, одна кодовая база

Поддерживать два отдельных форка приложения Rocket.Chat было бы невозможно. Каждый мёрж с upstream, каждый апгрейд версии React Native, каждое обновление зависимостей пришлось бы делать дважды. Вместо этого я веду один форкнутый репозиторий с системой переключения брендов, которая перезаписывает все платформенно-специфичные конфигурационные файлы в момент сборки.

Два бренда обслуживают разные аудитории. chat.ilia.ae — личный инстанс для консалтинговых проектов, координации разработки и коммуникации с клиентами. EL·Hub — self-hosted мессенджер для команды Estateliga по недвижимости, связывающий агентов, менеджеров и бэк-офис между Дубаем и Алматы. Оба приложения выглядят и ощущаются совершенно по-разному для конечных пользователей, но под капотом делят одну кодовую базу на React Native, одну навигационную структуру и один и тот же набор функций upstream Rocket.Chat.

Что различается между брендами: иконки приложения, splash-экраны, отображаемые имена, bundle-идентификаторы, URL серверов, конфигурации Firebase Cloud Messaging, метаданные App Store и Google Play, а также данные фикстур демо-режима для скриншотов. Переключение бренда затрагивает каждый конфигурационный файл, содержащий любое из этих значений, а система сборки гарантирует, что при переключении с одного бренда на другой в скомпилированном выводе не остаётся ни одного артефакта от предыдущего бренда.

Система white-label сборки: build.py

Ядро процесса сборки — build.py, единый Python CLI, управляющий обоими брендами на обеих платформах. Это не обёртка над Fastlane или каким-либо Ruby-тулчейном. Он читает определения брендов из YAML, напрямую перезаписывает платформенные конфиги и вызывает xcodebuild и gradle для собственно компиляции.

Переключение брендов

Когда ты выполняешь build.py switch --brand chat-ilia или build.py switch --brand el-hub, скрипт перезаписывает следующие файлы:

  • iOS: Info.plist (bundle ID, отображаемое имя, URL-схемы), .pbxproj (product bundle identifier, team ID), GoogleService-Info.plist (конфигурация Firebase), каталоги ассетов иконок приложения, ассеты экрана запуска
  • Android: build.gradle (applicationId, versionName), google-services.json (конфигурация Firebase), AndroidManifest.xml (имя пакета, хосты deep link), strings.xml (отображаемое имя), ресурсы иконок лаунчера для всех плотностей экрана
  • Общее: app.json (имя приложения, slug), конфигурационные файлы окружения (URL сервера, идентификатор бренда), ссылки на фикстуры демо-режима

Перезапись детерминированная и идемпотентная. Двойной запуск с тем же брендом даёт идентичный результат. Запуск с другим брендом чисто заменяет каждое значение. После каждого переключения есть шаг верификации, который делает diff рабочего дерева с эталонным состоянием для данного бренда и сообщает о файлах, не соответствующих ожиданиям.

Определения брендов хранятся в YAML-конфиге, который связывает каждый slug бренда с полным набором идентификаторов. Вот упрощённый вид записи бренда:

chat-ilia:
  display_name: "chat.ilia.ae"
  ios_bundle_id: "chat.ilia.ae"
  android_package: "chat.rocket.ilia.ae"
  server_url: "https://chat.ilia.ae"
  firebase_ios: "brands/chat-ilia/GoogleService-Info.plist"
  firebase_android: "brands/chat-ilia/google-services.json"
  icons: "brands/chat-ilia/icons/"
  fixtures: "brands/chat-ilia/fixtures.yaml"

el-hub:
  display_name: "EL·Hub"
  ios_bundle_id: "chat.estateliga.work"
  android_package: "chat.estateliga.work"
  server_url: "https://chat.estateliga.work"
  firebase_ios: "brands/el-hub/GoogleService-Info.plist"
  firebase_android: "brands/el-hub/google-services.json"
  icons: "brands/el-hub/icons/"
  fixtures: "brands/el-hub/fixtures.yaml"

Добавление третьего бренда означает добавление ещё одной записи в этот файл и предоставление соответствующих ассетов. Система сборки нигде не хардкодит имена брендов — она итерирует по конфигу и применяет всё, что в нём определено.

Процесс сборки iOS

Сборка iOS проходит через CocoaPods для разрешения зависимостей, затем xcodebuild с конфигурацией Release, нацеленной на .xcworkspace. На выходе — .xcarchive, который экспортируется в IPA для загрузки в App Store или дистрибуции через TestFlight. Флаги сборки контролируют signing identity, выбор provisioning profile и включение bitcode (не включается — Apple его депрекейтнул).

Pod install — отдельный, необязательный шаг, потому что он занимает 2-4 минуты и нужен только при изменении зависимостей. Build UI выводит его как чекбокс, чтобы я мог пропустить его при итеративных сборках, где менялся только код приложения.

Процесс сборки Android

Сборка Android использует Gradle с уже пропатченным applicationId бренда в файлах сборки. На выходе — APK (для прямой установки и тестирования) или AAB (Android App Bundle, обязательный для публикации в Google Play). Keystores привязаны к бренду — у каждого бренда свой ключ подписи, и система сборки проверяет наличие правильного keystore перед началом компиляции.

Управление версиями

Обновление версии координируется сразу в нескольких файлах одновременно. Одна команда build.py version --bump minor обновляет:

  • package.json — источник истины для строки версии
  • Info.plistCFBundleShortVersionString и CFBundleVersion
  • project.pbxprojMARKETING_VERSION и CURRENT_PROJECT_VERSION
  • Android build.gradleversionName и versionCode

Все четыре файла должны совпадать. Если они расходятся — а это случается при ручном редактировании файла или когда мёрж приносит изменения из upstream — страница версий в Build UI показывает предупреждение о расхождении с точным несовпадением. Подробнее об этом ниже.

Build UI: операторская консоль на Streamlit

Build UI — это веб-приложение на Streamlit, предоставляющее браузерный интерфейс для каждой операции, которую поддерживает build.py. Работает по адресу https://build.ilia.ae:8503/ с опциональной TLS-терминацией. Это не отдельная система со своей логикой — это тонкая обёртка, которая вызывает те же CLI-команды и показывает их вывод в реальном времени.

Анимированная демонстрация Build UI с навигацией по страницам, переключением брендов и потоковым выводом сборки в реальном времени

Интерфейс содержит 8 страниц: Home, Status, iOS, Android, Version, Upstream, Screenshots и Logs. Навигация через боковую панель с иконками Material Design. Каждая страница разделяет глобальную боковую панель, отображающую текущее состояние репозитория: активную ветку, чистое или грязное рабочее дерево, текущий бренд, строки версий iOS и Android, и временную метку последней сборки.

Потоковый вывод подпроцессов — ключевая фича. Когда ты запускаешь сборку (iOS или Android), UI создаёт подпроцесс и передаёт его stdout в блок st.code, обновляющийся в реальном времени. Ты видишь каждое предупреждение компилятора, каждое сообщение линковщика, каждую строку pod install по мере их появления. Хэндл подпроцесса хранится в session state Streamlit, что позволяет отменить запущенную сборку из UI без убийства всего серверного процесса.

Build ui home

Страница Home Build UI с панелями состояния сборки iOS и Android и индикатором грязного дерева

Страница Home — стартовый дашборд, показывающий общее состояние репозитория. Отображает текущий активный бренд, статус сборки iOS (время последней сборки, путь к выходному файлу, размер архива), статус сборки Android (APK/AAB выход, задача Gradle) и заметное предупреждение о грязном дереве, если в рабочей директории есть незакоммиченные изменения. Индикатор грязного дерева важен, потому что сборка из грязного дерева означает, что скомпилированный бинарник не соответствует никакому конкретному коммиту, что усложняет отладку проблем в релизе.

Также Home показывает git-ветку и последние сообщения коммитов, чтобы я мог убедиться, что собираю из правильной ветки перед запуском компиляции. Кнопки сборки на этой странице нет — она чисто информационная. Триггеры сборки находятся на страницах iOS и Android.

Build ui status

Страница Status Build UI с кроссплатформенным дашбордом состояния, сравнительными таблицами брендов и проверкой keystore

Status — это кроссплатформенный дашборд состояния. Он запускает проверки по обоим брендам и обеим платформам, а затем представляет результаты в сравнительных таблицах. Для каждого бренда показывается: bundle ID (iOS) и application ID (Android), строки версий из всех исходных файлов, статус конфигурации Firebase, статус signing identity, валидность provisioning profile и наличие keystore.

Самый полезный раздел — проверка расхождений. Она сравнивает значения конфигов, которые должны совпадать между файлами (например, версия в package.json и версия в Info.plist), и помечает любые несовпадения. Это ловит проблемы до компиляции, где они либо сломают сборку, либо произведут бинарник с неправильными метаданными. Защита keystore работает аналогично: если Android keystore для активного бренда отсутствует или имеет просроченный сертификат, страница статуса сообщает об этом до того, как ты потратишь 10 минут на сборку Gradle, которая упадёт на шаге подписи.

Build ui ios

Страница iOS Build UI с выпадающим списком брендов, чекбоксом pod install и опциями конфигурации xcodebuild

Страница iOS — место, где происходят сборки. Вверху — выпадающий список брендов (chat-ilia или el-hub). Ниже — два опциональных чекбокса: «Run pod install» и «Run xcodebuild.» Можно запустить любой из них или оба. Pod install отдельно полезен, когда ты только что смёрджил upstream-изменения, обновившие Podfile.lock. Xcodebuild отдельно полезен, когда поды уже установлены и нужен более быстрый цикл пересборки.

Когда ты нажимаешь кнопку сборки, сначала запускается переключение бренда (если выбранный бренд отличается от текущего), затем pod install (если отмечен), затем xcodebuild (если отмечен). Каждый шаг стримит вывод в блок кода. Если какой-либо шаг падает, pipeline останавливается, и вывод ошибки остаётся на экране. Конфигурация сборки по умолчанию — Release с signing identity для дистрибуции через App Store, но можно переключиться на Debug для более быстрой итерации при разработке.

Build ui android

Страница Android Build UI с селектором проекта, переключателем типа сборки Gradle и статусом проверки keystore

Страница Android зеркалит структуру страницы iOS, но адаптирована под специфику Android. Селектор проекта выбирает бренд. Переключатель типа сборки переключает между Release и Debug. Сборка Release производит подписанный AAB для Google Play; сборка Debug производит APK для локального тестирования и установки на эмулятор.

Защита keystore всегда видна на этой странице. Перед запуском сборки Gradle UI проверяет, что файл keystore существует, что пароль keystore установлен в переменных окружения и что алиас ключа соответствует бренду. Если что-то из этого не проходит, кнопка сборки деактивируется, а недостающее требование подсвечивается. Это предотвращает самую частую ошибку сборки Android: проблемы с конфигурацией подписи, которые всплывают только после 5-8 минут компиляции Gradle.

Build ui version

Страница Version Build UI с координированными элементами управления semver-бампом и предупреждениями о расхождении между package.json, Info.plist и build.gradle

Управление версиями в проекте React Native, нацеленном и на iOS, и на Android, требует синхронизации четырёх файлов: package.json, Info.plist, project.pbxproj и build.gradle. Страница Version читает все четыре, отображает их рядом и подсвечивает любые расхождения. Одна кнопка «Bump» инкрементирует версию во всех файлах атомарно.

Страница поддерживает semver-бамп: patch, minor и major. Также можно задать конкретную строку версии вручную, что удобно при синхронизации с тегом upstream-релиза Rocket.Chat. После бампа страница перечитывает все файлы и подтверждает их совпадение. Если ручное редактирование или неудачный мёрж оставили файлы рассинхронизированными, предупреждение о расхождении показывает, какой именно файл содержит какую версию, чтобы ты мог решить — принудительно выровнять или разобраться.

Build ui upstream

Страница Upstream Build UI с отслеживанием upstream-тегов Rocket.Chat, доступными обновлениями и статусом мёрджа

И chat.ilia.ae, и EL·Hub — форки официального мобильного клиента Rocket.Chat на React Native. Upstream-репозиторий регулярно выпускает новые теги, и поддержание форка в актуальном состоянии требует периодических мёрджей. Страница Upstream отслеживает связь между форком и официальным репозиторием.

Она показывает текущий URL upstream remote, последние полученные теги и на сколько коммитов форк опережает или отстаёт от последнего upstream-тега. Кнопка «Fetch tags» обновляет список тегов из remote. Сам мёрж намеренно не автоматизирован через UI. Upstream-мёрджи в проекте React Native часто порождают конфликты в нативных файлах сборки, lock-файлах зависимостей и конфигурационных файлах, требующих ручного разрешения. Страница показывает тебе состояние и позволяет решить, когда мёрджить; сам мёрж происходит в терминале, где можно нормально разрулить конфликты.

Build ui screenshots

Страница Screenshots Build UI с четырьмя вкладками: управление захватом, редактор заголовков, конфигурация фикстур и галерея скриншотов

Страница Screenshots — самая сложная часть Build UI. У неё четыре вкладки: Capture, Headlines, Fixtures и Gallery. Вместе они управляют полным pipeline от запуска захвата скриншотов на устройствах до компоновки финальных изображений для сторов.

Вкладка Capture

Вкладка Capture запускает захват скриншотов на симуляторах устройств. Для iOS используются Maestro-флоу, которые проводят Simulator через каждое демо-состояние, ждут стабилизации UI и захватывают в нужном разрешении для каждого семейства устройств (iPhone 6.9″, iPhone 6.5″, iPad 13″). Для Android используются кастомные команды захвата, которые запускают приложение с определёнными intent extras для установки демо-режима и локали, затем захватывают через adb.

Каждый запуск захвата производит сырые скриншоты, организованные по бренду, семейству устройств, локали и экрану. Сырые захваты не готовы для стора — им нужны рамки устройств, текст заголовков и фоновая композиция. Эта часть происходит в тулките mobile-stores-cicd, который берёт сырые захваты отсюда и производит финальные составные изображения для App Store Connect и Google Play Console.

Вкладка Headlines

Каждый скриншот в сторе имеет заголовок — текст, который появляется над или под рамкой устройства в финальном составном изображении. Вкладка Headlines позволяет редактировать текст заголовков по экранам и локалям. Она также запускает проверки уникальности между брендами: Apple требует, чтобы метаданные скриншотов отличались между приложениями одного разработчика, поэтому заголовки chat.ilia.ae не могут совпадать с заголовками EL·Hub.

Вкладка Fixtures

Фикстуры определяют демо-данные, отображаемые на скриншотах. Подробнее о системе фикстур — в следующем разделе.

Вкладка Gallery показывает все захваченные скриншоты в сетке, фильтруемой по бренду, устройству, локали и экрану. Это шаг ревью: ты просматриваешь все захваты на предмет проблем рендеринга, отсутствующего контента, обрезанного текста или специфичных для локали проблем вёрстки (арабский RTL-текст особенно требует визуальной проверки). Отсюда можно перезапустить отдельные захваты или пометить их как одобренные для передачи в pipeline композиции.

Панель синхронизации с Crowdin

И текст заголовков, и текст фикстур нужно перевести на четыре локали. Страница Screenshots включает панель синхронизации с Crowdin, которая пушит английские исходные строки в Crowdin, пуллит готовые переводы и записывает их обратно в YAML-файлы конкретных локалей. Это обеспечивает согласованность воркфлоу перевода с более широким pipeline метаданных, управляемым mobile-stores-cicd.

Build ui logs

Страница Logs Build UI с вкладочным аудит-трейлом: логи ошибок, предупреждений и полного вывода

Каждая CLI-операция и каждая сборка, запущенная из UI, пишет в общую лог-поверхность в web/.logs/. Страница Logs предоставляет три вкладки: Errors (отфильтровано по сбоям сборки, проблемам подписи, отсутствующим зависимостям), Warnings (предупреждения компилятора, уведомления о депрекации, алерты расхождения версий) и Full (полный нефильтрованный вывод). Логи имеют временные метки и теги с операцией, которая их породила.

Кнопка массовой очистки внизу удаляет старые лог-файлы, сохраняя последние запуски. Это полезно после недели итеративных сборок, когда директория логов накопила сотни мегабайт вывода xcodebuild. Лог-поверхность общая между CLI и UI — если ты запустишь build.py из терминала, вывод всё равно появится на странице Logs, и наоборот.

Демо-режим и система фикстур

Скриншоты для сторов должны показывать приложение в реалистичном состоянии: список разговоров со счётчиками непрочитанных, активный чат с пузырями сообщений, экран профиля пользователя с заполненными данными. Но генерировать это состояние вручную — создавать тестовых пользователей, отправлять сообщения, настраивать комнаты — утомительно и хрупко. Если сервер сбросится, демо-состояние пропадёт. Если нужны скриншоты на арабском, нужны арабские тестовые пользователи, отправляющие арабские сообщения в комнатах с арабскими названиями.

Система фикстур решает эту задачу, декларативно определяя демо-состояния в YAML. Файл фикстуры задаёт всё, что приложение должно отображать на каждом демо-экране: имена пользователей, аватары, названия комнат, содержимое сообщений, временные метки, счётчики непрочитанных, индикаторы набора текста, отметки о прочтении. Приложение на React Native читает эти фикстуры при запуске в демо-режиме и рендерит их так, будто это реальные данные с сервера.

Pipeline фикстур: кодогенерация из YAML в TypeScript

Поток данных фикстур работает так:

  1. fixtures.yaml содержит исходные демо-данные по брендам и локалям
  2. Скрипт кодогенерации обрабатывает YAML и генерирует fixtures.generated.ts
  3. Сгенерированный TypeScript-модуль экспортирует типизированные объекты фикстур, которые приложение импортирует в момент сборки
  4. Когда приложение запускается с флагом демо-режима, оно загружает фикстуры вместо подключения к серверу

Существует 6 демо-режимов, соответствующих 6 экранам, необходимым для скриншотов в сторах: splash (экран запуска), auth (экран логина с URL сервера), rooms (список каналов/разговоров), room (активный разговор с сообщениями), profile (профиль пользователя) и settings (экран настроек приложения). Каждый режим рендерит приложение в замороженном состоянии, которое выглядит реалистично, но полностью управляется данными фикстур.

Упрощённая запись фикстуры для экрана «rooms» выглядит так:

rooms:
  - name: "Development"
    type: channel
    unread: 3
    last_message: "Build 4.2.1 passed all tests"
    last_sender: "CI Bot"
    timestamp: "10:42 AM"
  - name: "Sarah Chen"
    type: direct
    unread: 1
    last_message: "Updated the API docs, ready for review"
    timestamp: "10:38 AM"
  - name: "Infrastructure"
    type: channel
    unread: 0
    last_message: "MongoDB backup completed successfully"
    last_sender: "Backup Service"
    timestamp: "09:15 AM"

Шаг кодогенерации трансформирует этот YAML в TypeScript с правильными аннотациями типов, так что компоненты React Native получают ровно те же структуры данных, которые они получили бы от реального API Rocket.Chat. Флаг демо-режима передаётся как launch argument (iOS) или intent extra (Android), что означает: бинарник приложения идентичен между демо и продакшн — отдельная демо-сборка не нужна.

Изоляция брендов

Фикстуры изолированы по брендам. В фикстурах chat.ilia.ae отображаются комнаты вроде «Development», «Infrastructure» и «Client Projects» с участниками, релевантными консалтинговой работе. В фикстурах EL·Hub — комнаты вроде «Dubai Marina Listings», «Agent Updates» и «Compliance» с именами членов команды по недвижимости. Названия комнат, содержимое сообщений, аватары пользователей и счётчики непрочитанных — всё разное между брендами. Это гарантирует, что скриншоты в App Store для chat.ilia.ae выглядят иначе, чем у листинга EL·Hub, что важно, потому что Apple ревьюит оба приложения и отклонит приложения, выглядящие как дубликаты.

Интеграция с Crowdin для текстов фикстур

Файлы YAML фикстур по умолчанию содержат английский исходный текст. Для русских, арабских и китайских скриншотов текст фикстур нужно перевести. Это происходит через Crowdin: английские строки фикстур пушатся как исходные строки, переводчики (или память переводов из предыдущих отправок) предоставляют версии для конкретных локалей, и система сборки пуллит переводы обратно в YAML-файлы локалей. Шаг кодогенерации затем производит отдельные модули fixtures.generated.ts для каждой локали.

Интеграция pipeline скриншотов с mobile-stores-cicd

Build UI захватывает сырые скриншоты. Сырые — значит: PNG-файл в точном разрешении устройства, показывающий приложение в определённом демо-состоянии, без рамки устройства, без текста заголовка и без фона. Эти сырые захваты — входные данные для pipeline композиции в mobile-stores-cicd.

Pipeline композиции (подробно описан в статье о mobile-stores-cicd) берёт каждый сырой скриншот, накладывает его на изображение рамки устройства (iPhone, iPad, Pixel и т.д.), добавляет локализованный заголовок над или под рамкой, рендерит результат на градиентный фон и выдаёт финальное составное изображение в точных пиксельных размерах, требуемых App Store Connect и Google Play Console.

Масштаб операции со скриншотами — нетривиальный. Каждому приложению нужны скриншоты для 5 семейств устройств (iPhone 6.9″, iPhone 6.5″, iPad 13″, Android-телефон, Android-планшет), 4 локалей (en-US, ru, ar-SA, zh-Hans) и 6 экранов (splash, auth, rooms, room, profile, settings). Это 120 скриншотов на приложение, 240 всего по обоим брендам. Build UI захватывает 120 сырых скриншотов на бренд. Mobile-stores-cicd компонует и загружает все 240 финальных изображений в соответствующие сторы.

ЭтапИнструментВходВыход
Настройка демо-состоянияBuild UI (фикстуры)fixtures.yamlfixtures.generated.ts
Захват сырых скриншотовBuild UI (Maestro / adb)Приложение в демо-режиме120 сырых PNG на бренд
Композиция с рамкамиmobile-stores-cicdСырые PNG + рамки устройств120 составных изображений на бренд
Загрузка в сторmobile-stores-cicdСоставные изображенияApp Store Connect + Google Play Console

Архитектура

Build UI построен на Streamlit, с боковой панелью Material Design-иконок для навигации. Компоновка страниц следует паттерну мультистраничного приложения Streamlit с одной важной модификацией: директория страниц называется pages_/ с подчёркиванием в конце, что отключает автоматическое обнаружение страниц Streamlit. Это даёт полный контроль над порядком и видимостью страниц в боковой навигации, вместо зависимости от алфавитной сортировки по именам файлов.

Модули общей библиотеки живут в web/lib/:

  • state.py — читает состояние репозитория (git-ветка, статус рабочего дерева, текущий бренд, строки версий) и кэширует в session state Streamlit
  • runner.py — управление подпроцессами с потоковым выводом stdout в блоки st.code, поддержкой отмены и обработкой кодов выхода
  • log_parser.py — парсит лог-файлы из web/.logs/, категоризирует записи по серьёзности (error, warning, info) и предоставляет фильтрованные представления
  • screenshots_config.py — управляет метаданными скриншотов: семейства устройств, локали, определения экранов, тексты заголовков, пути вывода
  • brand_diff.py — сравнивает конфигурацию между брендами, обнаруживает расхождения в строках версий и signing identity
  • crowdin_api.py — клиент Crowdin REST API для пуша исходных строк и пулла переводов

Лог-поверхность общая между CLI и UI. Оба пишут в одну и ту же директорию web/.logs/ в одном структурированном формате: временная метка, имя операции, уровень серьёзности и сообщение. Это означает, что можно запустить сборку из терминала и просмотреть её вывод на странице Logs позже, или запустить из UI и грепнуть лог-файлы из командной строки.

Хэндлы подпроцессов хранятся в session state Streamlit, что обеспечивает работу кнопки отмены. Когда сборка выполняется, сессия держит ссылку на объект subprocess.Popen. Кнопка отмены отправляет SIGTERM, ждёт корректного завершения и обновляет лог записью об отмене. Это важно для xcodebuild, который может занимать 10-15 минут на чистую сборку и не должен убиваться принудительно без очистки.

Структура директорий отражает эту архитектуру:

web/
├── app.py                    # Streamlit entry point, sidebar layout
├── pages_/                   # Page modules (trailing underscore disables auto-discovery)
│   ├── home.py
│   ├── status.py
│   ├── ios.py
│   ├── android.py
│   ├── version.py
│   ├── upstream.py
│   ├── screenshots.py
│   └── logs.py
├── lib/                      # Shared modules
│   ├── state.py              # Repository state reader, session cache
│   ├── runner.py             # Subprocess management, streaming, cancellation
│   ├── log_parser.py         # Log file parsing and severity classification
│   ├── screenshots_config.py # Device families, locales, screen definitions
│   ├── brand_diff.py         # Cross-brand configuration comparison
│   └── crowdin_api.py        # Crowdin REST API client
├── ssl/                      # TLS certificates (gitignored)
│   ├── fullchain.pem
│   └── privkey.pem
└── .logs/                    # Shared log surface (CLI and UI write here)

Безопасность и деплой

Build UI не имеет слоя аутентификации. Это внутренний инструмент, работающий на моей машине для разработки, доступный только с localhost или из моей сети Tailscale. Выставлять его наружу — плохая идея: он может запускать сборки, модифицировать конфигурационные файлы и выполнять произвольные команды подпроцессов.

TLS обеспечивается размещением файлов сертификатов в web/ssl/: fullchain.pem и privkey.pem. Когда эти файлы присутствуют, Streamlit отдаёт контент по HTTPS. Когда отсутствуют — откатывается на HTTP. Обычно это сертификаты Let’s Encrypt, сгенерированные через Certbot на хост-машине и залинкованные в директорию web.

Для постоянной работы Build UI может запускаться как macOS-сервис launchctl (автозапуск при логине, перезапуск при крэше) или Linux-сервис systemd. Описание сервиса указывает на точку входа Streamlit с настроенным портом, путями TLS и рабочей директорией. Все секреты (конфиги Firebase, пароли keystore, signing identity) в .gitignore и загружаются из переменных окружения или локальных конфигурационных файлов.

Присутствие в App Store и Google Play

Оба бренда доступны в обоих сторах:

ПриложениеApp StoreGoogle PlayПлатформы
chat.ilia.aeApp StoreGoogle PlayiPhone, iPad, Mac (Apple Silicon), Apple Watch
EL·HubApp StoreGoogle PlayiPhone, iPad, Mac (Apple Silicon), Apple Watch

iOS-приложение chat.ilia.ae требует iOS 15.1 или новее и поддерживает iPhone, iPad, Mac с Apple Silicon и Apple Watch. Метка конфиденциальности в App Store указывает «Data Not Collected», что правда — приложение подключается напрямую к self-hosted серверу Rocket.Chat, и сервер не передаёт данные третьим сторонам. Нет SDK аналитики, нет рекламного идентификатора, нет сервиса отчётов о сбоях, который звонит на внешний сервер.

Оба приложения локализованы на 4 локали: en-US (английский), ru (русский), ar-SA (арабский) и zh-Hans (упрощённый китайский). Метаданные стора — название приложения, подзаголовок, описание, ключевые слова, скриншоты, промо-текст — поддерживаются через pipeline метаданных mobile-stores-cicd, рендерятся из шаблонов Jinja2 с переопределениями значений по приложениям и локалям. Обновления метаданных стора проходят через CLI, а не через веб-интерфейс App Store Connect.

Арабская локаль (ar-SA) требует особого внимания из-за RTL-вёрстки (справа налево). Приложение поддерживает RTL нативно через I18nManager React Native, но данные фикстур и скриншоты требуют визуальной проверки — выравнивание текста, позиционирование иконок и направление навигации — всё переворачивается. Китайская локаль (zh-Hans) включена преимущественно для рынка ОАЭ, где значительное китаскоговорящее бизнес-сообщество. Тестирование и RTL, и CJK-типографики происходит на этапе захвата скриншотов, где я просматриваю вывод каждой локали на вкладке Gallery перед одобрением для композиции.

Почему не официальное приложение Rocket.Chat

Rocket.Chat публикует собственные мобильные приложения. Для большинства сценариев они работают нормально. Причина, по которой я поддерживаю кастомные форки, специфична для моих операционных требований:

  • Привязка к серверу: кастомные приложения по умолчанию подключаются к конкретному URL сервера. Членам команды не нужно вводить адрес сервера при первом запуске — приложение сразу открывает экран логина для нужного инстанса. Официальное приложение сначала требует выбрать или ввести URL сервера.
  • Айдентика бренда: EL·Hub должен выглядеть как продукт Estateliga, а не как обычный клиент Rocket.Chat. Кастомные иконки приложения, splash-экраны и цветовые схемы делают его похожим на собственный корпоративный инструмент.
  • Демо-режим: в официальном приложении нет демо-режима. Описанная выше система фикстур существует только в форкнутых сборках. Без неё генерация согласованных скриншотов на 4 локалях и 5 семействах устройств потребовала бы поддержки отдельного тестового сервера с предварительно настроенными демо-данными.
  • Каденция релизов: я контролирую, когда выходят обновления. Если upstream-релиз вносит регрессию, я могу его пропустить или cherry-pick’нуть конкретные фиксы. Официальное приложение обновляется по расписанию Rocket.Chat, а не по моему.

Рабочий процесс: типичный релиз

Вот как выглядит типичный цикл релиза, от мёрджа upstream до отправки в стор:

  1. Проверка upstream: открыть страницу Upstream, получить последние теги, просмотреть changelog нового релиза
  2. Мёрдж upstream: в терминале смёрджить upstream-тег в ветку форка, разрешить конфликты (обычно в Podfile.lock, package.json и нативных файлах сборки)
  3. Бамп версии: через страницу Version инкрементировать версию во всех четырёх файлах
  4. Сборка chat-ilia iOS: страница iOS, выбрать chat-ilia, отметить pod install + xcodebuild, собрать
  5. Сборка chat-ilia Android: страница Android, выбрать chat-ilia, Release AAB
  6. Переключиться на el-hub: страница iOS, выбрать el-hub, собрать iOS; страница Android, собрать Android
  7. Захват скриншотов: если визуальные изменения оправдывают новые скриншоты, через страницу Screenshots захватить сырые скриншоты для обоих брендов по всем локалям и устройствам
  8. Композиция и загрузка: передать сырые скриншоты в mobile-stores-cicd для композиции с рамками и загрузки в сторы
  9. Отправка: через mobile-stores-cicd отправить обновления метаданных, release notes и скомпилированные бинарники в App Store Connect и Google Play Console

Весь цикл занимает 2-3 часа для обоих брендов на обеих платформах, включая время компиляции. Без Build UI тот же процесс включал десятки терминальных команд, ручные правки конфигурационных файлов и документ-чеклист для отслеживания, какая комбинация бренд-платформа уже собрана. UI не ускоряет процесс по времени компиляции, но исключает категорию ошибок, когда забываешь переключить бренд перед сборкой или пропускаешь pod install после upstream-мёрджа.

Чеклист релиза в Build UI — неявный, а не явный. Каждая страница показывает свои пререквизиты: страница iOS не позволит собрать, если переключение бренда не прошло, страница Android блокирует при отсутствующих keystores, страница Version предупреждает о расхождении. Прохождение страниц по порядку — Status, Version, iOS, Android, Screenshots — естественно обеспечивает правильную последовательность. Раньше я вёл отдельный документ-чеклист; теперь сам Build UI и есть чеклист.

Во сколько это обходится

Серверная часть (инстанс Rocket.Chat, MongoDB, nginx, резервные копии) работает на существующей инфраструктуре вместе с другими сервисами. Инкрементальная стоимость пренебрежимо мала — Rocket.Chat потребляет скромные ресурсы при небольшом размере команды.

На стороне App Store расходы больше на программах для разработчиков, чем на инфраструктуре. Членство в Apple Developer Program — $99/год. Регистрация Google Play Developer — единовременный взнос $25. Никаких постоянных оплат за пользователя. Никаких оплат за сообщение. Никаких тарифов за хранение. Приложения бесплатны для скачивания и использования. Общая ежегодная стоимость эксплуатации двух брендированных мессенджеров в обоих сторах, с полной поддержкой push-уведомлений и постоянными обновлениями, составляет менее $200/год без учёта серверной инфраструктуры.

Ограничения и что я бы сделал иначе

Система фикстур хорошо работает для скриншотов, но добавляет бремя сопровождения. Каждый upstream-мёрдж нужно проверять на изменения в моделях данных, от которых зависят фикстуры. Если Rocket.Chat добавляет новое поле в компонент элемента списка комнат, фикстуры нужно обновить, иначе сгенерированный TypeScript выдаст ошибки типов.

Build UI на Streamlit означает ограничения Streamlit: нет постоянных WebSocket-соединений (каждое взаимодействие вызывает полный перезапуск страницы), ограниченная кастомизация компоновки и периодическая сложность управления состоянием при долгих подпроцессах. Дашборд на React был бы гибче, но скорость разработки Streamlit и нативная интеграция с подпроцессами Python делают его прагматичным выбором для инструмента одного оператора.

Переключение бренда — это операция перезаписи файлов, то есть она модифицирует рабочее дерево. Если ты переключишь бренд посреди незакоммиченной работы, нужно сначала сделать stash. Более изящная система использовала бы переменные окружения на этапе сборки и условную компиляцию вместо перезаписи файлов, но тулчейн React Native + CocoaPods + Gradle делает это сложнее, чем кажется. Перезапись файлов — некрасиво, но надёжно.

Часто задаваемые вопросы

Может ли кто угодно использовать chat.ilia.ae?

Текущий инстанс — для моей команды и консалтинговых клиентов. Приложения в App Store и Google Play бесплатны для скачивания, но подключаются к моему серверу — для входа нужен аккаунт. Если хочешь аналогичную систему для своей организации, я могу развернуть инстанс Rocket.Chat на твоей инфраструктуре с кастомно-брендированными приложениями, опубликованными под твоими аккаунтами разработчика.

Почему Rocket.Chat, а не Matrix, Mattermost или Zulip?

У Rocket.Chat самая зрелая кодовая база мобильного клиента (React Native, хорошо поддерживается), чистый REST API и достаточно большое сообщество, чтобы upstream-разработка стабильно продолжалась. У Matrix (Element) лучше федерация, но мобильные клиенты исторически были менее отполированными. Mattermost силён, но open-source версия имеет ограничения функционала. Модель тредов Zulip отличная, но мобильные приложения менее доработаны. Для моего кейса — два white-label приложения с кастомным брендингом и демо-режимами — React Native клиент Rocket.Chat был самой практичной основой для форка.

Есть Android-приложение?

Да. И chat.ilia.ae, и EL·Hub имеют Android-приложения в Google Play. Сборка Android использует ту же кодовую базу, ту же систему переключения брендов и тот же pipeline фикстур. Build UI управляет сборками Android наряду со сборками iOS.

Чем это отличается от WhatsApp или Telegram?

WhatsApp и Telegram — потребительские мессенджеры. Для личного общения они отличные, но для профессионального использования не подходят. Нет каналов с гранулярными правами, нет тредов, нет админ-контроля для управления пользователями и политиками хранения, нет экспорта данных, нет возможности self-hosting. Твои сообщения живут на инфраструктуре Meta или Telegram. С chat.ilia.ae сообщения остаются на сервере, который я эксплуатирую, с полным админ-контролем, полным владением данными и соответствием любой нормативной базе, применимой к разговору.

Как Build UI соотносится с mobile-stores-cicd?

Это отдельные инструменты в двухэтапном pipeline. Build UI отвечает за всё внутри репозитория React Native: переключение брендов, сборку, управление версиями, захват скриншотов и управление фикстурами. Mobile-stores-cicd отвечает за всё за пределами репозитория: взаимодействие с API App Store Connect и Google Play Console, шаблонизацию метаданных, композицию скриншотов с рамками устройств, отправку релизов и sideload-дистрибуцию. Точка передачи — сырые скриншоты и скомпилированные бинарники. Build UI их производит; mobile-stores-cicd их публикует.

Можешь развернуть это для моей команды?

Да. Деплой включает настройку сервера Rocket.Chat на твоей инфраструктуре (облако или on-premises), форк мобильного клиента, применение твоего брендинга, конфигурацию Firebase для push-уведомлений, публикацию приложений в твоих аккаунтах разработчика и опционально настройку Build UI для твоего оператора. Настройка сервера занимает день. Брендинг приложений и первоначальная подача в стор — неделю, включая время ревью App Store. Напиши с требованиями.

Система сборки open source?

Система сборки и код Build UI находятся в приватном репозитории. Мобильный клиент Rocket.Chat — open source (лицензия MIT), и мой форк сохраняет эту лицензию для базового кода. Слой брендинга, система фикстур и Build UI — проприетарный инструментарий. Доступ предоставляется по запросу для консалтинговых клиентов.

Исходный код и консалтинг

Репозиторий с Build UI, системой сборки и pipeline фикстур — приватный. Если хочешь ознакомиться с кодовой базой или оценить её для деплоя собственного self-hosted мессенджера для команды, запроси доступ здесь.

По вопросам консалтинга по self-hosted коммуникационной инфраструктуре, разработке white-label мобильных приложений или автоматизации мобильного CI/CD (включая тулкит mobile-stores-cicd, который отвечает за публикацию в сторах), свяжись со мной.

Нужна консультация?

Если вам нужна профессиональная экспертиза — запишитесь на бесплатную 15-минутную консультацию.

Оцените статью