← Все разделы

TRIDE.Offline — автономный режим клуба

План системы, позволяющей клубу полностью работать при потере интернета — от пары часов до двух суток: проводить тренировки по своим и купленным программам, сажать участников, вести записи и чекины, смотреть членства и пакеты клиентов, пропускать клиентов через СКУД — и бесшовно синхронизироваться с облаком после восстановления связи.
Согласовано: standalone Linux-сервис Сотрудники + клиенты в Wi-Fi Продажи офлайн исключены Без второго бэкенда

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. Тренировочный контур

Цепочка занятия сегодня:

Расписание booking_schedule_sessions→ Создание cycle_sessions + участники из броней→ tridemanager (in-memory движок, :7071): Session.load → ready→ broadcaster → комната zone_id:<id>→ Экран зоны: session_ready → /tride/sessions.get → sessionManager (локальные сторы)

Тренер запускает через пульт (/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. Что сегодня переживает обрыв интернета

3.Задача и сценарии

Сценарий: у клуба пропадает интернет (провайдер/авария) на период от пары часов до двух суток. Клуб продолжает работать физически: клиенты приходят, тренировки идут, ресепшен продаёт… нет — обслуживает, двери должны открываться.

СценарийПриоритетЧто должно работать
Клиент подходит к турникетуP0СКУД распознаёт карту/QR, проверяет членство, открывает, пишет проход, списывает визит по правилам клуба
Тренер проводит занятиеP0Занятие из расписания стартует на экране зоны с программой (своей или купленной), участники садятся с брони/по QR, датчики и визуализация работают, итоги сохраняются и доезжают после
Ресепшен: «запишите меня на вечер»P0Поиск клиента, просмотр членств/пакетов/остатков, запись на занятие (в т.ч. за визит пакета), чекин, отмена, no-show
Сотрудник смотрит «кто записан»P1Список записавшихся на занятие, состав, статусы, контакты — из снапшота + локальных мутаций
Клиент в Wi-Fi клуба открывает приложениеP1Расписание, свои записи/визиты, QR-пропуск; запись на занятие с локальной проверкой ёмкости
Интернет вернулсяP0Автоматический досыл oplog, разрешение конфликтов, переключение клиентов обратно на облако — без участия персонала

Вне scope (согласовано): продажи/оплаты/фискализация офлайн; SMS-вход новых пользователей офлайн; внешние CRM-синки; глобальные межклубные функции. Продажи исключены из плана сознательно: офлайн-фискализация — отдельная нерешённая тема (известный пробел offline-чеков), а финансовые рассинхроны дороже выгоды.

4.Выбор архитектуры: где живёт офлайн-ядро

Рассмотрены четыре варианта размещения локального сервера клуба. Согласовано: вариант B — standalone Linux-сервис.

✅
B. Standalone Linux-сервис (выбрано)согласовано
Отдельный Docker-сервис (TRIDE Edge) на мини-ПК или любом компьютере клуба. Обслуживает всех участников клубной сети: экраны, консоли, телефоны сотрудников и клиентов, СКУД-терминалы — независимо от того, есть ли в клубе Electron-консоль. Максимальная предсказуемость: единая точка с постоянным аптаймом (рекомендуется мини-ПК + UPS), простой docker-compose, изоляция от пользовательских устройств (бокс могут выключить/переставить). Плата: каждому клубу нужно железо и 30 минут установки — покрывается инструкцией и тарифной опцией.
⚪
A. Гибрид: edge-core в двух дистрибутивах (standalone + внутри Electron)
Одно ядро, две формы доставки. Плюс — ноль железа для клубов с консолью. Минусы, определившие отказ: «мастер» на консоли зависит от того, включён ли бокс (аптайм не контролируется платформой); смешение ролей (устройство визуализации = критическая инфраструктура) усложняет поддержку и диагностику; Android TV всё равно остаётся ведомым — два класса поведения. Решение пересмотреть, если встанет вопрос о минимизации железа.
⚪
C. Только внутри Electron-сборок
Ноль нового железа, но офлайн-возможность клуба привязана к одному пользовательскому боксу, который могут выключить на ночь (а обрыв интернета часто длиннее рабочего дня), и есть не во всех клубах. Покрытие и надёжность не соответствуют сценарию «сутки-двое».
⚪
D. Electron + Android TV (оба могут быть мастером)
Максимальное покрытие без железа, но Android TV непригоден как полноценный мастер: ограничения памяти/фона (Doze), нет предсказуемого аптайма, и главное — два рантайма ядра означает двойную тестовую матрицу и двойные баги. Отклонено как несоразмерно дорогое в поддержке.

Почему standalone выигрывает ещё и архитектурно

  • СКУД-терминалы получают один постоянный локальный адрес вебхука (терминал настраивается один раз; больше его не трогаем при никаких обрывах).
  • Один арбитр oplog на клуб — нет задачи выбора мастера и миграции состояния между устройствами.
  • Раздача web-SPA клиентам в Wi-Fi естественна: edge — это и origin приложений, и их API.
  • Снижение трафика через облако: экраны зоны и датчики получают локальный RTT вместо гонки кадров через облако (сегодня multidata ходит в облако даже между устройствами одной комнаты).

5.Архитектура TRIDE.Offline

ОБЛАКО TRIDE api.tride.ru · MongoDB «chat» · PM2 main (REST + socket.io) штатные модули: booking · memberships access_control · training_session tridemanager · pulse · broadcaster микросервисы edge-sync (новый сервис) дельта-снапшот по курсорам приём oplog → штатные сервисы админка: опция «Offline» выдача edge_id/secret · мониторинг ИНТЕРНЕТ ⚡ пока есть связь — синк каждые 30–60 с КЛУБНАЯ СЕТЬ (Wi-Fi / LAN) TRIDE EDGE (мини-ПК · Docker) API-шлюз /module/action · /acs/:token Снапшот (SQLite+шифр.) клиенты · членства · расписание Oplog (мутации) идемпотентные ключи · батчи СКУД-решатель @tride/offline-core Lite-движок занятий ready/start/finish · зоны Web-раздача SPA edge.tride.local · APP_CONFIG socket.io зоны · локальные комнаты zone_id · heartbeat-наблюдаемость Экраны зон / TV-боксы датчики UDP/BLE/HTTP · рендер Сотрудники (мобайл/десктоп) записи · чекины · клиенты Клиенты в Wi-Fi SPA с edge · QR-пропуск СКУД-терминал вебхук всегда на edge → облако Тренажёры / пульсометры локальный приём без облака все стрелки — локальный трафик, работает при обрыве интернета
Рис. 1 — Общая архитектура: облако остаётся источником истины, edge — локальный сервер клуба с снапшотом, oplog и локальными сервисами

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). Всё остальное — существующий код.

