247 lines
20 KiB
Markdown
247 lines
20 KiB
Markdown
# RideBus PWA
|
||
|
||
## Возможности
|
||
|
||
- Города из RideBus API, поиск остановок без повторяющихся названий и выбор направления чипами в карточке отправлений.
|
||
- Ближайшие отправления и полное расписание остановки на выбранную дату,
|
||
переключение направлений и фильтр по маршруту.
|
||
- Полное расписание в сетке по часам без скрытых рейсов; переход к ближайшему
|
||
времени и открытие конкретного рейса нажатием на отправление.
|
||
- «Мои поездки»: город, остановка, направление и маршрут в одной карточке.
|
||
Можно сохранить «Домой», «На работу» или своё название, переименовать и удалить.
|
||
На мобильном несколько поездок листаются каруселью; на десктопе карусель
|
||
включается от трёх поездок. Доступны свайпы, кнопки и управление с клавиатуры.
|
||
Частично видимые карточки плавно затухают у края. На первом и последнем
|
||
положении затухание остаётся только со стороны скрытых поездок.
|
||
«Открыть поездку» выбирает нужный город/направление/маршрут и прокручивает страницу
|
||
к карточке отправлений. При уменьшении движения переход происходит без анимации.
|
||
- Экран рейса с последовательностью остановок и временем на каждой из них,
|
||
с учётом укороченных/обратных рейсов и перехода через полночь.
|
||
- Кнопка «Поделиться отправлением» у ближайших отправлений и на экране рейса.
|
||
Ссылка сохраняет город, остановку, маршрут, дату и идентификатор конкретного рейса.
|
||
Используется системное меню отправки, затем копирование ссылки, если меню
|
||
недоступно. При недоступном буфере обмена можно скопировать ссылку вручную.
|
||
Отмена системного меню не копирует ссылку и не показывает ошибку.
|
||
- Активные сообщения об отменах и изменениях рядом с затронутыми маршрутами.
|
||
Полный текст сохраняется, сообщения нескольких маршрутов не дублируются,
|
||
а закончившиеся сообщения исчезают даже без сети.
|
||
- Кнопка «Остановки рядом»: запрос местоположения только после нажатия,
|
||
остановки в радиусе 1 км с расстояниями и группировкой одинаковых названий.
|
||
Отказ в разрешении не мешает обычному поиску. Координаты и результаты поиска
|
||
рядом не сохраняются в IndexedDB/localStorage и серверном кеше приложения.
|
||
Обычный список и поиск рядом используют общие строки, состояния и скроллбары.
|
||
- Ожидание в часах и минутах: «через 2 ч 1 мин».
|
||
- Избранные остановки с тремя ближайшими отправлениями прямо в карточках,
|
||
сохранением в браузере и переходом к расписанию.
|
||
- Короткие микроанимации карточек, кнопок, фильтров, закладки и счётчика избранного.
|
||
Эффекты отключаются при системной настройке уменьшения движения.
|
||
- Просмотр ранее загруженных данных без интернета. Кеш разделён по городам,
|
||
остановкам и режиму работы; время сохранения видно в интерфейсе.
|
||
- Установка PWA, локальные шрифты, иконки, офлайн-оболочка и предложение обновления.
|
||
- Отдельный демонстрационный режим `/?demo=1` с явно помеченными примерами.
|
||
Кнопка «Посмотреть пример» есть только при `npm run dev`; в релизной сборке
|
||
и Docker её нет, включая экран ошибки загрузки. Прямая ссылка на демонстрацию
|
||
сохранена для ручных проверок и тестов. Она не подменяет реальные данные при ошибке API.
|
||
|
||
В режиме «Ближайшие» API ETA возвращает ограниченное количество отправлений
|
||
для каждого маршрута. Историческая задержка исключается: показаны времена по
|
||
расписанию, а не обещание реального прибытия. В режиме «На дату» сервер получает
|
||
полное расписание маршрутов, учитывает праздничный тип дня, ограничения рейсов
|
||
по дням недели, начальную/конечную остановку укороченных рейсов и время пути.
|
||
В выбранную календарную дату включаются и рейсы предыдущего дня, достигающие
|
||
остановки после полуночи. Частичные ошибки обозначаются в интерфейсе.
|
||
|
||
Просмотренное полное расписание сохраняется отдельно для каждой даты и доступно
|
||
офлайн, включая прошедшие рейсы. Ближайшие отправления и карточки избранного
|
||
удаляют прошедшие времена из показа. Это не офлайн-база всех городов и дат:
|
||
сначала нужно открыть нужную остановку/дату в сети. Браузер может очищать
|
||
локальное хранилище. Просмотренные рейсы и сообщения также доступны из сохранённых
|
||
данных с предупреждением об устаревании. Поиск рядом требует сети и разрешения
|
||
браузера на местоположение; в демонстрации расстояния явно помечены как примеры.
|
||
Карты с движением транспорта, пушей и аккаунтов пока нет.
|
||
|
||
Расписание рейса показывает плановые времена, а не положение автобуса. Если
|
||
несколько вариантов приходят одновременно, ссылка из полного расписания
|
||
содержит идентификатор конкретного рейса. При изменившемся расписании приложение
|
||
предлагает выбрать отправление заново. Сообщения об отменах информируют пользователя,
|
||
но не удаляют времена из исходного расписания. На будущую дату рядом могут
|
||
отображаться текущие сообщения; в каждом виден полный период действия.
|
||
|
||
## Запуск
|
||
|
||
Требуется Node.js 22.12+ (проверено с Node.js 24).
|
||
|
||
```sh
|
||
npm install
|
||
cp .env.example .env
|
||
```
|
||
|
||
В `.env` задайте `RIDEBUS_API_KEY`. При создании этого проекта локальный файл
|
||
уже настроен по конфигурации лендинга. `.env` исключён из Git; ключ используется
|
||
только сервером и не попадает в JavaScript или запросы браузера.
|
||
|
||
```sh
|
||
npm run dev
|
||
```
|
||
|
||
Приложение: <http://localhost:5173>. Один Node-сервер обслуживает Vite и API,
|
||
поэтому отдельный процесс прокси не нужен. В development service worker отключён.
|
||
|
||
Для проверки установки и офлайн-работы используйте production-сборку:
|
||
|
||
```sh
|
||
npm run build
|
||
npm start
|
||
```
|
||
|
||
Порт задаётся переменной `PORT`, адрес — `HOST` (по умолчанию `127.0.0.1`).
|
||
При запуске в контейнере или на сервере используйте `HOST=0.0.0.0` и HTTPS
|
||
на внешнем reverse proxy. `localhost` допускает service worker без HTTPS.
|
||
Раздачи одного `dist/` недостаточно: нужен серверный обработчик `/api/data`.
|
||
|
||
## Развёртывание через Docker Compose
|
||
|
||
Требуются Docker Engine с Docker Compose v2 и HTTPS-прокси на сервере.
|
||
В `docker-compose.yml` один сервис `web`: он собирает фронтенд и запускает
|
||
Node-сервер, который обслуживает приложение и запросы к RideBus.
|
||
Образ собирается в несколько этапов; в конечном контейнере только `dist/`
|
||
и сервер без зависимостей сборки. Схема описана в
|
||
[документации Docker](https://docs.docker.com/build/building/multi-stage/).
|
||
|
||
На сервере скопируйте проект и создайте `.env`:
|
||
|
||
```sh
|
||
cp .env.example .env
|
||
```
|
||
|
||
Задайте `RIDEBUS_API_KEY`; при необходимости измените `RIDEBUS_API_BASE_URL`.
|
||
Ключ передаётся только процессу сервера при запуске контейнера; `.env` исключён
|
||
из контекста сборки. Compose остановится с понятной ошибкой, если ключ пустой.
|
||
`PWA_PORT=8080` и `PWA_BIND_ADDRESS=127.0.0.1` задают адрес на хосте.
|
||
Внутри контейнера `HOST=0.0.0.0` и `PORT=5173` фиксированы отдельно от настроек
|
||
локального запуска.
|
||
|
||
```sh
|
||
docker compose config --quiet
|
||
docker compose up -d --build --wait
|
||
docker compose ps
|
||
curl --fail http://127.0.0.1:8080/healthz
|
||
```
|
||
|
||
`/healthz` возвращает `{"status":"ok"}` при работающем HTTP-сервере.
|
||
Проверка не обращается к RideBus: ошибки источника расписаний отображаются
|
||
в самом приложении. Включены перезапуск процесса после сбоя, healthcheck
|
||
и ограничение размера логов. Контейнер работает от пользователя `node`
|
||
с файловой системой только для чтения и отдельным временным `/tmp`.
|
||
Параметры сервисов описаны в
|
||
[справочнике Compose](https://docs.docker.com/reference/compose-file/services/).
|
||
|
||
Направьте весь трафик вашего HTTPS-домена на `http://127.0.0.1:8080`, включая
|
||
`/api/`, `/healthz`, `/sw.js` и `/manifest.webmanifest`. Если Nginx уже работает
|
||
на хосте и TLS-сертификат настроен, добавьте в его HTTPS-блок `server`:
|
||
|
||
```nginx
|
||
location / {
|
||
proxy_pass http://127.0.0.1:8080;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
}
|
||
```
|
||
|
||
Если изменили `PWA_PORT`, укажите тот же порт в `proxy_pass`.
|
||
Не добавляйте кеширование для `/api/` и `/sw.js`. HTTPS нужен для установки
|
||
PWA, service worker, геолокации и системной отправки ссылки;
|
||
ограничения описаны в [MDN](https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Secure_Contexts).
|
||
Если ваш прокси работает в другом контейнере, подключите его и `web` к общей
|
||
Docker-сети и используйте upstream `http://web:5173`.
|
||
|
||
После обновления исходников повторите `docker compose up -d --build --wait`.
|
||
Для просмотра логов и остановки:
|
||
|
||
```sh
|
||
docker compose logs --tail=100 -f web
|
||
docker compose down
|
||
```
|
||
|
||
Серверная база данных и постоянный volume приложению не нужны: избранное,
|
||
поездки и просмотренные расписания хранятся в браузере, а серверный кеш
|
||
создаётся в памяти заново после перезапуска.
|
||
|
||
## Стек и файлы
|
||
|
||
- React, TypeScript, Vite, React Router.
|
||
- Иконки `@vkontakte/icons`, счётчик избранного отдельным бейджем в мобильной навигации.
|
||
- Sass и цвета в `src/_tokens.scss`; адаптивные стили в `src/styles.scss`.
|
||
- `src/_motion.scss` — длительности, отклик элементов и анимации появления;
|
||
длинные списки ограничивают количество одновременно анимируемых строк.
|
||
- `src/_features.scss` — поездки, почасовая сетка, ближайшие остановки и экран рейса.
|
||
- TanStack Query для загрузки и обновления, Dexie/IndexedDB для снимков и избранного.
|
||
- `vite-plugin-pwa` / Workbox для manifest, service worker и офлайн-оболочки.
|
||
- `server/ridebus.mjs` — адаптер RideBus, нормализация, таймауты, кеш.
|
||
- `server/index.mjs` — Node HTTP-сервер и фиксированный список методов чтения.
|
||
- `Dockerfile`, `docker-compose.yml`, `.env.example` — релизная сборка и запуск на сервере.
|
||
- `src/App.tsx` — поиск, выбор города/остановки и избранное.
|
||
- `src/components/Departures.tsx` — отправления, фильтр, сохранённые данные.
|
||
- `src/components/FavoriteCard.tsx` — ближайшие рейсы в избранном.
|
||
- `src/components/About.tsx` — страница автора проекта.
|
||
- `src/components/QuickTrips.tsx` — сохранение и карточки поездок.
|
||
- `src/components/TripCarousel.tsx` — адаптивная карусель с кнопками, свайпами и клавиатурой.
|
||
- `src/components/ShareDeparture.tsx` — отправка/копирование ссылки на конкретное отправление.
|
||
- `src/components/StopList.tsx` — общие строки и контейнер для обоих списков остановок.
|
||
- `src/components/HourlyTimetable.tsx`, `RoutePage.tsx` — почасовая сетка и конкретный рейс.
|
||
- `src/components/ServiceAlerts.tsx`, `NearbyStops.tsx` — изменения и поиск рядом.
|
||
- `src/lib/storage.ts` — версия 2 базы; существующие снимки/избранное сохраняются при обновлении.
|
||
- `server/timetable.mjs` — расчёт времени остановки по полному расписанию рейсов.
|
||
- `server/features.mjs` — нормализация сообщений и ближайших остановок.
|
||
|
||
Прокси использует `GET /api/v1/cities`, `GET /api/v1/stops` и
|
||
`GET /api/v1/eta/stop/:id`. Для полного расписания используются
|
||
`GET /api/v1/stops/:id`, `GET /api/v1/routes/:id/stops` и
|
||
`GET /api/v1/routes/:id/schedule/today?date=YYYY-MM-DD`.
|
||
Экран рейса дополнительно использует `GET /api/v1/routes/:id`;
|
||
сообщения — `GET /api/v1/alerts/active`, поиск рядом —
|
||
`GET /api/v1/stops/nearby?lat=…&lon=…&radius=1000`.
|
||
Город передаётся в `X-City-Id`, ключ — в `X-API-Key`.
|
||
Браузер получает нормализованные данные с того же origin.
|
||
Параметры города/остановки доступны в URL (`/?city=1&stop=…`), в том числе
|
||
при переходе из избранного. Полное расписание использует
|
||
`/?city=1&stop=…&view=day&date=2026-10-07`.
|
||
Сервер кеширует полное расписание и его исходные данные на 5 минут,
|
||
разделяя города и даты; ограничивает параллельные запросы к RideBus шестью.
|
||
|
||
## Проверки
|
||
|
||
```sh
|
||
npm test
|
||
npm run typecheck
|
||
npm run build
|
||
PLAYWRIGHT_BROWSERS_PATH=.playwright npx playwright install chromium
|
||
npm run test:e2e
|
||
```
|
||
|
||
Playwright проверяет desktop и экран iPhone 13 в Chromium: поиск, фильтры,
|
||
сохранение избранного, ожидание в часах/минутах, выбор даты и направления,
|
||
страницу автора, поездки, миграцию хранилища, диалоги с клавиатурой,
|
||
почасовую сетку и экран рейса, разрешённую/отклонённую геолокацию,
|
||
отсутствие сохранённых координат, фильтрацию/истечение сообщений,
|
||
карусель от двух поездок на мобильном/трёх на десктопе, touch-свайпы,
|
||
переход к отправлениям между городами, уменьшение движения,
|
||
отправку/копирование/отмену и ручной резервный способ поделиться рейсом,
|
||
одинаковое оформление списков остановок,
|
||
затухание края карусели и отсутствие кнопок демо в релизном интерфейсе,
|
||
разделение городов, честную ошибку API и настоящий офлайн-перезапуск
|
||
через service worker, включая расписание на конкретную дату и рейс.
|
||
Это эмуляция размера экрана,
|
||
а не проверка на физическом iPhone/Safari. Тесты используют демонстрационные
|
||
данные и контролируемые ответы; координаты задаются в тестовом браузере.
|
||
Скриншоты сохраняются в `test-results/`.
|
||
|
||
```sh
|
||
npm run icons
|
||
```
|
||
|
||
Эта команда пересоздаёт PNG-иконки из исходного логотипа. Иконки и шрифты
|
||
поставляются вместе с приложением и не требуют сети после первого открытия.
|