- Introduced ServiceWindow component to display the last departure today and the first tomorrow. - Implemented cancellation notice parsing to handle route cancellations effectively. - Enhanced departure display logic to include cancellation alerts and status updates. - Created information blocks for displaying service information, weather, and QR codes. - Added tests for departure features, information handling, and service window functionality. - Updated fixtures for testing various scenarios including cancellations and long wait times.
79 lines
20 KiB
Markdown
79 lines
20 KiB
Markdown
# RideBus Board
|
||
|
||
Табло остановки: React + TypeScript + Vite, сервер Node.js / Express. Тёмная тема с фоном `#000000` и светлая с `#FFFFFF`, локальный шрифт Manrope, контрастная типографика, отдельные акцентные палитры, иконки `@vkontakte/icons` и официальный SVG-логотип из RideBus PWA. По умолчанию — **Полоцк, Швейная фабрика, stopId=1**. Реальный API подтверждает эту остановку и 22 маршрута.
|
||
|
||
## Запуск
|
||
|
||
Требуется Node.js 24+ (проверено с Node 24.14.1).
|
||
|
||
```sh
|
||
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 рейсов. Строки рассчитываются по фактической доступной высоте, нижняя панель занимает отдельное место. Если все строки не помещаются с читаемым размером текста, список сменяется страницами каждые 7 секунд с явным счётчиком. На 879×593 и 390×667 шесть рейсов показаны страницами по три; на 855×723 четыре помещаются одновременно. В узкой компоновке время отправления находится под названием и не пересекается с ожиданием.
|
||
- В шапке — название и направление конкретной площадки из API, часы и дата в `Europe/Minsk`. Поле «основная» не выдаётся за направление; если направления нет, оно не придумывается. Город, подпись «Остановка», «RideBus API» и «Все направления» убраны из шапки.
|
||
- Информационный экран: первый и последний рейс, погода, QR-код и предупреждения. Карточка движения показывает последний рейс сегодня и первый завтра с номером и направлением по полному расписанию, с учётом фильтра маршрута и прибытий после полуночи. Без полного расписания время не подставляется из короткого списка ближайших рейсов. Часы и ближайший рейс остаются видимыми. В вертикальной и короткой компоновке карточки сменяются при следующих посещениях экрана; их также можно выбрать кнопками «Движение», «Погода», «RideBus».
|
||
- В настройках → «Экраны» → «Блоки информации» каждый из трёх блоков можно включить или отключить. Оставшиеся карточки занимают доступную ширину, отключённые пропускаются в компактном цикле. При отключении всех блоков информационный экран исключается из обычного цикла; активные предупреждения о движении всё равно показываются. Выбор сохраняется в браузере; «Сбросить цикл» включает все блоки.
|
||
- Блок маршрутов показывает только номер и название. На широком экране список занимает 2–3 колонки, в узкой карточке — одну. Вместимость определяется доступной высотой; остальные маршруты сменяются страницами каждые 6,5 секунды с плавным переходом названий и чисел. Пауза останавливает смену страниц. Тип транспорта, повторные времена, наклонённые дорожки, фоновые рисунки и технический статус кеша убраны.
|
||
- Уведомления об отменах, объездах, задержках и другие сообщения оформлены простой карточкой с иконкой, номерами маршрутов и сроком действия. Исходные заголовки, тексты и детали отмен сохраняются. Статус предупреждений отображается только при их недоступности.
|
||
- Цикл 25 секунд / 8 секунд, настраиваемый. До ближайшего рейса ≤2 минут главный экран удерживается; можно отключить в настройках. Отмены и объезды исключают рекламную сцену из цикла.
|
||
- Опциональная рекламная сцена с текстом и длительностью; по умолчанию выключена, помечена как предпросмотр. Загрузка медиа и кампании в MVP не входят.
|
||
- Настройки остановки, маршрута, числа строк, интервалов и деморежима сохраняются на устройстве. Новая остановка загружает свои данные и свой кеш.
|
||
- Полный экран, пауза, ручная смена сцен. Клавиши: `S` — настройки; `F` — полный экран; `→` — следующий экран; `пробел` — пауза. В настройках `Esc` завершает изменение поля, затем закрывает меню без сохранения.
|
||
- Настройки разделены на четыре раздела: «Остановка», «Оформление», «Экраны», «Реклама». Разделы, закрытие и «Сохранить» всегда видны. Для браузеров с курсором от пульта (например, Яндекс Браузер на Mi Box) доступны постоянные кнопки «Выше / Ниже»: наведите курсор и нажмите `OK`. Они прокручивают содержимое выбранного раздела без колеса мыши и клавиатурных событий; у границы кнопка отключается. Черновик сохраняется при переходах между разделами.
|
||
- Если браузер передаёт клавиши страницы, стрелки переводят фокус между полями и прокручивают содержимое раздела. `OK / Enter` активирует кнопку, переключатель или выбор цвета; для списка и числа включает изменение значения стрелками. Повторный `OK / Enter` возвращает к навигации. Отключённые и скрытые поля пропускаются, выбранный элемент подсвечен. `Home / End` достигают крайних кнопок, `PageUp / PageDown` прокручивают раздел. `Tab` возвращает обычное клавиатурное управление; мышь и касание сохраняют стандартное поведение.
|
||
- Кнопки управления и курсор скрываются после 5 секунд без взаимодействия. Движение мыши, касание или клавиатура возвращают кнопки; открытые настройки и работа с клавиатурным фокусом не скрываются. Высота панели сохраняется, а освободившуюся ширину занимает подпись «Обновлено» справа.
|
||
- Изменившиеся цифры часов, времени отправления, ожидания и номеров маршрутов прокручиваются вертикально. Названия рейсов при смене страниц плавно сдвигаются тем же движением. Неизменившиеся позиции чисел остаются на месте; увеличение идёт вверх, уменьшение — вниз. При `prefers-reduced-motion` движение выключено.
|
||
- Для чёрного фона доступны лайм, янтарь, голубой, мята, лаванда и белый; для белого — синий, зелёный, кармин, фиолетовый, бирюзовый и графит. Акцент и логотип меняются вместе. Контраст каждой палитры с её фоном не меньше 7:1. Тема и выбранные цвета сохраняются отдельно.
|
||
- Длинные номера, включая «16тк», автоматически вписываются с отступами. Центрирование и нижнее выравнивание чисел рассчитаны по границам глифов шрифта. Ожидание от часа отображается как «2 ч 1 мин»; единицы увеличены и выровнены снизу. Завтрашние отправления имеют одну подпись времени с пометкой «завтра».
|
||
|
||
## Данные и работа без связи
|
||
|
||
Сервер запрашивает `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, одна инструкция и ссылка. Размер кода рассчитывается по свободному месту. QR использует скруглённые чёрные модули, белый фон и свободное поле шириной 4 модуля. Декодирование нескольких ссылок и QR на снимках страницы проверено автоматически.
|
||
|
||
Погода запрашивается независимо через `/api/weather` у [Open-Meteo](https://open-meteo.com/en/docs) по публичным координатам остановки из API. В шапке — температура и состояние; на информационном экране — текущая температура, состояние и прогноз температуры с крупными иконками на три следующих часа. Это модельные погодные данные. Обновление раз в 10 минут; сохранённая погода помечается и показывается не дольше часа, затем скрывается. При отсутствии координат или сбое погоды транспорт продолжает работать. Данные сохраняются по городу и остановке; RideBus-ключ не передаётся поставщику погоды. Указаны источник и CC BY 4.0.
|
||
|
||
Бесплатный Open-Meteo подходит для прототипа; [условия сервиса](https://open-meteo.com/en/terms) требуют коммерческую подписку для приложения с рекламой. Сервер уже поддерживает `OPEN_METEO_API_KEY` и автоматически выбирает коммерческий endpoint, ключ остаётся на сервере. Панель оператора планируется отдельно в `polotsk-transit-api`.
|
||
|
||
Production-сборка устанавливает Service Worker, сохраняющий оболочку, шрифты и иконки для перезагрузки без сети. Требуются **HTTPS или localhost** и хотя бы одна успешная загрузка; в режиме разработки Service Worker выключен. API-ответы не кешируются Service Worker. В приватном режиме браузера постоянное хранилище может быть недоступно.
|
||
|
||
## Проверки
|
||
|
||
```sh
|
||
npm test
|
||
npm run build
|
||
npm run probe
|
||
```
|
||
|
||
Тесты проверяют календарное время, устаревшие снимки, идентичность города/остановки, источники ETA, многомаршрутные предупреждения, сбои отдельных источников, объединение запросов и HTTP-контракт. `probe` — проверка реального API без вывода ключа.
|
||
|
||
## Развёртывание
|
||
|
||
```sh
|
||
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 загружает новую оболочку в фоне, активируя обновление при следующем закрытии/перезапуске табло.
|