Почему «fallback на edge», а не «всегда через edge» для всех

Вариант «все клиенты всегда ходят через edge» даёт идеальную бесшовность, но делает edge единой точкой отказа для онлайнового трафика клуба и добавляет звено в самый горячий прод-путь. Компромисс: обычные приложения переключаются только при обрыве (прод-путь не трогаем), а компоненты, которым локальность нужна всегда — СКУД и экраны зон — закреплены за edge постоянно (им локальность даёт выигрыш RTT даже в онлайне).

6.Edge-нода: доставка и состав

6.1. Дистрибутив и требования

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-FiMiddleware-генератор 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_events1–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. Свежесть и границы доверия снапшота

8.Офлайн-домены: как работает каждый

8.1. Авторизация адаптированная

SMS-код офлайн невозможен, JWT живёт 15 минут. Решение — три пути входа в offline:

  1. Уже авторизованные устройства (основной путь): edge валидирует подпись JWT (HS256 — секрет общий, edge доверенный компонент платформы), сверяет sid со снапшотом активных сессий клуба, не проверяет exp в пределах offline_grace. Права/клуб — из снапшота сессии. Работает для сотрудников, клиентов и экранов.
  2. Экраны/боксы: штатный автологин по hw_id (club/get.device.token) — edge отдаёт тот же формат ответа из снапшота devices клуба.
  3. Сотрудники без живой сессии: локальный PIN-вход (выбор профиля из снапшота + PIN; хэш ротируется через облако). Права — те же wildcard-наборы (rbac/permissions.js из общего пакета).

Новые клиентские регистрации офлайн — нет (осознанно). Сотрудник может завести черновик клиента (client.draft_add) для записи — дедуп при синке.

8.2. СКУД и проходы P0

