Maintenance Mode
Система технического обслуживания для контролируемого отключения приложения.
Позволяет админам мгновенно заблокировать доступ к API, показать пользователям информативный оверлей и при этом сохранить возможность тестирования через bypass-список.
1. Summary
Goal: Безопасное отключение приложения для обновлений, миграций БД, исправления критических багов — без потери данных и с понятным UX для пользователей.
User Value: Вместо непонятных ошибок пользователь видит экран "Технические работы" с опциональным сообщением от администратора.
2. Business Logic
States
Maintenance mode имеет три состояния:
| Состояние | isActive | scheduledAt | Поведение |
|---|---|---|---|
| Idle | false | null | Приложение работает нормально |
| Scheduled | false | datetime | Показывается предупреждающий баннер с обратным отсчётом |
| Active | true | null | Все /api/* запросы блокируются (503), пользователи видят оверлей |
Core Mechanics
1. Мгновенная активация
Админ нажимает "Включить" → isActive = true → middleware блокирует все /api/* запросы → пользователи видят оверлей.
2. Запланированное обслуживание
Админ выбирает время → scheduledAt = datetime → фронтенд показывает баннер "Плановое обслуживание через N мин" → при наступлении времени lazy auto-activation переводит в Active.
Нет cron-задачи. Когда любой запрос вызывает getState() и видит что scheduledAt <= now,
сервис автоматически переключает isActive = true. Это избавляет от отдельного планировщика.
3. Деактивация
Админ нажимает "Отключить" → isActive = false, все поля обнуляются → middleware перестаёт блокировать → оверлей исчезает через polling (10 сек).
4. Bypass-список
Telegram ID в переменной окружения MAINTENANCE_BYPASS_TELEGRAM_IDS (через запятую) могут обходить блокировку для тестирования во время maintenance.
Middleware проверяет bypass по заголовку X-Telegram-Init-Data. Запросы без этого заголовка
(raw fetch без initData) не могут быть идентифицированы как bypass — они будут заблокированы.
Поэтому инфраструктурные endpoints исключены из middleware целиком (см. Architecture).
Frontend UI
- Overlay (Active)
- Banner (Scheduled)
Полноэкранный блокирующий оверлей — рендерится через createPortal на document.body.
- Показывается когда
isActive === true && bypassed === false - Блокирует скролл (
useScrollLock) - Отображает кастомное сообщение или дефолтное "Ведутся технические работы"
- Z-index:
Z.MAINTENANCE(выше всех элементов приложения)
Не-блокирующий предупреждающий баннер вверху экрана.
- Показывается когда
scheduledAt !== null && isActive === false - Обратный отсчёт: "Плановое обслуживание через 2ч 15мин"
- Можно закрыть (dismissed на текущую сессию)
- Автообновление каждые 30 сек
Admin UI
- Indicator
- Settings Page
Status indicator в хедере админ-панели — зелёный/жёлтый/красный бейдж.
- Быстрое включение/отключение одной кнопкой
- Редактирование сообщения inline
- Ссылка на расширенные настройки (
/settings?tab=maintenance)
Полная страница настроек (/settings?tab=maintenance).
- Активация/деактивация
- Планирование на конкретное время
- Редактирование сообщения (пустое поле = сброс к дефолтному)
- Отмена запланированного обслуживания
- Статус и временные метки
Polling Strategy
| Состояние | refetchInterval | Зачем |
|---|---|---|
| Active | 10 сек | Быстро убрать оверлей при деактивации |
| Scheduled | 30 сек | Обновлять обратный отсчёт |
| Idle | false (не поллить) | Экономия трафика |
Frontend Integration (LoadingOrchestrator)
Early System Check — проверка maintenance выполняется первым шагом (STEP 0) в LoadingOrchestrator.initialize(), до загрузки Bootstrap.
Зачем: Предотвращает бесполезные запросы к БД (~8 запросов Bootstrap) во время технических работ, экономит ~100-200ms загрузки.
Как работает:
LoadingOrchestratorустанавливаетloadingState = 'checking_maintenance'- Выполняется запрос к
/api/maintenance/status(без auth) - Если
isActive === true && bypassed === false→ показываетсяMaintenanceOverlay, инициализация останавливается - Если
isActive === falseилиbypassed === true→ продолжается STEP 0.5 (season check)
Graceful Degradation:
try {
const status = await checkMaintenanceStatus();
if (status.isActive && !status.bypassed) {
// Блокируем дальнейшую загрузку
return;
}
} catch (error) {
// При ошибке проверки НЕ блокируем пользователя
console.warn('Maintenance check failed, continuing');
// Продолжаем к следующему шагу
}
Если проверка maintenance упала с ошибкой (нет интернета, 500 от backend), пользователь не блокируется и может продолжить работу с приложением. Это предотвращает ситуацию, когда баг в проверке maintenance блокирует всех пользователей.
Файлы:
frontend/src/components/LoadingOrchestrator.tsx— функцияcheckMaintenanceStatus(), вызов вinitialize()frontend/src/types/loading.types.ts—checking_maintenanceloading state
Edge Cases
| Ситуация | Поведение |
|---|---|
| Юзер активен при включении maintenance | Следующий API запрос получит 503 → оверлей появится |
| Юзер пассивен (не делает запросов) | Не узнает до следующего действия (polling отключен в idle) |
| Bypass-админ при активном maintenance | bypassed: true в статусе → оверлей скрыт, API работает |
| Пустое сообщение от админа | message = null → frontend показывает дефолтное "Ведутся технические работы" |
| Запланированное время наступило | Lazy auto-activation при следующем getState() вызове |
| LoadingOrchestrator при активном maintenance | Early check (STEP 0) блокирует Bootstrap → экономия ~8 DB запросов |
| Bootstrap при активном maintenance | Bootstrap hydration содержит maintenance state без bypassed — поле приходит только из /api/maintenance/status |
3. ADR (Architectural Decisions)
Singleton Prisma model
Проблема: Нужно хранить одно глобальное состояние maintenance.
Решение: Prisma модель MaintenanceMode с id = "singleton" и upsert паттерном.
Альтернативы (отклонены):
- GlobalSettings (ещё одна таблица) — overengineering, у maintenance своя логика (scheduled, activatedBy)
- Redis/env переменные — не персистентны, теряются при рестарте
Module-scope cache с 10s TTL
Проблема: getState() вызывается на каждый /api/* запрос через middleware.
Решение: In-memory cache с 10s TTL. Один DB-запрос на 10 секунд вместо тысяч.
Последствия: После activate/deactivate cache инвалидируется явно (invalidateCache()), задержка для пользователей — до 10 секунд.
Infrastructure endpoints excluded from middleware
Проблема: Onboarding flow (/api/telegram/*) использует raw fetch без X-Telegram-Init-Data заголовка. Maintenance middleware блокирует эти запросы, bypass-админы видят экран активации вместо приложения.
Решение: Исключить инфраструктурные prefix'ы из middleware:
/api/maintenance/*— polling статуса/api/telegram/*— onboarding (bot check, session create/activate)
Почему не добавлять header: Это бы размазало фикс по множеству frontend-файлов и было бы хрупким — каждый новый raw fetch мог бы забыть header.
Maintenance middleware применяется ТОЛЬКО к /api/* маршрутам.
/admin/*, /rust/*, /health, /metrics, /onboarding/* — НЕ блокируются.
503 interceptor в apiRequest
Проблема: Как показать оверлей мгновенно, не дожидаясь polling?
Решение: apiRequest в api.service.ts перехватывает 503 с кодом MAINTENANCE_MODE, мгновенно обновляет React Query cache ['maintenance'] и бросает MaintenanceError.
Последствия: Оверлей появляется мгновенно при первом неудачном запросе, без ожидания polling-цикла.
4. Architecture
Request Flow
Key Components
| Компонент | Путь | Описание |
|---|---|---|
| Service | backend/src/domains/maintenance/services/maintenance.service.ts | CRUD + cache + lazy auto-activation |
| Middleware | backend/src/domains/maintenance/middleware/maintenance.middleware.ts | onRequest hook, bypass check |
| Status Route | backend/src/domains/maintenance/routes/maintenance-status.routes.ts | Public endpoint + bypass flag |
| Admin Controller | backend/src/domains/maintenance/controllers/admin-maintenance.controller.ts | Admin API handler |
| Admin Routes | backend/src/domains/maintenance/routes/admin-maintenance.routes.ts | Admin endpoints registration |
| Schemas | backend/src/domains/maintenance/schemas/maintenance.schemas.ts | Fastify JSON schemas |
| Types | backend/src/domains/maintenance/types/index.ts | MaintenanceState, MaintenanceCacheEntry |
| Overlay | frontend/src/components/MaintenanceOverlay.tsx | Full-screen blocking overlay (portal) |
| Banner | frontend/src/components/MaintenanceBanner.tsx | Scheduled countdown banner |
| Hook | frontend/src/hooks/useMaintenance.ts | TanStack Query + adaptive polling |
| Indicator | admin/src/components/layout/MaintenanceIndicator.tsx | Admin header indicator |
| Settings | admin/src/components/settings/MaintenanceSettings.tsx | Full admin settings page |
Middleware Scope
/api/* → BLOCKED during maintenance
/api/maintenance/* → EXCLUDED (infrastructure)
/api/telegram/* → EXCLUDED (onboarding)
/admin/* → NOT affected
/rust/* → NOT affected
/health, /metrics → NOT affected
/onboarding/* → NOT affected (not under /api/)
5. Database Schema
Models
| Модель | Таблица | Описание |
|---|---|---|
| MaintenanceMode | maintenance_mode | Singleton — глобальное состояние maintenance |
Fields
| Поле | Тип | Описание |
|---|---|---|
id | String | Всегда "singleton" |
isActive | Boolean | Активен ли maintenance mode |
message | String? | Кастомное сообщение (null = дефолтное) |
scheduledAt | DateTime? | Запланированное время активации |
activatedAt | DateTime? | Когда был активирован |
activatedBy | String? | Telegram ID админа, активировавшего |
updatedAt | DateTime | Auto-updated |
6. API Endpoints
- Public API
- Admin API
| Метод | Эндпоинт | Описание | Auth |
|---|---|---|---|
| GET | /api/maintenance/status | Текущее состояние + bypassed flag | Нет (public) |
Response:
{
"success": true,
"data": {
"isActive": true,
"message": "Обновление базы данных",
"scheduledAt": null,
"activatedAt": "2025-01-15T10:00:00Z",
"bypassed": false
}
}
Все endpoints требуют Bearer Auth (admin token).
| Метод | Эндпоинт | Описание |
|---|---|---|
| GET | /admin/maintenance/status | Текущее состояние |
| POST | /admin/maintenance/activate | Мгновенная активация |
| POST | /admin/maintenance/deactivate | Деактивация |
| POST | /admin/maintenance/schedule | Запланировать на время |
| PATCH | /admin/maintenance/message | Обновить сообщение |
| POST | /admin/maintenance/cancel-schedule | Отменить запланированное |
POST /admin/maintenance/activate:
{ "message": "Обновление до версии 2.0" }
POST /admin/maintenance/schedule:
{
"scheduledAt": "2025-01-20T03:00:00Z",
"message": "Плановое обслуживание"
}
PATCH /admin/maintenance/message:
{ "message": "Новое сообщение" }
Пустая строка "" сбрасывает к дефолтному сообщению.
7. Configuration
Environment Variables
| Переменная | Описание | Пример |
|---|---|---|
MAINTENANCE_BYPASS_TELEGRAM_IDS | Comma-separated Telegram IDs для bypass | 123456789,987654321 |
Через запятую, пробелы вокруг запятых допустимы: 123, 456, 789.
8. Related
- User Management — admin access control
- Bootstrap — bootstrap hydration includes maintenance state