- Implemented StatusWidget component for embedding RideBus service status. - Created widget-client.js for client-side interaction with the status widget. - Developed install-widget-integrations script to automate integration into PWA, website, and admin projects. - Introduced server-side widget rendering in widget.mjs for generating status widget HTML. - Added comprehensive tests for widget functionality and integration.
37 KiB
RideBus Status
Независимый сайт статуса RideBus в визуальном стиле ridebus-pwa и ridebus-website: Manrope, общая палитра, логотип, округлые карточки, иконки @vkontakte/icons, светлая и тёмная темы, русский и английский языки.
Запуск
Нужен Node.js 24+. SQLite встроен в Node; внешняя база для первого запуска не требуется.
npm ci
cp .env.example .env
npm run dev
Для браузерных проверок нужен Chromium: npm run browsers:install. На macOS автоматически используется установленный Google Chrome; на другом хостинге можно указать STATUS_BROWSER_EXECUTABLE_PATH. Dockerfile устанавливает Chromium и системные зависимости. Если браузер недоступен, сценарий становится непроверенным, а доступность страницы остаётся отдельным показателем.
Страница: http://localhost:5188. Все пять реальных адресов уже настроены. Проверки выполняются сервером даже без открытого браузера. История начинается с первого запуска; пропущенные дни остаются без данных.
npm run build
npm start
В production один Node-процесс обслуживает собранный интерфейс, публичный API и мониторинг. При завершении процесса очередная проверка дожидается окончания, затем закрывается база.
Сервисы и настройки
Встраиваемый виджет
Виджет показывает текущую доступность и состояние сценариев выбранных сервисов. Используются те же правила свежести и плановых работ, что и на основной странице. Зелёный статус исчезает при истечении измерений, сетевой ошибке или отсутствии ответа сайта мониторинга. История и транспортные данные для виджета не загружаются.
Публичный iframe: /embed?services=pwa,api&lang=ru&theme=auto; параметры services, lang (ru/en) и theme (light/dark/auto) необязательны. Без списка проверяются все сервисы. Публичный /api/widget?services=pwa,api возвращает компактную сводку. Демо-режим виджетом не поддерживается. Обновление — каждые 30 секунд, в скрытой вкладке запросы приостанавливаются.
В исходники трёх проектов добавлен компонент из integrations/StatusWidget.tsx: основной сайт (website,api), PWA (pwa,api), админка (admin,api). Он проверяет origin и источник сообщений iframe и показывает запасную ссылку, если мониторинг недоступен. Iframe не получает ключи API или токены приложения; запросы данных идут с credentials: omit, referrer отключён.
После выбора хостинга задайте URL сайта статуса перед сборкой клиентов:
| Проект | Переменная |
|---|---|
| ridebus-website | NEXT_PUBLIC_STATUS_URL=https://your-status-domain.example |
| ridebus-pwa | VITE_STATUS_URL=https://your-status-domain.example |
| polotsk-transit-api/frontend | VITE_STATUS_URL=https://your-status-domain.example |
Без адреса production-виджет скрыт; в development используется http://127.0.0.1:5188. Значения URL публичные и не являются секретами. После изменения необходима новая сборка. Если у хостинга задан CSP, разрешите домен мониторинга в frame-src. Виджет также можно вставить в другой сайт обычным iframe шириной 260px и высотой 44px с title, referrerpolicy="no-referrer" и sandbox allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox.
| Сервис | Проверка по умолчанию |
|---|---|
| API | https://api.ridebus.by/health: HTTP 200 и JSON status: healthy |
| Сайт | https://ridebus.by/: HTTP 200 и HTML |
| PWA | https://pwa.ridebus.by/: HTTP 200 и HTML |
| Табло | https://board.ridebus.by/: HTTP 200 и HTML |
| Админка | https://admin.ridebus.by/: HTTP 200 и HTML |
config/services.json позволяет добавлять сервисы, задавать переводы, публичные ссылки и правила проверки. Поддерживаются виды html, json, http; для JSON — expectedJson: { "path": "status", "value": "healthy" }. Для защищённого запроса укажите headerEnv: { "X-API-Key": "MY_SECRET_VARIABLE" }, а значение храните в переменной окружения.
URL можно переопределить через STATUS_API_URL, STATUS_WEBSITE_URL, STATUS_PWA_URL, STATUS_BOARD_URL, STATUS_ADMIN_URL. Значение off отключает проверки сервиса и показывает отсутствие данных. Публичная ссылка остаётся из конфигурации. Любые URL проверок и секретные заголовки остаются на сервере; публичный API возвращает только безопасную сводку. При перенаправлении на другой origin пользовательские заголовки удаляются.
Для дополнительной проверки данных API задайте RIDEBUS_API_KEY. Тогда кроме /health проверяется непустой список городов по STATUS_API_DATA_URL (по умолчанию /api/v1/cities). Без ключа проверяется только здоровье API: это не проверка подключения к БД. HTML-проверки подтверждают загрузку страницы, а не полный пользовательский сценарий, авторизацию или получение отправлений.
Пользовательские сценарии
В конфигурации для API, PWA и табло задан scenario. Их проверки выполняются отдельным циклом раз в 300 секунд и не задерживают обычные проверки доступности:
- API: с серверным
RIDEBUS_API_KEYполучить города, остановки выбранного города и ETA контрольной остановки.X-City-Idотправляется только городским запросам. - PWA: через публичный
/api/dataполучить города, остановки и отправления, затем открыть приложение с выбранной остановкой в новом браузерном контексте и дождаться отображения данных. - Табло: получить
/api/config, остановки и/api/board, затем проверить отображение в браузере. Если табло требует сопряжения, нужна cookie отдельного существующего тестового устройства вSTATUS_BOARD_TEST_COOKIE. С этой cookie монитор также получает/api/device/statusи использует город и остановку, назначенные устройству в админке. Общая/api/configсодержит настройки автономного табло и может указывать другую остановку. Отсутствие доступа, незавершённое сопряжение или отозванное устройство отмечаются как непроверенный сценарий, не как сбой табло.
Контрольный город выбирается автоматически (Полоцк при наличии), остановка — из каталога. Можно закрепить их через STATUS_TEST_CITY_ID и STATUS_API_TEST_STOP_ID, STATUS_PWA_TEST_STOP_ID, STATUS_BOARD_TEST_STOP_ID. Для сопряжённого табло город и остановка всегда берутся из настроек устройства; STATUS_BOARD_TEST_STOP_ID применяется только к автономному табло. Пустые ближайшие отправления, в том числе ночью, допустимы. Некорректный ответ, ошибки маршрутов или неполные данные проваливают проверку.
В STATUS_BOARD_TEST_COOKIE нужно значение заголовка Cookie целиком: "имя=value; другое_имя=value", без префикса Cookie:. Храните его в кавычках в .env; в чат и публичную конфигурацию cookie не добавляйте. После добавления или обновления перезапустите сервер. Истёкший тестовый сеанс снова отображается как неподключённая проверка.
Переход к проверке настроек сопряжённого устройства начинает отдельную историю сценария табло. Результаты прежней проверки общей остановки сохраняются в базе, но не считаются сбоями нового сценария; история обычной доступности сохраняется.
Браузерные контексты каждый раз новые: сохранённое расписание и service worker не маскируют проблему. Сеанс тестового табло загружается через browserContext.addCookies(). POST/PUT/PATCH/DELETE заблокированы, сопряжение новых устройств не выполняется; при использовании cookie запросы на другой origin также блокируются. Проверки браузера используют prefers-reduced-motion: reduce и включённый sandbox Chromium. PWA/табло проверяются без авторизации администратора; вход и CRUD админки не тестируются.
Интерфейс отдельно показывает доступность и результат сценария, этапы, время и причину неподключённой проверки. Подтверждённая проблема сценария создаёт инцидент. Проценты и дневные графики по-прежнему описывают обычные проверки доступности; успешный HTTP не делает сценарий автоматически рабочим.
STATUS_BROWSER_CHECKS=off оставляет проверки данных без проверки отображения и явно обозначается в подробностях. Настройки интервала, таймаута браузера и порога медленного полного сценария: STATUS_SCENARIO_INTERVAL_SECONDS, STATUS_SCENARIO_TIMEOUT_MS, STATUS_SCENARIO_SLOW_MS. Изменение .env требует перезапуска процесса.
По умолчанию: интервал 60 секунд, таймаут 5 секунд, замедление выше 1500 мс, сбой после 3 неудачных проверок подряд, восстановление после 2 успешных. Первая неудача отображается как перебои. При отсутствии свежей проверки более 3 интервалов статус становится неизвестным, в том числе в уже открытом браузере.
Проверки выполняются параллельно, новый цикл не накладывается на предыдущий. Следующий цикл запускается через заданный интервал после окончания текущего. Кнопка обновления перечитывает сводку, не позволяет посетителям запускать дополнительные запросы к сервисам. Общий статус становится полностью рабочим только при свежих успешных данных по всем сервисам.
История и инциденты
SQLite находится в data/status.sqlite (настраивается STATUS_DB_PATH). Проверки хранятся 90 дней. Графики показывают 7/30/90 календарных дней по UTC; конкретное время отображается в часовом поясе посетителя. Процент доступности — доля успешных выполненных проверок, а не SLA по длительности: периоды без мониторинга исключены. Частично покрытые дни заштрихованы. Изменение URL или правил создаёт новую историю для этого сервиса.
Подтверждённый сбой автоматически создаёт инцидент, восстановление закрывает его. Реальные инциденты не означают, что команда уже взяла проблему в работу. Ручные сообщения доступны через CLI на хостинге, без публичного административного API:
npm run incident -- list
npm run incident -- create service=api "title-ru=Проверяем задержки API" "title-en=Investigating API delays" "message-ru=Команда проверяет причину задержек." "message-en=The team is investigating response delays."
npm run incident -- resolve 1
Параметр service можно опустить для общего сообщения. CLI использует ту же БД и переменные окружения. При запуске в контейнере: docker compose exec status node server/incidents-cli.mjs list.
Влияние и хронология
Для сбоев карточка и подробности объясняют, что ограничено для пассажира: загрузка приложения, расписания или отправлений. Тексты RU/EN задаются в impact у сервиса в config/services.json. Сохранённые данные описаны как возможная помощь, с предупреждением об их актуальности. Альтернативный сервис предлагается только при его текущем рабочем статусе. Непроверенный сценарий и отсутствие тестового доступа не считаются подтверждённым сбоем.
Инцидент сохраняет начальное сообщение и все последующие обновления. Автоматическое обнаружение и восстановление подписаны как результаты мониторинга. Стадии работы команды добавляются явно через CLI:
npm run incident -- update 1 stage=investigating "message-ru=Проверяем загрузку расписания." "message-en=Investigating timetable loading."
npm run incident -- update 1 stage=identified "message-ru=Причина найдена, готовим исправление." "message-en=The cause is identified; preparing a fix."
npm run incident -- update 1 stage=monitoring "message-ru=Исправление применено, наблюдаем." "message-en=The fix is applied; monitoring."
Обновить можно ручной или автоматически созданный инцидент. Автоматический инцидент закрывается только после подтверждённого восстановления проверок; ручные стадии не изменяют результаты мониторинга. Ручной инцидент можно закрыть с пояснением:
npm run incident -- resolve 1 "message-ru=Работа восстановлена. Причина — ошибка источника данных." "message-en=Service restored. The cause was a data source error."
После закрытия любого инцидента можно добавить отдельный разбор, не меняя время восстановления:
npm run incident -- update 1 stage=postmortem "message-ru=Причина и меры для предотвращения повторения." "message-en=The cause and measures to prevent recurrence."
Публичного API записи нет. Старая история получает только события, уже зафиксированные в БД; промежуточные действия команды не придумываются.
Расширенная статистика
Блок «Работа в цифрах» на главной странице позволяет выбрать сервис. Период 7/30/90 календарных дней задаётся общим переключателем истории. В подробностях сервиса также показаны длительность сбоев и последнее восстановление.
- Время сбоев: измеренная длительность автоматически подтверждённых инцидентов доступности и пользовательских сценариев в выбранном периоде. Начало — момент подтверждения, окончание — подтверждённое восстановление. Пересечения двух видов проверок считаются один раз.
- Самый долгий инцидент: наибольшая измеренная длительность одного инцидента в выбранном периоде. Ручные сообщения не являются измеренными сбоями.
- Последнее восстановление: последнее автоматическое восстановление по текущим настройкам проверок, в том числе за пределами выбранного периода. Указано, восстановилась доступность или сценарий. При отсутствии записи показано «Не зафиксировано».
- Покрытие времени: доля периода со свежими результатами всех подключённых видов проверок. Для сценария без тестового доступа покрытие отсутствует. Непроверенный сценарий не получает фиктивные
0сбоев.
Длительность оценивается по сохранённым результатам: состояние действует до следующей проверки, максимум до границы свежести в 3 интервала. Плановые окна и более длинные пропуски исключены. Если часть инцидента не покрыта проверками, ≥ обозначает минимальную измеренную длительность. Это не SLA и не точное время начала сетевой аварии.
Для сценариев добавлена отдельная история scenario_checks. Старые последние состояния и инциденты сохраняются; отсутствовавшие измерения не восстанавливаются предположениями. Эта часть статистики начинает накапливаться после обновления. История сценариев очищается с той же политикой хранения, что и обычные проверки.
График отклика переключается между 24 часами и выбранным периодом. Ползунок показывает конкретный час или день, среднее время ответа и число проверок; пробелы остаются пробелами. Среднее и 95-й процентиль рассчитаны по успешным обычным проверкам. Изменение относительно предыдущего периода показывается только при достаточном количестве измерений и заполнении обоих окон. Для сравнения двух полных 90-дневных окон потребуется хранить не менее 180 дней (STATUS_RETENTION_DAYS=180); текущая политика по умолчанию остаётся 90 дней.
Сводка имеет внутренний кэш до 5 секунд, сбрасываемый при новых проверках и изменениях SQLite через CLI. Публичные HTTP-ответы по-прежнему Cache-Control: no-store.
RSS для посетителей
На странице «Подписаться на обновления» выберите сервисы, язык и типы событий, затем скопируйте ссылку в своё RSS-приложение. Лента включает каждое обновление инцидента, разборы, анонсы работ, начало и окончание планового окна, отмену и завершение командой. Окончание окна не означает фактического завершения работ.
/feed.xml— все сервисы и оба типа событий, RU по умолчанию./feed.xml?lang=en&services=api,pwa&types=incidents— инциденты API/PWA на английском./feed.xml?lang=ru&services=board&types=maintenance— работы табло.
Общие сообщения без конкретного сервиса включаются в любую выбранную ленту. Возвращаются последние 100 событий с постоянными GUID; повторное чтение не создаёт новое событие. Неизвестные сервисы, типы и пустой выбор отклоняются. RSS всегда содержит реальные данные; demo=1 в ленте не поддерживается. Регистрации и серверной базы подписчиков нет. Частоту получения определяет RSS-приложение посетителя.
Задайте STATUS_PUBLIC_URL после выбора домена: он используется для постоянных ссылок в RSS и Telegram. За reverse proxy это должен быть публичный HTTPS-адрес сайта статуса. XML экранируется, тексты внутри описаний также экранируются для HTML в RSS-приложениях.
Плановые работы
Работы хранятся отдельно от инцидентов. Укажите тексты RU/EN, сервисы и точное плановое окно с часовым поясом:
npm run maintenance -- schedule services=api,pwa starts=2026-10-13T02:00:00+03:00 ends=2026-10-13T02:30:00+03:00 "title-ru=Обслуживание API" "title-en=API maintenance" "message-ru=Возможны короткие перерывы в загрузке расписания." "message-en=Brief interruptions to timetable loading are expected."
npm run maintenance -- list
npm run maintenance -- finish 1
npm run maintenance -- cancel 2
Это пример команды, а не созданный анонс. Если services опущен, окно относится ко всем сервисам. finish завершает уже начавшиеся работы, cancel отменяет ещё не истёкшее окно. По окончании планового времени показывается «Плановое окно закончилось»; это не заявление команды о фактическом завершении работ.
Во время активного окна соответствующие карточки получают статус обслуживания. Проверки продолжаются, неудачные результаты остаются в истории и влияют на её процент. Ожидаемая недоступность не открывает новый инцидент и не создаёт уведомление. После окна подтверждение нового сбоя начинается заново. Уже существовавший инцидент закрывается только при настоящем восстановлении. Старая БД обновляется автоматически, без удаления истории.
Уведомления владельцу в Telegram
Отправка выключена, пока не заданы обе переменные TELEGRAM_BOT_TOKEN и TELEGRAM_CHAT_ID в .env или секретах хостинга. Бот должен иметь доступ к выбранному чату; для личного чата сначала откройте бота и нажмите Start. Добавьте STATUS_PUBLIC_URL после выбора хостинга, чтобы сообщения содержали ссылку на сайт. Язык и часовой пояс: STATUS_NOTIFY_LANG, STATUS_TIMEZONE.
npm run notify -- status
npm run notify -- test
test отправляет реальное тестовое сообщение в настроенный чат. Проверка настройки самостоятельно ничего не отправляет. Уведомления создаются только для подтверждённого сбоя и восстановления доступности/сценария. Новых подписок посетителей этот этап не добавляет.
Очередь сохраняется в SQLite и продолжает доставку после перезапуска. При сетевых ошибках, Telegram 5xx или ограничении частоты отправка повторяется с задержкой, с учётом retry_after; восстановление не обгоняет уведомление о своём сбое. После 8 попыток запись помечается как неотправленная. Неактуальные события старше суток или от прежней конфигурации проверки не отправляются. Сбой доставки не останавливает мониторинг. Сетевой разрыв после приёма сообщения Telegram может привести к повторной доставке.
Секреты, cookie, ответы транспортного API и детали ошибок Telegram не попадают в публичную сводку, логи или сообщения. Автоматические тесты Telegram используют подставной транспорт.
/?demo=1 — явно обозначенный пример с тестовыми данными. Он не пишет в рабочую БД и не выдаёт показатели за реальные. Основная страница всегда открывает реальные статусы. Настройки языка и темы сохраняются только локально в браузере; аналитики и сторонних трекеров нет.
Нижняя панель повторяет разметку, типографику, отступы и адаптацию ridebus-pwa; подпись — «Город ближе, когда всё работает». В обычной production-странице ссылка на пример скрыта. В разработке она доступна, а в явно открытой демонстрации остаётся возврат к реальным статусам. RSS и публичная JSON-сводка доступны из раздела «О мониторинге»; подписка также доступна из раздела инцидентов.
Другой хостинг
Проект не привязан к Sites или инфраструктуре RideBus. Нужен хостинг, который поддерживает постоянно работающий Node.js 24+ или Docker, Chromium с sandbox, исходящий HTTPS и постоянный диск. Задайте команду сборки npm ci && npm run build, установите Chromium (npx playwright install --with-deps chromium, если он не установлен), команду запуска npm start, платформенный PORT, HOST=0.0.0.0 и путь STATUS_DB_PATH внутри постоянного диска. HTTPS обеспечивается хостингом.
Используйте один экземпляр процесса для этой SQLite-БД. Не размещайте базу на сетевой файловой системе. Бесплатный тариф, который усыпляет процесс, оставит пробелы в мониторинге. Чисто статический или обычный serverless-хостинг потребует другого backend/планировщика и базы: фоновые проверки этой реализации там не выполняются.
Netlify
Для всего текущего проекта Netlify не является прямой заменой Node/Docker-хостингу. Functions выполняются во временной среде: текущий цикл таймеров и локальный SQLite-файл не являются постоянными сервисом и хранилищем. Scheduled Functions ограничены 30 секундами; Background Functions — 15 минутами, поэтому они также требуют отдельного запуска и архитектуры мониторинга.
Netlify подходит для интерфейса dist, если мониторинг, SQLite и Chromium работают на отдельном независимом хостинге. Потребуется направить /api/status и /feed.xml на этот backend; простая публикация dist без него не даст работающие статусы и RSS. Полный вариант на Netlify возможен после переноса хранилища, например в Netlify Database / PostgreSQL, и адаптации проверок, блокировок и очереди к отдельным функциям. Такая миграция в этот этап не входит.
Для первого полноценного запуска проще разместить весь проект на независимом хостинге с постоянным Node-процессом/контейнером и диском. Netlify-конфигурация и публикация пока не создавались.
Docker:
docker compose up -d --build
История сохраняется в volume status-data. Не удаляйте volume при обновлении. Для резервной копии используйте SQLite backup либо остановите процесс и скопируйте БД вместе с WAL/SHM-файлами. Размещайте мониторинг на независимом от сервисов RideBus хостинге.
Compose запускает контейнер от пользователя node, использует init, отдельную shared memory и официальный seccomp-профиль Playwright для sandbox Chromium. Профиль и его лицензия лежат в deploy/. На Linux-хосте должны быть разрешены непривилегированные user namespaces; запуск контейнера и браузера нужно проверить на выбранном хостинге. При недоступном sandbox сайт продолжит обычные HTTP-проверки и покажет непроверенный сценарий.
API и проверка
GET /healthz: здоровье самого приложения статуса, HTTP 200 независимо от сбоев RideBus.GET /api/status?range=30: реальная сводка без секретов,Cache-Control: no-store.GET /api/status?range=30&demo=1: явно помеченный пример. Поддерживаются только 7/30/90 дней.GET /feed.xml?lang=ru&services=api,pwa&types=incidents,maintenance: RSS 2.0 реальных событий, только GET/HEAD,Cache-Control: no-store.
npm test
npm run typecheck
npm run build
Серверные тесты проверяют реальные локальные HTTP-маршруты, подтверждение сбоев и восстановления, сохранение SQLite, отсутствие секретов в публичном ответе, таймауты и пробелы в истории. Для проверки интерфейса можно использовать реальные статусы и явно обозначенный пример /?demo=1.
Дополнительно проверяются плановые окна и миграция БД, сценарии данных, очередь и повторы Telegram. Браузерный тест использует настоящий Chromium/Chrome, локальный стенд, запрет записывающих запросов и CSS reduced motion; без установленного браузера он явно пропускается. Микроанимации интерфейса повторяют длительности и кривую движения из ridebus-pwa: появления, отклик на нажатие, стрелки ссылок и диалоги. При уменьшенном движении анимации и смещения отключены.