TRIDE.Offline — автономный режим клуба
1.Резюме
Решение. В клубе устанавливается TRIDE Edge — компактный автономный сервер (standalone Linux-сервис в Docker, мини-ПК или любой компьютер клуба). Edge держит зашифрованный снапшот данных клуба (клиенты, членства, расписание, брони, программы тренировок, идентификаторы СКУД, сотрудники и права), отвечает на запросы клиентских приложений в существующем формате API TRIDE, принимает локальные мутации в журнал операций (oplog) и, когда интернет возвращается, досылает их в облако — где они применяются штатными сервисами бэкенда, а не пишутся в базу напрямую.
Ключевой принцип — «без второго бэка». Edge не дублирует бизнес-логику облака: он кэширует read-данные и переиспользует минимальный набор правил (списание визита, ёмкость занятия, antipassback, верификация QR), вынесенных в общий пакет @tride/offline-core, который подключают и облако, и edge. Все накопленные офлайн-операции при синхронизации прогоняются через существующие модули бэкенда (booking, memberships, access_control, training_session) — с идемпотентными ключами и разрешением конфликтов.
Что получает клуб при обрыве на 2–48 часов
- СКУД продолжает пропускать: терминал всегда настроен на локальный edge (в онлайне edge прозрачно проксирует в облако, в офлайне решает сам по снапшоту членств и карт, списывает визиты по тем же правилам, пишет события проходов).
- Тренировки проводятся полностью: старт занятия с локального lite-движка, посадка участников с брони и по QR, живые данные датчиков (приём уже локальный — UDP/BLE/HTTP), экраны зоны рендерят как сегодня, результаты пишутся локально и доезжают после.
- Ресепшен работает: поиск клиента, карточка с кастомными полями, активные членства и пакеты с остатками, расписание, список записанных, ручная запись на занятие, чекины, отметки no-show.
- Клиенты в Wi-Fi клуба продолжают пользоваться приложением: расписание, свои записи, запись на занятие, QR-пропуск — через локально раздаваемый SPA.
- Не работает (осознанное ограничение): любые продажи/оплаты/фискализация, вход по SMS новых пользователей, внешние интеграции (psport/1С), глобальные соревнования, стриминг, AI — полный список в разделе 9.
Как это достигается малой кровью. Исследование кодовой базы показало, что фундамент уже готов: приложения несут весь UI-бандл локально (AppImage/Android), у Electron уже поднимаются локальные серверы и AceBase, у Android TV — SQLite-буфер статистики занятий с досылом; приём датчиков и живой рендер тренировки — полностью локальные; в execPost уже есть runtime-override адреса API (window.TRIDE_API_HOST), а конфиг клуба уже кэшируется на устройстве. Не хватает трёх вещей: локального источника данных и авторизации, локальных мутаций и локального решателя СКУД, локального движка старта занятий. Именно их закрывает TRIDE Edge.
2.Как устроен TRIDE сегодня
Факты аудита кодовой базы (frontend @ 8.0.7, backend, все пути — от корней репозиториев). Этот раздел — фундамент плана: каждый элемент архитектуры TRIDE.Offline ниже опирается на конкретные механизмы, уже существующие в коде.
2.1. Клиентские приложения и сборки
Один и тот же код SPA (Svelte 4 + Vite 4) собирается в несколько форм: Web/PWA, Electron (десктоп-консоли и ТВ-боксы), Capacitor 6 (Android/iOS, включая Android TV-флейвор tridetv). Флейворы app_versions/ (tride, rock, flowrow, cyclestar, way2fit + клубные через prepareclub) отличаются темой, иконками и конфигом — код один.
| Форма | Где живёт UI | Локальные серверы/хранилища уже есть | Обновления |
|---|---|---|---|
| Electron electron/index.cjs, ~1820 строк; builder-конфиг в package.json |
Бандл dist/ упакован в AppImage, раздаётся локальным HTTPS Express :3000 под домен-маской game.tride.ru → 127.0.0.1 | AceBase :5757 (статистика тренировок), /localvideos, /localmusic, /files, прокси TechnoGym, WS света :4444, сенсоры (ANT+/BLE/WASP/UDP), watchdog renderer'а | electron-updater, generic-провайдер dist.tride.ru, проверка раз в 15 мин |
| Capacitor / Android TV capacitor.config.ts, webDir: dist, server.url не задан |
Бандл в APK (androidScheme: https) | HTTP-сервер сенсоров :2408 (capacitor-http-sensors), UDP-приём датчиков :11111 (FitCare/ANT+) и :3111 (TrideGate), SQLite sessionsstat — локальная статистика участников с досылом, Filesystem (видео/музыка/логотипы) | Self-hosted capgo-протокол (@capgo/capacitor-updater) на dist.tride.ru; на screenMode-устройствах бандлы ставятся принудительно |
| Web / PWA public/webpush-sw.js; server/index.js + generators.js (middleware) |
Облако, но SW кэширует app-shell: /assets/* cache-first, HTML network-first с офлайн-фолбэком | Service worker tride-runtime-v1 (лимит 300), offline.html; middleware генерирует per-club index.html с инжектом window.APP_CONFIG | Деплой web-бандла на N серверов (CI, web-deploy.sh) |
screenMode — режим клубного экрана (панель у тренажёров): устройство регистрируется в коллекции devices с screenmode: true, zone_id, screenmode_type; флаг приезжает в ответе devices/register / get.device.token (автологин по hw_id) и уводит приложение на маршрут /screen. TV-сборка дополнительно получает нативный watchdog (Foreground Service + WakeLock), ScreenRecoveryService (самовозврат на /screen) и периодическую очистку WebView-кэша вне тренировки.
2.2. Как клиент общается с облаком
Единый REST-транспорт execPost (frontend/src/lib/api/api.js): POST /<module>/<action>, тело — JSON {...params, access_token}. Адрес определяется каскадом: window.TRIDE_API_HOST (runtime-override, сегодня ставится только виджетами) → import.meta.env.VITE_API_HOST (вшит на билде) → дефолт. Сетевой сбой превращается в {error:'network_error'} и каждый вызов справляется сам — общего офлайн-кэша ответов сегодня нет.
Boot-последовательность авторизованного клиента: /auth/checktoken → /cycle/getmydata (профиль, права, клубы, screenmode) → параллельно /settings/get, /users/getsettings, /cycle/listmyclubs, /devices/register; конфиг клуба — /tride/club.getconfig (needauth:false — тема, меню, settings, network_settings, access) и уже кэшируется на устройстве в clubSettings_<club_id> + синхронное зеркало clubSettingsSync_<club_id> для раннего сидинга до рендера. Токен — JWT HS256, 15 минут, sid указывает на token_sessions; вход — SMS-код.
Realtime — socket.io (msgpack) на sockets_host: регистрация сокета через devices.register, подписки на модули-каналы (cycle, tride, devices…), пинг каждые 10 с, собственный реконнект-цикл. Чат — отдельный сокет. Датчики устройства уходят в облако усреднёнными батчами cycle/multidata раз в 3 с (volatile — при обрыве кадры для облака теряются, локальный рендер продолжает).
2.3. Тренировочный контур
Цепочка занятия сегодня:
Тренер запускает через пульт (/tride/sessions.start → tridemanager). Кадры датчиков (пульс/мощность/каденс) приходят на экран локально (UDP/BLE/HTTP — см. таблицу ниже), экран рендерит сам и шлёт батчи в облако; tridemanager маршрутизирует кадры участникам, пишет cycle_activities + session_graphs, рассылает pdata зонам. Финиш → updateStats → статусы брони completed → statsmanager/pulse/бейджи. Тренировочная программа — документ workouts: профиль-таймлайн блоков {зоны мощности/каденса/пульса, интервалы, повторения} + дорожки музыки/видео/триггеров; свои (club/library), сетевые и купленные (store + entitlements); версии в workout_versions.
| Уже локально (переживает обрыв) | Только через облако (теряется при обрыве) |
|---|---|
|
• Приём датчиков: UDP :11111 (FitCare/ANT+), :3111 (TrideGate), HTTP :2408 (боксы), BLE, WASP, TechnoGym • Живой рендер тренировки на экране: sessionManager считает зоны/итоги в локальных сторах • SQLite/AceBase-буфер итогов участников с досылом (sessions.syncparticipant / getnotsynced / setsynced) • Предзагруженные медиа (offlinemedia/offlineworkouts у боксов), конфиг клуба и тема |
• Создание/старт/финиш занятия (расписание → cycle_sessions → tridemanager) • Посадка участников по QR, пульт тренера, управление экраном • Кадры multidata в облако (volatile, без ретрансмиссии) • Записи/чекины, СКУД-решения, синк с расписанием и внешними CRM |
2.4. Данные клуба (клиенты, записи, членства, СКУД)
Полный реестр — backend/app/system/db/mongo.collections.js (~560 коллекций, БД chat). Ключевые для офлайна:
| Домен | Коллекции | Что важно для офлайна |
|---|---|---|
| Клиенты | users (клиент = юзер с clubs_client[]), club_client_profiles ({club_id, uid} — статус, фото, consents), club_client_fields + club_fields_data (кастомные поля: определения и значения), client_tags | Поиск по имени/телефону на ресепшене; кастомные поля в карточке. Сводка «Клиенты 2.0» club_client_summary — производная, пересчитывается облаком |
| Расписание и брони | booking_schedule_sessions (занятия: services, zones, capacity, booking_settings, tride_sync), booking_bookings (статусы pending…no_show, participants, check_in, payment, waitlist) | «Кто записан/забронировал», локальная запись и чекин. Ёмкость и waitlist — с локальной проверкой |
| Членства и пакеты | booking_membership_products (unlimited / limited_visits / class_pack / limited_time), booking_memberships (usage_limits, remaining_visits, freeze, network_access, статусы), списание — membership.visits.js по политике visit_charge (on_booking / on_visit) | «Какие пакеты/членства действуют, сколько визитов осталось»; локальное списание при проходе/чекине, событийный пересчёт после синка |
| СКУД | booking_access_control_systems (точки), user_access_identifiers (карты/браслеты/QR, uid_norm), booking_access_events (entry/exit/denied), access_qr_nonces; настройки в clubs.access_control; вебхук /acs/:token/checkaccess — pull-модель, списка доступа на контроллер не выгружается | Решение о проходе — чистая функция от (идентификатор, членства, правила) → идеально для локального решателя на снапшоте |
| Права | club_roles, users.roles, реестр прав rbac/config/permissions.js (wildcard-матчинг), права кладутся в сессию при логине | Локальная проверка прав тех же сотрудников; PIN-вход по снапшоту |
2.5. Что сегодня переживает обрыв интернета
- уже есть UI-бандл на устройстве (Electron/Android), локальные медиа, кэш конфига клуба и темы, offline.html у PWA;
- уже есть приём датчиков и живой рендер идущего занятия на экране зоны;
- уже есть локальный буфер итогов участников (SQLite на TV / AceBase в Electron) с серверным досылом;
- уже есть единственная персистентная офлайн-очередь мутаций — health-активности (health:offline_queue), паттерн идемпотентности уже обкатан;
- нет локальной авторизации, источника read-данных, мутаций (записи/чекины), СКУД-решений, старта занятий. Это и есть объём TRIDE.Offline.
3.Задача и сценарии
Сценарий: у клуба пропадает интернет (провайдер/авария) на период от пары часов до двух суток. Клуб продолжает работать физически: клиенты приходят, тренировки идут, ресепшен продаёт… нет — обслуживает, двери должны открываться.
| Сценарий | Приоритет | Что должно работать |
|---|---|---|
| Клиент подходит к турникету | P0 | СКУД распознаёт карту/QR, проверяет членство, открывает, пишет проход, списывает визит по правилам клуба |
| Тренер проводит занятие | P0 | Занятие из расписания стартует на экране зоны с программой (своей или купленной), участники садятся с брони/по QR, датчики и визуализация работают, итоги сохраняются и доезжают после |
| Ресепшен: «запишите меня на вечер» | P0 | Поиск клиента, просмотр членств/пакетов/остатков, запись на занятие (в т.ч. за визит пакета), чекин, отмена, no-show |
| Сотрудник смотрит «кто записан» | P1 | Список записавшихся на занятие, состав, статусы, контакты — из снапшота + локальных мутаций |
| Клиент в Wi-Fi клуба открывает приложение | P1 | Расписание, свои записи/визиты, QR-пропуск; запись на занятие с локальной проверкой ёмкости |
| Интернет вернулся | P0 | Автоматический досыл oplog, разрешение конфликтов, переключение клиентов обратно на облако — без участия персонала |
Вне scope (согласовано): продажи/оплаты/фискализация офлайн; SMS-вход новых пользователей офлайн; внешние CRM-синки; глобальные межклубные функции. Продажи исключены из плана сознательно: офлайн-фискализация — отдельная нерешённая тема (известный пробел offline-чеков), а финансовые рассинхроны дороже выгоды.
4.Выбор архитектуры: где живёт офлайн-ядро
Рассмотрены четыре варианта размещения локального сервера клуба. Согласовано: вариант B — standalone Linux-сервис.
Отдельный Docker-сервис (TRIDE Edge) на мини-ПК или любом компьютере клуба. Обслуживает всех участников клубной сети: экраны, консоли, телефоны сотрудников и клиентов, СКУД-терминалы — независимо от того, есть ли в клубе Electron-консоль. Максимальная предсказуемость: единая точка с постоянным аптаймом (рекомендуется мини-ПК + UPS), простой docker-compose, изоляция от пользовательских устройств (бокс могут выключить/переставить). Плата: каждому клубу нужно железо и 30 минут установки — покрывается инструкцией и тарифной опцией.
Одно ядро, две формы доставки. Плюс — ноль железа для клубов с консолью. Минусы, определившие отказ: «мастер» на консоли зависит от того, включён ли бокс (аптайм не контролируется платформой); смешение ролей (устройство визуализации = критическая инфраструктура) усложняет поддержку и диагностику; Android TV всё равно остаётся ведомым — два класса поведения. Решение пересмотреть, если встанет вопрос о минимизации железа.
Ноль нового железа, но офлайн-возможность клуба привязана к одному пользовательскому боксу, который могут выключить на ночь (а обрыв интернета часто длиннее рабочего дня), и есть не во всех клубах. Покрытие и надёжность не соответствуют сценарию «сутки-двое».
Максимальное покрытие без железа, но Android TV непригоден как полноценный мастер: ограничения памяти/фона (Doze), нет предсказуемого аптайма, и главное — два рантайма ядра означает двойную тестовую матрицу и двойные баги. Отклонено как несоразмерно дорогое в поддержке.
Почему standalone выигрывает ещё и архитектурно
- СКУД-терминалы получают один постоянный локальный адрес вебхука (терминал настраивается один раз; больше его не трогаем при никаких обрывах).
- Один арбитр oplog на клуб — нет задачи выбора мастера и миграции состояния между устройствами.
- Раздача web-SPA клиентам в Wi-Fi естественна: edge — это и origin приложений, и их API.
- Снижение трафика через облако: экраны зоны и датчики получают локальный RTT вместо гонки кадров через облако (сегодня multidata ходит в облако даже между устройствами одной комнаты).
5.Архитектура TRIDE.Offline
5.1. Режимы работы и переключение
| Режим | Когда | Поведение |
|---|---|---|
| ONLINE норма | Интернет есть | Клиентские приложения работают с облаком напрямую (прод-путь не меняется). Edge в фоне: раз в 30–60 с тянет дельту снапшота, отдаёт heartbeat с метриками. СКУД-терминал и экраны зон — всегда через edge: edge онлайн работает прозрачным прокси в облако (см. 5.2), заодно прогревая снапшот и снижая RTT. |
| DEGRADED обрыв | Облако недоступно, edge жив | Edge за N секунд (2 подряд неудачных пробы облака) переходит в offline: начинает отвечать сам на read-API, принимать мутации в oplog, решать СКУД локально, вести занятия lite-движком. Клиентские приложения обнаруживают edge и переключаются (5.2). UI показывает индикатор «Клуб работает автономно». |
| RECOVERY возврат | Интернет вернулся | Edge: прогрев снапшота → досыл oplog батчами (применение облаком через штатные сервисы, идемпотентно) → сверка курсоров → возврат в ONLINE. Клиенты переключаются обратно автоматически. Отчёт о периоде офлайна (сколько событий/мутаций/конфликтов) — в админку и сотруднику. |
| STALE редко | Edge не синкался > grace-периода | Политики устаревания (см. 7.4): авторизация офлайн ограничена 7 сут., СКУД — 14 сут. (пропустить клиента важнее идеальной свежести), после — edge переходит в защитный read-only и требует связи. |
5.2. Ключевые механизмы на стороне клиента (минимальный след)
В клиентское приложение добавляется один маленький модуль EdgeRouter (общий для web/electron/capacitor). Всё остальное — существующий код.
- Адрес edge приезжает из облака заранее: в tride/club.getconfig → network_settings.offline = {enabled, edge_host}. EdgeRouter сохраняет его рядом с уже кэшируемым конфигом клуба — к моменту обрыва адрес уже известен.
- Переключение API: сегодня execPost уже поддерживает runtime-override через window.TRIDE_API_HOST (api.js:10–11) — EdgeRouter просто ставит его при переходе в offline и убирает при возврате. Сокеты — аналогично через переопределение хоста в socketsManager (переподключение уже встроено его реконнект-циклом).
- Проба без таймаут-ожидания: при network_error от облака 2 раза подряд EdgeRouter пробует edge_host/status с таймаутом 1.5 с; успех → offline-режим; индикатор в топбаре + баннер ограничений.
- Web-клиенты, открывшие приложение уже в офлайне: облако недоступно, SPA не загрузится оттуда — но edge сам раздаёт web-бандл (кэш последней web-сборки + сгенерированный middleware-генератором index.html с APP_CONFIG.api_host = edge_host). QR-табличка «Wi-Fi клуба» на ресепшене ведёт на https://edge.tride.local.
- Экраны зон и СКУД-терминалы — настроены на edge постоянно (см. 8.2/8.3), для них переключения не существует.
Почему «fallback на edge», а не «всегда через edge» для всех
Вариант «все клиенты всегда ходят через edge» даёт идеальную бесшовность, но делает edge единой точкой отказа для онлайнового трафика клуба и добавляет звено в самый горячий прод-путь. Компромисс: обычные приложения переключаются только при обрыве (прод-путь не трогаем), а компоненты, которым локальность нужна всегда — СКУД и экраны зон — закреплены за edge постоянно (им локальность даёт выигрыш RTT даже в онлайне).
6.Edge-нода: доставка и состав
6.1. Дистрибутив и требования
- Форма: docker-compose (tride-edge + том данных /data со снапшотом и oplog). Также поставляется bare-metal systemd-юнит для клубов без Docker.
- Железо (рекомендация): x86 мини-ПК (класс Intel N100), 8 ГБ RAM, 64 ГБ SSD, пассивное охлаждение, + USB-UPS. Потребление edge — десятки МБ RAM, диск ≤ 2 ГБ.
- Сеть: статический IP или DNS-имя в роутере клуба (edge.tride.local; edge дополнительно анонсирует себя по mDNS). HTTPS с самоподписанным сертификатом клуба (приложения TRIDE уже игнорируют self-signed на embedded-хостах — механика game.tride.ru).
- Активация: в админке TRIDE включается опция клуба «Offline» → генерируется edge_id + edge_secret (ротация в 1 клик, паттерн club_notify_providers / access_control.token) → клуб вводит их при первом запуске edge → edge делает первичную инициализацию (полный снапшот) и появляется в мониторинге.
- Self-update: edge сам обновляется из облака (тот же канал dist.tride.ru, образ/бандл с версией схемы снапшота и oplog — совместимость версий обязательна).
6.2. Внутренний состав edge
| Компонент | Назначение | Переиспользование |
|---|---|---|
| API-шлюз | Отвечает на существующий протокол POST /<module>/<action> (read-подмножество + offline-мутации) и вебхуки /acs/:token/*; socket.io с теми же событиями модулей | Формат запроса/ответа — 1:1 с диспетчером main (request.process.js) |
| Снапшот-хранилище | SQLite (WAL) с таблицами-зеркалами коллекций + JSON-колонки; шифрование тома (SQLCipher/LUKS); FTS-индекс по клиентам (имя/телефон) | Схемы документов — те же, что в Mongo (поля/статусы не изобретаются) |
| Oplog | Журнал локальных мутаций: тип операции, payload, идемпотентный ключ (uuid edge), время, автор, статус применения | Паттерн — health:offline_queue (идемпотентность, батчи, лимит попыток) уже обкатан во фронте |
| СКУД-решатель | Локальный decideAccessCore(): карта/QR → членство → зоны → долг → antipassback → вердикт; пишет локальные booking_access_events | Код выносится из access_control/services/decision.js в общий пакет — облако и edge используют одну функцию |
| Lite-движок занятий | Подмножество tridemanager для одной зоны/занятия: load программы из снапшота workouts, ready/start/pause/finish, состав, маршрутизация кадров, рассылка pdata локальным зонам; пишет activity-графики в oplog | Протокол событий — существующий (cycle/session_ready…); итоги досчитывает облако штатной обработкой |
| Web-раздача | Кэш web-бандла SPA + генерация index.html с APP_CONFIG (api_host=edge) для клиентов в Wi-Fi | Middleware-генератор frontend/server/generators.js — тот же механизм, что прод-web |
| Sync-агент | Pull дельты по курсорам, push oplog, heartbeat, self-update | Новый лёгкий сервис облака edge-sync (по паттерну микросервисов PM2) |
| @tride/offline-core | Общий пакет правил: списание визитов (visit_charge), ёмкость/waitlist, antipassback, верификация QrBlock, матчинг прав (rbac permissions), Joi-схемы брони | Импортируется из существующих модулей бэка — версия пакета = версия бэка |
7.Синхронизация с облаком
Облако — всегда источник истины. Edge может отставать, но не расходиться: все офлайн-изменения — события (event-based), которые облако применяет и на их основе пересчитывает производные состояния (остатки визитов, сводки, статусы).
7.1. Pull: снапшот данных клуба
Новый сервис облака edge-sync (или модуль main) отдаёт снапшот и дельты по курсорам. Каждая сущность — курсор по updated_at (у BaseModel поле автоуправляется), soft-delete приезжает как removed:true. Частота: раз в 30–60 с по изменённым; полный реснапшот — при инициализации и раз в 7 суток (самопроверка целостности).
| Сущность | Состав снапшота | Оценка объёма* |
|---|---|---|
| Конфиг клуба | clubs: settings, access_control (вкл. QR-ключ), timezone, зоны, network_settings | < 1 МБ |
| Клиенты | users клуба: uid, ФИО, телефоны, теги, avatar-id — поля-минимум (без документов/адресов) | 10 000 × ~0.3 КБ ≈ 3 МБ |
| Клубные профили и поля | club_client_profiles (статус, consents), определения club_client_fields + значения (поля с offline_sync:false — исключены) | 2–5 МБ |
| Членства и пакеты | booking_memberships активные/будущие (usage_limits, dates, network_access) + продукты | 3 000 × ~1.5 КБ ≈ 4.5 МБ |
| Расписание и брони | booking_schedule_sessions ±14 дней; booking_bookings ±7 дней (состав, статусы, check_in) | 1–2 МБ |
| Программы тренировок | workouts клуба/сети + купленные (entitlements) — профили, без медиа (медиа — предзагрузка на экранах, offlinemedia уже есть) | 1–5 МБ |
| СКУД | user_access_identifiers (активные), точки доступа, booking_client_accounts (балансы для debt_limit), последние 30 дней booking_access_events | 1–3 МБ |
| Сотрудники и права | users-сотрудники: роли, права (wildcard-наборы), офлайн-PIN (хэш), devices клуба (hw_id → zone/screenmode) | < 1 МБ |
| Сессии авторизованных | uid/sid/права активных сессий клуба (для офлайн-валидации) | < 0.5 МБ |
*Клуб 10 000 клиентов / 3 000 активных членств — типичный крупный. Итого снапшот 15–25 МБ, дельта за минуту — килобайты. Диск edge (с запасом на oplog и web-бандл) — < 2 ГБ.
7.2. Push: oplog мутаций
Каждая офлайн-мутация записывается в oplog с идемпотентным ключом и применяется облаком через существующие сервисы — это и есть «без второго бэка»: одна кодовая база бизнес-логики, два рантайма.
| Операция oplog | Применение в облаке (штатный путь) | Идемпотентность |
|---|---|---|
| access.event — проход/отказ | access_control/decision.js evaluateAndRecord + списание визита через membership.visits.js | ключ = event uuid; повтор игнорируется |
| membership.visit_charge — списание (чекин/выход) | decrementMembershipVisits по политике visit_charge | ключ; сервер пересчитывает остаток по событиям |
| booking.create / cancel | сервис бронирования (booking.js) с флагом источника from_offline_op | ключ; конфликт ёмкости → политика (7.3) |
| booking.checkin / noshow / status | штатные переходы статусов (statusTransitions) | ключ + проверка текущего статуса |
| client.draft_add — новый клиент офлайн (сотрудник завёл) | addOrBindClientByPhone — дедуп по телефону уже встроен | телефон + ключ |
| session.lifecycle — старт/финиш занятия, участники | training_session sync-путь: создание/закрытие cycle_sessions + ScheduleSession (механика уже есть — session_sync.js) | session_id = id ScheduleSession |
| session.results — графики/итоги участников | тот же путь, что досыл экранов сегодня: sessions.syncparticipant → cycle_activities/session_graphs → штатная обработка statsmanager/pulse/бейджи | (uid, session_id); повтор — upsert |
| acs.nonces_used — использованные QR-nonce | вставка в access_qr_nonces (анти-replay после восстановления) | сам ключ — nonce |
Порядок применения: батчи по 100 операций, строже по времени; ответ облака — пер-операционный статус (applied / conflict / rejected), edge обновляет oplog и локальное зеркало. Лимит попыток и карантин подозрительных — по паттерну synctasks (exsync).
7.3. Конфликты — политики по доменам
| Домен | Конфликт | Политика |
|---|---|---|
| СКУД / списания | Визит списан офлайн и одновременно онлайн (два клуба сети; клиент зашёл там и там) | Не конфликт в общем случае: применение — события, остаток = пересчёт от событий. Если лимит исчерпан — событие применяется с флагом over_limit, в отчёте синка; списание в минус не идёт |
| Брони (capacity) | Офлайн-записи превысили ёмкость занятия из-за параллельных онлайн-записей | Превышение допускается в рамках overbooking_percent (механизм уже в модели). Сверх — бронь принимается со статусом waitlisted + задача сотруднику («разрешить конфликт записи»); клиенту — уведомление |
| Отмена vs чекин | Клиент записан офлайн, а онлайн-сессия отменила занятие | Событие по времени: отмена занятия лечит все брони штатно; офлайн-чекин в отменённое занятие → помечается, не создаёт дубль визита |
| Новый клиент | Сотрудник завёл офлайн, клиент зарегистрировался онлайн с тем же телефоном | Штатный дедуп addOrBindClientByPhone (привязка, не дубль) |
| Редактирование данных клиента | Правки офлайн и онлайн | Офлайн-правки клиентских полей ограничены read-mostly: разрешены только клубные поля профиля; применяется принцип last-write-wins по полю с журналом (риск минимален, изменения редки) |
7.4. Свежесть и границы доверия снапшота
- offline_grace: авторизация по локальным сессиям — до 7 суток с последнего успешного синка; дальше только PIN сотрудника +QR-вход экранов (hw_id).
- СКУД: работает до 14 суток (пропуск клиента важнее; риск — устаревшие членства: продолжают пускать по действовавшему на момент снапшота; все проходы журналируются и сверяются после).
- Мутации брони — до 3 суток, дальше edge предлагает только просмотр (защита от расползания конфликтов).
- Все границы — настройки клуба (профили «строгий/сбалансированный»), по умолчанию сбалансированный.
8.Офлайн-домены: как работает каждый
8.1. Авторизация адаптированная
SMS-код офлайн невозможен, JWT живёт 15 минут. Решение — три пути входа в offline:
- Уже авторизованные устройства (основной путь): edge валидирует подпись JWT (HS256 — секрет общий, edge доверенный компонент платформы), сверяет sid со снапшотом активных сессий клуба, не проверяет exp в пределах offline_grace. Права/клуб — из снапшота сессии. Работает для сотрудников, клиентов и экранов.
- Экраны/боксы: штатный автологин по hw_id (club/get.device.token) — edge отдаёт тот же формат ответа из снапшота devices клуба.
- Сотрудники без живой сессии: локальный PIN-вход (выбор профиля из снапшота + PIN; хэш ротируется через облако). Права — те же wildcard-наборы (rbac/permissions.js из общего пакета).
Новые клиентские регистрации офлайн — нет (осознанно). Сотрудник может завести черновик клиента (client.draft_add) для записи — дедуп при синке.
8.2. СКУД и проходы P0
- Терминал всегда настроен на edge (один раз при установке). Онлайн: edge — прозрачный прокси вебхука в облако (решение и журнал остаются штатными). Офлайн: решает локальный decideAccessCore из @tride/offline-core — тот же код, что в облаке, потому поведение и тексты причин отказа идентичны.
- QR: static (номер карты) работает тривиально; dynamic QrBlock верифицируется пер-клубным ключом из снапшота (ключ выдается edge при инициализации по TLS, хранится зашифрованным). TTL в офлайне расширяется политикой (×4), иначе клиентские QR протухали бы; одноразовость — локальный nonce-реестр, использованные nonces доезжают в облако (анти-replay).
- Списания визитов — локально по visit_charge (on_entry/on_exit — настройка уже в клубе); остаток локального зеркала уменьшается мгновенно, облако при синке подтверждает событиями. Внешние СКУД-владельцы (psport/1С клубы): локально не списываем, только пускаем и журналируем (уважаем их external_ownership).
- Антиpassback — по локальному зеркалу событий (30 дней в снапшоте + локальные).
8.3. Тренировки P0
- Программы: снапшот workouts покрывает свои, сетевые и купленные (entitlements) — тренер в offline запускает всё, что доступно клубу сегодня. Медиа: экраны играют предзагруженное (механизм offlinemedia/offlineworkouts уже существует), облачное видео/музыка офлайн недоступны — экраны переключаются на локальный плейлист/визуализацию.
- Экраны зон ходят на edge постоянно (сокеты и REST): в онлайне edge проксирует в облако (RTT датчиков зоны падает — сегодня батчи гоняются через облако), в офлайне обслуживает локально. Протокол событий не меняется: экран подписан на те же session_ready/pdata/….
- Lite-движок edge: занятие создаётся из снапшота расписания (ScheduleSession ±14 дней) или вручную тренером; посадка участников — из брони и по QR (ShowQR на экране → приложение/скан сотрудника → локальная проверка членства/брони); старт/пауза/финиш — с пульта тренера через edge-сокет.
- Данные: датчики продолжают приходить на экраны локально (уже так); экраны шлют батчи multidata на edge вместо облака — edge маршрутизирует участникам, рассылает pdata локальным зонам, копит графики. Чего edge не делает: сложные сводные итоги участников — их досчитывает облако при синке (штатная обработка ready_for_processing → statsmanager/pulse), экран же рендерит live сам, как сегодня.
- Результаты: граффики и activity-записи уходят в oplog (session.results) и применяются тем же путём, каким сегодня досылаются локальные SQLite-буферы экранов (sessions.syncparticipant) — то есть статистика, стрики, бейджи, пульс-сессии клиентов доедут штатно.
- Стриминг тренера, 3D-миры, глобальные соревнования — офлайн нет (п. 9).
8.4. Записи, чекины, расписание P0
- Просмотр расписания (±14 дней) и составов занятий — из снапшота; «кто записан/в листе ожидания/пришёл» — снапшот + локальные мутации (edge мержит по времени).
- Запись на занятие (сотрудник или клиент в Wi-Fi): локальная проверка ёмкости/waitlist/окна записи (booking_settings уже в модели), валидация по Joi-схеме из общего пакета, локальный документ брони, oplog booking.create. Списание визита пакета при записи (policy on_booking) — локально, событием.
- Чекины: ручной (сотрудник), по QR, автоматом от СКУД (check_in_method: access_control); no-show отмечается вручную/по правилам — всё в oplog.
- Отмена/перенос — аналогично, с политикой конфликтов из 7.3.
8.5. Данные клиентов на ресепшене P0 чтение ограниченная запись
- Поиск (имя/телефон) — FTS по снапшоту, карточка: контакт, клубное фото, статус, кастомные поля, теги; активные членства/пакеты с остатками и сроками, заморозки; предстоящие и прошедшие записи (снапшот + локальные); история проходов (30 дней + локальные).
- Запись офлайн ограничена: клубные поля профиля, комментарий, теги, статус; смена телефона/документов — онлайн-only. Персональные данные минимизированы в снапшоте (см. 10).
8.6. Клиенты в Wi-Fi клуба P1
- Установленные приложения (мобайл/Electron): входили до обрыва — работают через EdgeRouter (валидация локальной сессии по 8.1): расписание, свои записи/визиты, QR-пропуск (обновляется локально по club-ключу), запись на занятие.
- Web/PWA и новые устройства: открывают https://edge.tride.local — edge раздаёт SPA (кэш web-сборки + APP_CONFIG с api_host=edge). Вход — только если сессия жива (SMS офлайн нет); просмотр расписания и вход по QR-пропуску — для авторизованных.
- Чат, поддержка, AI-ассистент, пуши — офлайн скрыты из UI (баннер ограничений).
9.Чего офлайн не будет (осознанные ограничения)
Исключено по согласованию
- Продажи, оплаты, фискализация, возвраты, пополнения баланса/бонусов — онлайн-only (офлайн-фискализация — отдельная тема)
- Заморозки/разморозки членств, смена продуктов
Технологически недоступно без интернета
- SMS-код и новые регистрации; OAuth/мессенджер-вход
- Внешние интеграции (psport, 1С, Bitrix — их источник истины вне нас)
- Стриминг занятия, облачное видео/музыка (кроме предзагруженных), 3D-миры
- Глобальные соревнования и межклубные лидерборды (локальный экран — только локальный состав)
- Пуши FCM/APNs, AI-ассистент, экспорты/отчёты, интеграции носимых (Garmin/Strava)
10.Безопасность и 152-ФЗ
- Периметр: edge размещается в служебном VLAN/Wi-Fi (гостевая сеть — только до web-раздачи и API read-only клиента); доступ к мутациям — по правам ролей; edge_secret ротируется из админки.
- Секреты на площадке: пер-клубный QR-ключ, PIN-хэши, снапшот ПД. Меры: шифрование тома снапшота, ключи только в памяти процесса, аудит-лог локальных операций (кто/что/когда — уезжает в облако при синке), не хранить дольше grace-политики, авто-вайп при ротации edge_secret.
- Минимизация ПД в снапшоте (152-ФЗ): поля-минимум клиентов, документы/адреса не синкаются; кастомные поля исключаются флагом offline_sync:false; журнал доступа к карточкам в офлайне. Согласия клиентов не требуются сверх действующих (обработка в интересах клуба-оператора, средства на площадке клуба).
- Сценарий угона edge: секреты ротируются из облака (мгновенная инвалидация), снапшот зашифрован, мутации с украденного edge не примутся после ротации; heartbeat подписан.
- Аудит и наблюдаемость: heartbeat-метрики (возраст снапшота, глубина oplog, режим), алерты в админку: «клуб офлайн > 2 ч», «edge не выходил на связь > 24 ч», «конфликты после синка > N».
11.План внедрения
Оценки трудоёмкости — ориентировочные (1 разработчик-владелец + подключение второго на пиках Ф2/Ф3), недели чистой разработки без буферов на ревью/релизы.
- edge-runtime (Node, API-шлюз-скелет, SQLite, конфиг, self-update), docker-compose + systemd, генерация edge_id/secret в админке (паттерн club_notify_providers)
- Сервис edge-sync: манифест снапшота, дельта по курсорам, heartbeat; первичная инициализация снапшота
- Выделение пакета @tride/offline-core (первый состав: схемы, rbac-матчинг, QrBlock-верификация) — бэк импортирует его же
- Read-API на edge (конфиг клуба, клиенты/профили/поля, членства, расписание/брони, программы), офлайн-авторизация (JWT+снапшот сессий, PIN, device-токен)
- EdgeRouter во фронте: адрес из network_settings.offline, проба/переключение, индикатор режима; web-раздача SPA с edge (переиспользование generators.js)
- decideAccessCore в offline-core, /acs/:token/* на edge, режим «терминал всегда на edge» (прокси в онлайне)
- QR офлайн (static + QrBlock с расширенным TTL, локальные nonces), локальные списания визитов, oplog: access.event / visit_charge / nonces_used + применение облаком
- Отчёт после синка: сверка проходов/списаний
- Lite-движок (создание из расписания/вручную, посадка с брони и QR, start/pause/finish), перевод экранов зон на edge (в онлайне — прокси), локальные комнаты zone_id
- oplog: session.lifecycle / session.results → штатная обработка облаком; интеграция с существующим SQLite-досылом экранов
- booking.create/cancel/checkin/noshow + client.draft_add в oplog, локальная ёмкость/waitlist, конфликты и их UI («разрешить конфликт записи»)
- Клиентские фичи в Wi-Fi: запись на занятие, QR-пропуск локального обновления
- Chaos-тесты на e2e-стенде (обрывы/частичная связность/двойной синк), мониторинг-алерты, runbook установки для клубов (мини-ПК + UPS + DNS), инструкция терминала
- Пилот на 2–3 клубах (включая клуб с psport-СКУД — режим «не списываем локально»), затем включение опцией по тарифу
Почему такой порядок фаз
Ф0–Ф1 дают ценность уже на read-only (ресепшен видит клиентов/расписание), Ф2 ставит СКУД — самый критичный P0 с простым контрактом (один вебхук), Ф3 — самый сложный домен (realtime), Ф4 достраивает мутации на уже обкатанном oplog. Каждая фаза независимо релизуема и откатываема (edge не участвует в прод-пути, пока опция не включена).
12.Риски и открытые вопросы
| Риск | Вероятность / цена | Митигция |
|---|---|---|
| Железо edge — сама точка отказа (умер мини-ПК) | Низкая / высокая | Рекомендация по железу + UPS; мониторинг «edge молчит» → дежурный; в будущем — опция standby-ноды (не в MVP) |
| Рассинхрон списаний визитов (сеть клубов) | Средняя / средняя | Event-based пересчёт остатков облаком; отчёт сверки после синка; флаг over_limit вместо минуса |
| Конфликты ёмкости при массовом офлайн-бронировании | Средняя / низкая | overbooking_percent + понижение в waitlist + задача сотруднику; верхняя граница офлайн-мутаций брони (3 сут.) |
| Устаревший снапшот ≠ реальность (клиент оплатил онлайн во время обрыва провайдера клуба — «интернет есть, но не у клуба») | Средняя / низкая | Онлайн-клиент после синка увидит корректные данные; проходы за период сверяются; grace-политики |
| Безопасность секретов на площадке | Низкая / высокая | Шифрование тома, ротация secret из облака, VLAN, аудит-лог, авто-вайп (раздел 10) |
| Версионная несовместимость edge ↔ облако (oplog-схема) | Средняя / средняя | schema_version в oplog; облако применяет на 1 версию назад, edge обновляется перед облаком (каналы dist) |
| «Прозрачный прокси» СКУД/экранов через edge в онлайне добавляет задержку/отказ | Низкая / средняя | Прокси stateless с байпасом в облако при перезапуске edge; таймауты меньше терминальных; флаг отказа прокси → терминал/экран временно прямо в облако (конфиг с двумя адресами) |
| Персонал не настроит DNS/терминал | Средняя / низкая | Runbook + удалённая ассистенция установки; допускается работа по статическому IP; mDNS как фолбэк |
Открытые вопросы (решаются на старте Ф0): модель тарификации опции; конкретная модель мини-ПК для рекомендации/поставки; политика TTL QR для офлайна (×4 — рабочая гипотеза, согласовать с безопасностью); глубина снапшота истории проходов (30 дней — гипотеза); нужен ли локальный поиск клиентов по частичному номеру телефона с маскированием (privacy-баланс); взаимодействие с клубами, где СКУД уже на psport/1С (только журналирование — подтвердить на пилоте).
13.Приложение: справочник по кодовой базе
| Тема | Ключевые файлы / имена |
|---|---|
| API-транспорт фронта | frontend/src/lib/api/api.js (execPost, TRIDE_API_HOST override), src/lib/appConfig.js (APP_CONFIG → env → default) |
| Boot/кэш конфига клуба | src/lib/tride.app.js (Tride.init/initAuthed), src/lib/clubThemeBootstrap.js, src/lib/managers/ClubManager.js (clubSettings_<id>), src/lib/socketsManager.ts |
| Экраны/screenMode | app/apps/tridetv · VITE_ISTV · /screen, src/lib/tride/services/ScreenRecoveryService.js, UpdateService.js (OTA), SQLite src/lib/managers/databasemanager/sessionsstat.js |
| Диспетчер бэка | backend/app/apps/main/system/system.transports/process/request.process.js (runModuleAction), api/index.js (реестр), формат {…params, access_token} |
| Движок занятий | app/apps/tridemanager/ (Session.js, ParticipantActivity.js), app/modules/schedule/services/session_sync.js, broadcaster комнаты zone_id:<id> |
| Брони/членства | app/modules/booking/ (booking.scheme.js, statusTransitions, waitlist), app/modules/memberships/ (client_membership.js, visit_charge, visits.js) |
| СКУД | app/modules/access_control/: api.access.webhook.js (/acs/:token), services/decision.js (decideAccess), services/qr.js + qrblock.js, user_access_identifiers, access_qr_nonces |
| Клиенты клуба | app/modules/clients/: clients.crud.js, client_add.js (дедуп по телефону), client_fields.crud.js + values.js, club_client_profiles |
| Права | app/modules/rbac/config/permissions.js, services/perms.js (wildcard), права в сессии при логине |
| Синк-паттерны для переиспользования | health:offline_queue (идемпотентные батчи), exsync/synctasks (очередь задач, карантин), sessions.syncparticipant/getnotsynced (досыл результатов), club_notify_providers (пер-клубные секреты) |
| Коллекции-минимум снапшота | clubs, users, club_client_profiles, club_client_fields, club_fields_data, booking_membership_products, booking_memberships, booking_schedule_sessions, booking_bookings, workouts, user_access_identifiers, booking_access_control_systems, booking_access_events, booking_client_accounts, devices |