Files
ridebus-board/README.md
Zakhar Timoshenko e3074555a7 feat: add service window component and related functionality
- 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.
2026-10-09 23:55:47 +03:00

79 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 загружает новую оболочку в фоне, активируя обновление при следующем закрытии/перезапуске табло.