Терминал → edge/acs/:token/checkaccess→ идентификация: карта (uid_norm) / QR (static или QrBlock)→ активное членство (клуб → сеть → пул)→ зоны / долг / antipassback / дедуп 3с→ вердикт терминалу {grant_access, text}→ событие в oplog + локальное списание визита

8.3. Тренировки P0

8.4. Записи, чекины, расписание P0

8.5. Данные клиентов на ресепшене P0 чтение ограниченная запись

8.6. Клиенты в Wi-Fi клуба P1

9.Чего офлайн не будет (осознанные ограничения)

Исключено по согласованию

  • Продажи, оплаты, фискализация, возвраты, пополнения баланса/бонусов — онлайн-only (офлайн-фискализация — отдельная тема)
  • Заморозки/разморозки членств, смена продуктов

Технологически недоступно без интернета

  • SMS-код и новые регистрации; OAuth/мессенджер-вход
  • Внешние интеграции (psport, 1С, Bitrix — их источник истины вне нас)
  • Стриминг занятия, облачное видео/музыка (кроме предзагруженных), 3D-миры
  • Глобальные соревнования и межклубные лидерборды (локальный экран — только локальный состав)
  • Пуши FCM/APNs, AI-ассистент, экспорты/отчёты, интеграции носимых (Garmin/Strava)

10.Безопасность и 152-ФЗ

11.План внедрения

Оценки трудоёмкости — ориентировочные (1 разработчик-владелец + подключение второго на пиках Ф2/Ф3), недели чистой разработки без буферов на ревью/релизы.

Ф0
Каркас: edge-runtime + edge-sync + активация клуба ~3–4 нед
  • edge-runtime (Node, API-шлюз-скелет, SQLite, конфиг, self-update), docker-compose + systemd, генерация edge_id/secret в админке (паттерн club_notify_providers)
  • Сервис edge-sync: манифест снапшота, дельта по курсорам, heartbeat; первичная инициализация снапшота
  • Выделение пакета @tride/offline-core (первый состав: схемы, rbac-матчинг, QrBlock-верификация) — бэк импортирует его же
Критерий выхода: снапшот клуба лежит на edge, /status и метрики видны в админке, self-update работает.
Ф1
Read-only офлайн + авторизация ~3 нед
  • Read-API на edge (конфиг клуба, клиенты/профили/поля, членства, расписание/брони, программы), офлайн-авторизация (JWT+снапшот сессий, PIN, device-токен)
  • EdgeRouter во фронте: адрес из network_settings.offline, проба/переключение, индикатор режима; web-раздача SPA с edge (переиспользование generators.js)
Критерий выхода: сотрудники и клиенты в Wi-Fi видят данные клуба без интернета (результаты e2e: обрыв канала на тестовом стенде).
Ф2
СКУД локально ~3 нед
  • decideAccessCore в offline-core, /acs/:token/* на edge, режим «терминал всегда на edge» (прокси в онлайне)
  • QR офлайн (static + QrBlock с расширенным TTL, локальные nonces), локальные списания визитов, oplog: access.event / visit_charge / nonces_used + применение облаком
  • Отчёт после синка: сверка проходов/списаний
Критерий выхода: на стенде турникет открывает/отказывает офлайн по картам и QR, все события и списания доезжают идемпотентно, повторный синк — без дублей.
Ф3
Тренировки локально ~4 нед
  • Lite-движок (создание из расписания/вручную, посадка с брони и QR, start/pause/finish), перевод экранов зон на edge (в онлайне — прокси), локальные комнаты zone_id
  • oplog: session.lifecycle / session.results → штатная обработка облаком; интеграция с существующим SQLite-досылом экранов
Критерий выхода: полное занятие (создание → посадка 8+ участников → старт → датчики → финиш) в офлайне; статистика/пульс/бейджи клиентов корректно доезжают.
Ф4
Мутации брони + клиенты в Wi-Fi ~3 нед
  • booking.create/cancel/checkin/noshow + client.draft_add в oplog, локальная ёмкость/waitlist, конфликты и их UI («разрешить конфликт записи»)
  • Клиентские фичи в Wi-Fi: запись на занятие, QR-пропуск локального обновления
Критерий выхода: запись/чекин офлайн всеми ролями; стресс-сценарий «двое суток офлайна, 300 проходов + 6 занятий + 100 записей» — синк без потерь.
Ф5
Эксплуатация и rollout ~2 нед + пилот
  • 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
Экраны/screenModeapp/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