15 KiB
RideBus Board
Табло остановки: React + TypeScript + Vite, сервер Node.js / Express. Фон строго #000000, локальный шрифт Manrope, контрастная белая типографика, настраиваемый акцент, иконки @vkontakte/icons и официальный SVG-логотип из RideBus PWA. По умолчанию — Полоцк, Швейная фабрика, stopId=1. Реальный API подтверждает эту остановку и 22 маршрута.
Запуск
Требуется Node.js 24+ (проверено с Node 24.14.1).
npm install
cp .env.example .env
# Добавьте RIDEBUS_API_KEY в .env.
npm run build
npm start
Откройте http://127.0.0.1:5180. Для разработки — npm run dev. В этом workspace ключ уже подключён локально из существующей конфигурации RideBus; .env исключён из Git и Docker. При отсутствии ключа первая загрузка включает явно обозначенный деморежим. Демонстрацию также можно открыть по /?demo=1; она никогда не подменяет реальные данные при сбое API.
Возможности
- Главный экран целиком отдан рейсам: ближайший крупно и следующие 2–6 рейсов. Строки рассчитываются по фактической доступной высоте, нижняя панель занимает отдельное место. Даже 6 следующих отправлений помещаются на 1280×720 и 390×667. На очень низких экранах список сменяется равными страницами каждые 7 секунд с явным счётчиком; рейсы не обрезаются.
- В шапке — название и направление конкретной площадки из API, часы и дата в
Europe/Minsk. Поле «основная» не выдаётся за направление; если направления нет, оно не придумывается. Город, подпись «Остановка», «RideBus API» и «Все направления» убраны из шапки. - Информационный экран: маршруты и статус полного кеша, погода, QR-код и предупреждения. Часы и ближайший рейс остаются видимыми. В вертикальной компоновке показывается одна карточка; при следующих посещениях информационного экрана карточки сменяются. Их также можно выбрать кнопками «Маршруты», «Погода», «RideBus».
- Цикл 25 секунд / 8 секунд, настраиваемый. До ближайшего рейса ≤2 минут главный экран удерживается; можно отключить в настройках. Отмены и объезды исключают рекламную сцену из цикла.
- Опциональная рекламная сцена с текстом и длительностью; по умолчанию выключена, помечена как предпросмотр. Загрузка медиа и кампании в MVP не входят.
- Настройки остановки, маршрута, числа строк, интервалов и деморежима сохраняются на устройстве. Новая остановка загружает свои данные и свой кеш.
- Полный экран, пауза, ручная смена сцен. Клавиши:
S— настройки;F— полный экран;→— следующий экран;пробел— пауза.Escзакрывает настройки. - Кнопки управления и курсор скрываются после 5 секунд без взаимодействия. Движение мыши, касание или клавиатура возвращают кнопки; открытые настройки и работа с клавиатурным фокусом не скрываются. Место панели сохраняется, поэтому список рейсов не скачет.
- Изменившиеся цифры часов, времени отправления, минут ожидания и номеров маршрутов прокручиваются вертикально. Неизменившиеся позиции остаются на месте; увеличение идёт вверх, уменьшение — вниз. При
prefers-reduced-motionдвижение выключено. - В настройках есть 6 контрастных схем: лайм, янтарь, голубой, мята, лаванда и белый. Акценты и официальный логотип меняются вместе, фон остаётся чёрным. Все цвета имеют контраст с чёрным не меньше 7:1. Схема сохраняется на устройстве.
- Длинные номера, включая «16тк», автоматически вписываются с отступами; текст не касается рамок.
Данные и работа без связи
Сервер запрашивает GET /api/v1/stops, /stops/:id, /eta/stop/:id, /alerts/active, передавая X-City-Id и серверный X-API-Key. Интерфейс опрашивает сервер каждые 20 секунд; одновременные запросы объединяются, ответ кешируется на 12 секунд. Ключ, ошибки и сырые ответы upstream не отправляются в браузер. Прокси разрешает только явно заданные GET-операции: /api/config, /api/stops, /api/board, /api/timetable, /api/weather.
Текущий ETA endpoint вычисляет время из расписания и исторических задержек, а не текущей GPS-позиции. Табло показывает время с пометкой «По расписанию»; историческая задержка не выдаётся за прогноз. Используются абсолютные дата и время, поэтому часы не запускают истёкшие рейсы повторно.
Полный кеш выбранной остановки загружается независимо через /api/timetable: все варианты /routes/:id/schedules, остановки маршрутов с временными смещениями и /holidays/year/:year. Сохраняются будние, субботние, воскресные и праздничные варианты, сроки действия и ограничения отдельных рейсов. Праздник без своего графика использует воскресный, как в RideBus API. Учитываются укороченные и обратные рейсы; прибытие после полуночи сохраняет настоящую дату и обслуживающий день. Обновление каждые 30 минут, ограниченная параллельная загрузка, одновременные запросы объединяются. Кеш публикуется целиком: сбой отдельного маршрута или календаря не заменяет предыдущую полную копию неполными данными.
При наличии полного кеша табло рассчитывает все прибытия сегодня и завтра локально. Поэтому оно продолжает выбирать рейсы после полуночи без подключения; завтрашние времена явно помечены «завтра». Кеш хранится в IndexedDB и резервно в localStorage отдельно по городу и площадке. Срок использования без обновления ограничен 7 сутками; по истечении этого срока табло ожидает свежую загрузку. Переводы рабочих дней могут отражаться только в календаре и расписании, которые предоставляет API. Демоданные не сохраняются вместо реального расписания. Если полный кеш ещё не получен, используется прежний снимок до трёх ближайших рейсов каждого маршрута, только для его исходного дня и не старше суток.
При сбое появляется пометка сохранённых данных. Истёкшие рейсы удаляются. Если сохранённые рейсы закончились, табло честно сообщает об этом и продолжает переподключение. Предупреждения фильтруются по городу, маршрутам, срокам действия; сохраняются полный текст и детали отмены. Полный кеш расписания не подтверждает актуальность предупреждений — без свежего ответа API это явно указано.
QR-код создаётся локально, без стороннего сервиса генерации, и открывает https://pwa.ridebus.by/?city=1&stop=1 для текущей остановки (параметры меняются вместе с площадкой). Адрес PWA можно переопределить серверной переменной BOARD_PWA_URL. QR использует чёрные модули, светлый фон текущей схемы и свободное поле шириной 4 модуля.
Погода запрашивается независимо через /api/weather у Open-Meteo по публичным координатам остановки из API. В шапке — температура и состояние; на информационном экране — ощущаемая температура, ветер в м/с и три следующих часа. Процент осадков — максимальная почасовая вероятность в этих трёх часах. Это модельные погодные данные. Обновление раз в 10 минут; сохранённая погода помечается и показывается не дольше часа, затем скрывается. При отсутствии координат или сбое погоды транспорт продолжает работать. Данные сохраняются по городу и остановке; RideBus-ключ не передаётся поставщику погоды. Указаны источник и CC BY 4.0.
Бесплатный Open-Meteo подходит для прототипа; условия сервиса требуют коммерческую подписку для приложения с рекламой. Сервер уже поддерживает OPEN_METEO_API_KEY и автоматически выбирает коммерческий endpoint, ключ остаётся на сервере. Панель оператора планируется отдельно в polotsk-transit-api.
Production-сборка устанавливает Service Worker, сохраняющий оболочку, шрифты и иконки для перезагрузки без сети. Требуются HTTPS или localhost и хотя бы одна успешная загрузка; в режиме разработки Service Worker выключен. API-ответы не кешируются Service Worker. В приватном режиме браузера постоянное хранилище может быть недоступно.
Проверки
npm test
npm run build
npm run probe
Тесты проверяют календарное время, устаревшие снимки, идентичность города/остановки, источники ETA, многомаршрутные предупреждения, сбои отдельных источников, объединение запросов и HTTP-контракт. probe — проверка реального API без вывода ключа.
Развёртывание
docker compose up --build -d
Контейнер доступен на 127.0.0.1:5180. Для удалённого плеера поставьте HTTPS reverse proxy перед этим портом. API-ключ передаётся только через environment; не включайте его в URL или frontend-переменные VITE_*. /healthz проверяет работу процесса, а не доступность RideBus API.
На Linux-плеере откройте страницу Chromium в kiosk mode и настройте автозапуск графической сессии. Пример службы — deploy/ridebus-kiosk.service; пути Chromium и переменные дисплея зависят от ОС. Restart=always восстанавливает процесс браузера. Docker восстанавливает сервер после перезагрузки. Настройку и испытание на физическом плеере нужно выполнить отдельно: служба не обнаруживает зависший renderer, а удалённый мониторинг устройств в MVP отсутствует.
Изменения кода выкладывайте целой сборкой; Service Worker загружает новую оболочку в фоне, активируя обновление при следующем закрытии/перезапуске табло.