up
This commit is contained in:
@@ -152,6 +152,36 @@ Venue автоматически учитывает браузеры игрок
|
|||||||
При ошибке предыдущий снимок и время остаются видимыми, доступен повтор.
|
При ошибке предыдущий снимок и время остаются видимыми, доступен повтор.
|
||||||
«Нет связи» означает отсутствие heartbeat сайта, а не диагностику Wi-Fi.
|
«Нет связи» означает отсутствие heartbeat сайта, а не диагностику Wi-Fi.
|
||||||
|
|
||||||
|
Блок «Wi-Fi площадки» (этапы 14–15) показывает текущие Wi-Fi-клиенты
|
||||||
|
всего Cudy: MAC, IP, диапазон и доступный сигнал в dBm. Он не связывает MAC
|
||||||
|
с UUID браузера или выбранной игрой, не показывает имена клиентов и трафик.
|
||||||
|
Сборщик читает роутер каждые 10 секунд; при ошибке оставляет последний успешный
|
||||||
|
снимок и его время, в том числе после перезапуска сборщика. Снимок старше 30 секунд помечается устаревшим; отсутствие
|
||||||
|
сигнала отображается как «Нет данных». История не сохраняется.
|
||||||
|
|
||||||
|
Поддержан и проверен TR3000 V1 с прошивкой 2.5.27-20260804-183625, штатная
|
||||||
|
английская HTML-страница клиентов. Неизвестная разметка возвращает ошибку,
|
||||||
|
а не пустой список. После обновления прошивки нужно проверить адаптер снова.
|
||||||
|
Настройки Cudy сборщик не меняет; использует только вход и чтение списка.
|
||||||
|
Он работает отдельным процессом в сети `entrance`, без доступа к БД/RustFS;
|
||||||
|
backend читает только общий том снимка, подключённый к нему read-only.
|
||||||
|
|
||||||
|
Сбор Cudy необязателен. Для включения создайте `venue/cudy/` с правами 0700,
|
||||||
|
сохраните пароль администратора в `venue/cudy/password` с правами 0600 и добавьте
|
||||||
|
в локальный `venue/.env`:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
CUDY_PASSWORD_FILE=/run/secrets/cudy/password
|
||||||
|
CUDY_URL=http://192.168.10.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Пароль не записывается в `.env`, API, логи или Git; `venue/cudy/` исключён из Git.
|
||||||
|
URL допускает только числовой локальный IP без credentials и пути. После
|
||||||
|
обновления артефактов запустите штатный `make venue`; отдельно сборщик backend
|
||||||
|
бинарника запускается с `--collect-cudy`. Без `CUDY_PASSWORD_FILE` обращения
|
||||||
|
к роутеру отсутствуют и панель сообщает, что сбор не настроен. Работающая
|
||||||
|
площадка при реализации этапов 14–15 не обновлялась.
|
||||||
|
|
||||||
Разработчик без роли организатора/администратора/автора в venue получает
|
Разработчик без роли организатора/администратора/автора в venue получает
|
||||||
список игр и только вкладку состояния: сведения игры ограничены ID, названием,
|
список игр и только вкладку состояния: сведения игры ограничены ID, названием,
|
||||||
описанием, форматом, местом, плановым началом и статусом. Команды, пароли,
|
описанием, форматом, местом, плановым началом и статусом. Команды, пароли,
|
||||||
|
|||||||
+6
-3
@@ -27,13 +27,13 @@
|
|||||||
|
|
||||||
### ED-029 — Автономная площадка и панель организатора
|
### ED-029 — Автономная площадка и панель организатора
|
||||||
|
|
||||||
- Статус: `IN_PROGRESS` — этап 3 принят пользователем без WAN; этапы 4–13 завершены; аппаратная приёмка этапа 8 подтверждена пользователем.
|
- Статус: `IN_PROGRESS` — этап 3 принят пользователем без WAN; этапы 4–14 завершены; автоматическая часть этапа 15 завершена, аппаратная приёмка остаётся; этап 8 принят пользователем.
|
||||||
- Репозитории: корневой, `evening_detective_server`, `evening_detective_frontend`.
|
- Репозитории: корневой, `evening_detective_server`, `evening_detective_frontend`.
|
||||||
- Цель: реализовать принятый пользователем план из 15 последовательных этапов: автономное окружение; локальные организаторы; прежние адреса через Cudy; пакет игры; экспорт со статусом офлайн-режима; локальный импорт; интерфейс передачи; полный билетный и игровой flow; пакет результатов; возврат онлайн; интерфейс возврата; устройства; панель состояния; сбор данных Cudy; Wi-Fi и испытание площадки.
|
- Цель: реализовать принятый пользователем план из 15 последовательных этапов: автономное окружение; локальные организаторы; прежние адреса через Cudy; пакет игры; экспорт со статусом офлайн-режима; локальный импорт; интерфейс передачи; полный билетный и игровой flow; пакет результатов; возврат онлайн; интерфейс возврата; устройства; панель состояния; сбор данных Cudy; Wi-Fi и испытание площадки.
|
||||||
- Критерии: проверяемый результат каждого этапа; цикл «онлайн → ноутбук → проведение → онлайн» через интерфейс с сохранением QR, билетов и связей; повторные импорты без дублей; мониторинг устройств и Wi-Fi; полный прогон без WAN, 100 клиентов и проверка покрытия.
|
- Критерии: проверяемый результат каждого этапа; цикл «онлайн → ноутбук → проведение → онлайн» через интерфейс с сохранением QR, билетов и связей; повторные импорты без дублей; мониторинг устройств и Wi-Fi; полный прогон без WAN, 100 клиентов и проверка покрытия.
|
||||||
- Ограничения: только тестовые игры до завершения возврата; существующие правила игры и формат `offline` сохраняются; без несогласованных новых зависимостей и отклонений от стандартов; без персональных списков игроков, CPU/памяти Cudy и автоматического переключения серверов; настройки роутера менять только по явному запросу пользователя; секреты и пакеты вне Git; commit/push выполняет пользователь.
|
- Ограничения: только тестовые игры до завершения возврата; существующие правила игры и формат `offline` сохраняются; без несогласованных новых зависимостей и отклонений от стандартов; без персональных списков игроков, CPU/памяти Cudy и автоматического переключения серверов; настройки роутера менять только по явному запросу пользователя; секреты и пакеты вне Git; commit/push выполняет пользователь.
|
||||||
- Проверка: Go-стандарт и `go test ./...`; PostgreSQL-интеграционные тесты для атомарности; frontend-стандарт, type-check, lint, lint:css и build; штатная генерация protobuf; ручные контрольные точки после этапов 3, 8, 11, 15.
|
- Проверка: Go-стандарт и `go test ./...`; PostgreSQL-интеграционные тесты для атомарности; frontend-стандарт, type-check, lint, lint:css и build; штатная генерация protobuf; ручные контрольные точки после этапов 3, 8, 11, 15.
|
||||||
- Прогресс: этапы 1–2 реализованы и проверены в локальном Docker-окружении. Добавлены отдельный Compose, подготовка образов заранее, постоянные тома, миграции перед стартом, healthchecks и прокси. Явный `APP_MODE=venue` отключает SMTP, фоновые письма и внешний webhook; почтовые операции аккаунта возвращают безопасную ошибку до изменения данных. Локальные аккаунты создаются штатным `dev-user.sql`, роли и правила автоматического подтверждения команд сохраняются. Этап 3 принят пользователем после проверки без WAN; формат и код пакета игры (этап 4) реализованы и проверены. Экспорт со статусом офлайн-режима (этап 5) и локальный импорт (этап 6) реализованы и проверены. Этап 7 реализован и проверен; этап 8 проверен на изолированном стенде и принят пользователем на аппаратной контрольной точке; этапы 9–13 реализованы и проверены; этапы 14–15 ещё не завершены.
|
- Прогресс: этапы 1–2 реализованы и проверены в локальном Docker-окружении. Добавлены отдельный Compose, подготовка образов заранее, постоянные тома, миграции перед стартом, healthchecks и прокси. Явный `APP_MODE=venue` отключает SMTP, фоновые письма и внешний webhook; почтовые операции аккаунта возвращают безопасную ошибку до изменения данных. Локальные аккаунты создаются штатным `dev-user.sql`, роли и правила автоматического подтверждения команд сохраняются. Этап 3 принят пользователем после проверки без WAN; формат и код пакета игры (этап 4) реализованы и проверены. Экспорт со статусом офлайн-режима (этап 5) и локальный импорт (этап 6) реализованы и проверены. Этап 7 реализован и проверен; этап 8 проверен на изолированном стенде и принят пользователем на аппаратной контрольной точке; этапы 9–13 реализованы и проверены; этап 14 реализован и проверен на настоящем TR3000; код и автоматические проверки этапа 15 завершены, ожидается аппаратная приёмка Wi-Fi.
|
||||||
- Дополнение по запросу пользователя: структура `venue` приведена к `prod` (`backend/`, `frontend/dist/`, `migrations/`, `Dockerfile.backend`, `docker-compose.yml`, `deploy.sh`); имена основных Compose-сервисов совпадают. Общая генерация `make build` / `make prod-build` готовит артефакты обеих папок; frontend собирается отдельно с настройками своего окружения. Текущие файлы `prod` при этой работе не изменяются; сборка проверяется с production-выводом в `/tmp`.
|
- Дополнение по запросу пользователя: структура `venue` приведена к `prod` (`backend/`, `frontend/dist/`, `migrations/`, `Dockerfile.backend`, `docker-compose.yml`, `deploy.sh`); имена основных Compose-сервисов совпадают. Общая генерация `make build` / `make prod-build` готовит артефакты обеих папок; frontend собирается отдельно с настройками своего окружения. Текущие файлы `prod` при этой работе не изменяются; сборка проверяется с production-выводом в `/tmp`.
|
||||||
- Упрощение запуска: отдельные `init.sh` и `prepare.sh` удалены, всё собрано в `deploy.sh`. Единственная команда запуска — `make venue`: создаёт `.env` только при его отсутствии, при необходимости готовит зависимости/образы и автоматически создаёт организатора. Повторный готовый запуск не собирает и не скачивает образы. `make venue-update` явно пересобирает окружение с интернетом. Аккаунт организатора синхронизируется со значениями `.env` при каждом запуске. Первый запуск, повторный запуск без интернета и обновление проверяются `python3 tests/venue/deploy.py` с подставными командами; реальный готовый запуск проверен через Docker и API-smoke.
|
- Упрощение запуска: отдельные `init.sh` и `prepare.sh` удалены, всё собрано в `deploy.sh`. Единственная команда запуска — `make venue`: создаёт `.env` только при его отсутствии, при необходимости готовит зависимости/образы и автоматически создаёт организатора. Повторный готовый запуск не собирает и не скачивает образы. `make venue-update` явно пересобирает окружение с интернетом. Аккаунт организатора синхронизируется со значениями `.env` при каждом запуске. Первый запуск, повторный запуск без интернета и обновление проверяются `python3 tests/venue/deploy.py` с подставными командами; реальный готовый запуск проверен через Docker и API-smoke.
|
||||||
- Настройки площадки: по запросу пользователя необязательный `venue/.env.example` удалён; настройки создаются автоматически через `make venue`. Убраны ссылка и проверка примера; `python3 tests/venue/deploy.py` прошёл. Существующая `.env` не изменялась.
|
- Настройки площадки: по запросу пользователя необязательный `venue/.env.example` удалён; настройки создаются автоматически через `make venue`. Убраны ссылка и проверка примера; `python3 tests/venue/deploy.py` прошёл. Существующая `.env` не изменялась.
|
||||||
@@ -84,7 +84,10 @@
|
|||||||
- Результат этапа 13 (2026-10-11): venue-вкладка «Состояние» `/games/:id?tab=state` показывает серверные connected/serverTime, количество доступных устройств/устройств без связи/подключений, UUID профиля, последние обращения и раскрываемые подключения с типом, ID команды/экрана, статусом, IP и User-Agent. Нет связи идёт первым, затем UUID; непривязанные браузеры исключает фильтр игры. Generated GetDevices вызывается сразу и каждые 5 секунд через useSimplePolling только на активной вкладке, без параллельных запросов; поздние ответы после смены игры, ухода и уничтожения игнорируются. Первичная загрузка, пустой список, встроенная ошибка, сохранение предыдущего снимка/времени и повтор реализованы без повторных toast. Вкладка доступна с клавиатуры. Игровые показатели, Cudy и история присутствия не добавлялись.
|
- Результат этапа 13 (2026-10-11): venue-вкладка «Состояние» `/games/:id?tab=state` показывает серверные connected/serverTime, количество доступных устройств/устройств без связи/подключений, UUID профиля, последние обращения и раскрываемые подключения с типом, ID команды/экрана, статусом, IP и User-Agent. Нет связи идёт первым, затем UUID; непривязанные браузеры исключает фильтр игры. Generated GetDevices вызывается сразу и каждые 5 секунд через useSimplePolling только на активной вкладке, без параллельных запросов; поздние ответы после смены игры, ухода и уничтожения игнорируются. Первичная загрузка, пустой список, встроенная ошибка, сохранение предыдущего снимка/времени и повтор реализованы без повторных toast. Вкладка доступна с клавиатуры. Игровые показатели, Cudy и история присутствия не добавлялись.
|
||||||
- Доступ этапа 13: GetGames/GetGame и UI permissions расширены для разработчика только в venue. Разработчик без организатора/администратора/автора получает явный whitelist ID/названия/описания/формата/места/планового начала/статуса; GetGame использует краткое чтение без сценария/команд. UI скрывает создание и результаты, показывает название и только состояние, нормализует другие tab, не загружает экраны/музыку/план/карту/ответы; heartbeat остаётся браузерным. Дополнительные роли, импорт и запреты игровых действий сохранены; prod новые права не получает. При смене роли полный поздний GetGame не применяется, приватные баннер/модалки скрываются.
|
- Доступ этапа 13: GetGames/GetGame и UI permissions расширены для разработчика только в venue. Разработчик без организатора/администратора/автора получает явный whitelist ID/названия/описания/формата/места/планового начала/статуса; GetGame использует краткое чтение без сценария/команд. UI скрывает создание и результаты, показывает название и только состояние, нормализует другие tab, не загружает экраны/музыку/план/карту/ответы; heartbeat остаётся браузерным. Дополнительные роли, импорт и запреты игровых действий сохранены; prod новые права не получает. При смене роли полный поздний GetGame не применяется, приватные баннер/модалки скрываются.
|
||||||
- Проверки этапа 13: gofmt, полный go test ./..., race-тесты device_presence_service/app с управляемыми часами этапа 12; frontend type-check/lint/lint:css/build и self-check gameState прошли. Self-check покрывает сводку/сортировку, активность, отсутствие параллельных запросов, поздние ответы, ошибку/восстановление. Изолированные PostgreSQL/RustFS API-регрессии и Firefox проверили организатора, три подключения одного профиля, клавиатуру, смену контекста, пустой список, потерю/восстановление GetDevices, остановку запросов на другой вкладке, разработчика venue/prod, безопасные GetGame/GetGames и отсутствие закрытых фоновых запросов. API/protobuf/миграции/зависимости не менялись. Ручное ревью diff по Go/frontend-стандартам и независимое read-only ревью выполнены; пользовательские изменения prod/reproxy/backend-бинарника сохранены. Работающие окружения и Cudy не менялись.
|
- Проверки этапа 13: gofmt, полный go test ./..., race-тесты device_presence_service/app с управляемыми часами этапа 12; frontend type-check/lint/lint:css/build и self-check gameState прошли. Self-check покрывает сводку/сортировку, активность, отсутствие параллельных запросов, поздние ответы, ошибку/восстановление. Изолированные PostgreSQL/RustFS API-регрессии и Firefox проверили организатора, три подключения одного профиля, клавиатуру, смену контекста, пустой список, потерю/восстановление GetDevices, остановку запросов на другой вкладке, разработчика venue/prod, безопасные GetGame/GetGames и отсутствие закрытых фоновых запросов. API/protobuf/миграции/зависимости не менялись. Ручное ревью diff по Go/frontend-стандартам и независимое read-only ревью выполнены; пользовательские изменения prod/reproxy/backend-бинарника сохранены. Работающие окружения и Cudy не менялись.
|
||||||
- Следующий шаг: этап 14 — сбор данных Cudy. Этап 13 завершён; общая задача остаётся `IN_PROGRESS` и не переносится в архив. ZIP остаётся неподписанным. Фактический ZIP 1 ГиБ, нагрузка на 100 клиентов и покрытие Wi-Fi не проверялись; развёртывание, commit/push не выполнялись.
|
- Результат этапа 14 (2026-10-11): отдельный процесс того же backend-бинарника `--collect-cudy` читает текущих Wi-Fi-клиентов Cudy через штатный вход и GET страницы клиентов раз в 10 секунд. Проверен настоящий TR3000 V1 2.5.27-20260804-183625; настройки не менялись. Собираются только MAC/IP/диапазон/доступный сигнал dBm, без имён, трафика и истории. Пароль читается из локального файла, не выдаётся API/логами; `venue/cudy/` исключён из Git. URL ограничен числовым private/loopback IP, redirects тем же host/scheme, ответ 2 МиБ, HTTP timeout 5 секунд, сессия переиспользуется и восстанавливается. Неизвестная разметка вызывает ошибку вместо ложного пустого списка. Collector изолирован в entrance без БД/RustFS; backend сохраняет внутреннюю сеть и читает общий атомарный снимок read-only. Без credential-файла обращений к роутеру нет.
|
||||||
|
- Результат автоматической части этапа 15: Wi-Fi-блок вкладки состояния показывает весь Cudy независимо от игры, MAC/IP/диапазон/dBm или «Нет данных», число клиентов, последний успешный снимок, ошибку/устаревание. При ошибке collector сохраняет прежние данные и время, включая перезапуск; свежесть 30 секунд считает сервер, история не сохраняется. MAC не связывается предположениями с UUID браузера. Существующий GetDevices расширен Wi-Fi-полем с теми же venue/role guards, нового endpoint/миграций/зависимостей нет. Обновлены protobuf/OpenAPI и оба generated clients.
|
||||||
|
- Проверки этапов 14–15: gofmt, `go test ./...`, race Cudy/wifi/presence/app, frontend type-check/lint/lint:css/build и self-check состояния прошли. Mock Cudy проверяет hash/CSRF/session/readonly GET, DOM/оба диапазона/сигнал/неизвестную разметку; снимок проверен атомарно и с управляемыми часами; CLI self-check подтвердил сохранение клиентов/времени после перезапуска при недоступном роутере. Firefox выявил прежнюю гонку раннего возврата ZIP до загрузки ID игры; действия передачи/возврата теперь появляются только после загрузки положительного ID. Итоговый изолированный Firefox-прогон обоих форматов (1440/390 px) прошёл: Wi-Fi fixtures, устройства/роли/polling и полный цикл ZIP до повторного возврата после перезапуска. Полный API-flow с PostgreSQL/race-регрессиями также прошёл; временные стенды удалены. `load.py` прошёл на отдельном Docker-стенде без WAN: 100 виртуальных профилей, 25 команд, 60 секунд, 300 heartbeat, 1200 story, 50 ходов, 25 ответов и 12 опросов состояния; ровно 100 доступных устройств и ноль HTTP-ошибок. p95: heartbeat 118 мс, story 68 мс, move 109 мс, answers 45 мс, devices 138 мс (бюджет теста 2 с). Нагрузка backend идёт по HTTP напрямую; DNS/HTTPS/Nginx проверены отдельно `dns.py --docker`/`https.py --docker`. Ручное ревью по стандартам и независимое read-only ревью выполнены; замечание о ложном пустом DOM исправлено и покрыто регрессией.
|
||||||
|
- Следующий шаг: аппаратная контрольная точка этапа 15 по `tests/venue/README.md`: после отдельно согласованного обновления площадки Android/iPhone, отключённые WAN/мобильные данные, 100 настоящих Wi-Fi-устройств и покрытие зала. Пользователь выбрал автоматическую нагрузку 100 виртуальных клиентов с отдельной ручной аппаратной приёмкой. До её подтверждения ED-029 остаётся `IN_PROGRESS` и не переносится в архив. Всего 15 этапов. ZIP остаётся неподписанным, фактический ZIP 1 ГиБ не проверен; развёртывание, commit/push не выполнялись.
|
||||||
|
|
||||||
### ED-024 — Общая дизайн-система фронтенда
|
### ED-024 — Общая дизайн-система фронтенда
|
||||||
|
|
||||||
|
|||||||
+51
-1
@@ -62,7 +62,8 @@ python3 tests/venue/flow.py --apply --browser
|
|||||||
(`offline`). Проверяются клавиатурный focus, отказ на ход с сохранением ввода,
|
(`offline`). Проверяются клавиатурный focus, отказ на ход с сохранением ввода,
|
||||||
блокировка повторной отправки и кнопки на паузе при уже введённом коде.
|
блокировка повторной отправки и кнопки на паузе при уже введённом коде.
|
||||||
Локальный HTTP-прокси переключает один публичный адрес между двумя реальными
|
Локальный HTTP-прокси переключает один публичный адрес между двумя реальными
|
||||||
backend, имитируя смену DNS; API-ответы не подменяются. Тестовые ссылки, ZIP,
|
backend, имитируя смену DNS; Игровые API-ответы не подменяются; ошибки, пустой список и состояния Wi-Fi
|
||||||
|
панели мониторинга дополнительно проверяются управляемыми ответами GetDevices. Тестовые ссылки, ZIP,
|
||||||
профиль Firefox и сборки остаются во временном каталоге до конца прогона.
|
профиль Firefox и сборки остаются во временном каталоге до конца прогона.
|
||||||
|
|
||||||
`--results` дополнительно проверяет этап 9: серверный сервис собирает ZIP
|
`--results` дополнительно проверяет этап 9: серверный сервис собирает ZIP
|
||||||
@@ -155,3 +156,52 @@ Firefox проверяет игрока, организатора и экран
|
|||||||
ошибку/восстановление и отбрасывание ответов после смены игры, ухода и unmount.
|
ошибку/восстановление и отбрасывание ответов после смены игры, ухода и unmount.
|
||||||
Временные границы connected остаются в тестах сервиса этапа 12 с управляемыми
|
Временные границы connected остаются в тестах сервиса этапа 12 с управляемыми
|
||||||
часами; клиент пороги не пересчитывает.
|
часами; клиент пороги не пересчитывает.
|
||||||
|
|
||||||
|
|
||||||
|
## Wi-Fi и нагрузка (этапы 14–15)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 tests/venue/load.py
|
||||||
|
python3 tests/venue/flow.py --apply --browser
|
||||||
|
```
|
||||||
|
|
||||||
|
`load.py` использует тот же одноразовый стенд без WAN: 100 виртуальных UUID,
|
||||||
|
25 новых команд по четыре браузера, 60 секунд. Каждый профиль отправляет
|
||||||
|
heartbeat раз в 20 секунд и читает историю раз в 5 секунд; по одному профилю
|
||||||
|
команды выполняют два хода и сохраняют ответ. Организатор опрашивает состояние
|
||||||
|
раз в 5 секунд. Проверяются ровно 100 доступных устройств, 50 сохранённых ходов,
|
||||||
|
25 ответивших команд и отсутствие HTTP-ошибок. Отчёт содержит число запросов,
|
||||||
|
p50/p95/max; p95 ≤ 2 секунды — бюджет этого теста, не обещание для любого железа.
|
||||||
|
Запросы идут прямо к backend по HTTP: DNS, HTTPS и прокси проверяются отдельно
|
||||||
|
`dns.py --docker` и `https.py --docker`. Это нагрузка backend, не проверка радио.
|
||||||
|
|
||||||
|
Go-тесты адаптера Cudy проверяют вход/продление сессии, ограничение URL/ответа,
|
||||||
|
разбор обеих полос, отсутствующий сигнал, ошибку при неизвестной разметке,
|
||||||
|
атомарный снимок, свежесть с управляемыми часами и venue/prod/роли. Firefox
|
||||||
|
проверяет MAC/IP/диапазон/dBm, устаревание, ошибку и отсутствие сигнала; эти
|
||||||
|
состояния Wi-Fi задаются fixture. Отдельно адаптер проверен чтением настоящего
|
||||||
|
TR3000 V1 2.5.27-20260804-183625 без изменения настроек роутера.
|
||||||
|
|
||||||
|
### Аппаратная контрольная точка этапа 15
|
||||||
|
|
||||||
|
После согласованного обновления реальной площадки, только на тестовой игре:
|
||||||
|
|
||||||
|
1. Включить сбор Cudy по корневому README; отключить WAN и мобильные данные.
|
||||||
|
На Android/iPhone открыть прежние домены, проверить DNS и доверенный HTTPS.
|
||||||
|
2. Подключить 100 настоящих Wi-Fi-устройств (несколько вкладок одного профиля
|
||||||
|
не заменяют устройства); открыть игру 25 командами, держать нагрузку
|
||||||
|
минимум 60 секунд, выполнять ходы и сохранять ответы.
|
||||||
|
3. Сверить клиентов Cudy с панелью, диапазоны и доступный сигнал; пройти края
|
||||||
|
и проблемные зоны площадки, проверить материалы и действия в каждой зоне.
|
||||||
|
MAC/UUID не сопоставлять предположениями; «Нет связи» относится к heartbeat.
|
||||||
|
4. Проверить паузу/продолжение, перезапуск venue и повторную регистрацию
|
||||||
|
браузеров, смену контекста/несколько вкладок; убедиться, что панели в prod нет.
|
||||||
|
5. Временно остановить сборщик: через 30 секунд снимок должен устареть,
|
||||||
|
прежние данные и время остаться видимыми. Запустить снова и проверить
|
||||||
|
восстановление. Потерю API и восстановление проверить без потери снимка.
|
||||||
|
6. Завершить игру, вернуть ZIP на источник, проверить результаты и прежние
|
||||||
|
ссылки. Зафиксировать оборудование, число клиентов, длительность,
|
||||||
|
задержки/ошибки и покрытие; сообщить результат для приёмки ED-029.
|
||||||
|
|
||||||
|
Автоматические проверки не заменяют эту приёмку. Пока она не подтверждена,
|
||||||
|
ED-029 остаётся `IN_PROGRESS`. Фактический ZIP 1 ГиБ также не проверен.
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ def check_browser_flow(stand, fixtures):
|
|||||||
if js('return ' + script):
|
if js('return ' + script):
|
||||||
return
|
return
|
||||||
time.sleep(.1)
|
time.sleep(.1)
|
||||||
raise AssertionError('Browser timed out: ' + description)
|
raise AssertionError('Browser timed out: ' + description + ': ' + str(js('return document.body.innerText')).replace('\n', ' ')[:1500])
|
||||||
|
|
||||||
def navigate(path):
|
def navigate(path):
|
||||||
command('WebDriver:Navigate', {'url': stand.public + path})
|
command('WebDriver:Navigate', {'url': stand.public + path})
|
||||||
@@ -177,6 +177,10 @@ def check_browser_flow(stand, fixtures):
|
|||||||
wait("!document.body.innerText.includes('Не удалось обновить состояние')", 'state recovery')
|
wait("!document.body.innerText.includes('Не удалось обновить состояние')", 'state recovery')
|
||||||
js("window.stateMode='empty';return true;")
|
js("window.stateMode='empty';return true;")
|
||||||
wait("document.body.innerText.includes('Устройства этой игры пока не зарегистрированы')", 'empty devices', 10)
|
wait("document.body.innerText.includes('Устройства этой игры пока не зарегистрированы')", 'empty devices', 10)
|
||||||
|
js("window.fetch=(url,options)=>{if(String(url).includes('/api/devices?')){window.stateCalls++;return Promise.resolve(new Response(JSON.stringify({devices:[],serverTime:new Date().toISOString(),wifi:{enabled:true,hasError:window.wifiFailed,stale:window.wifiFailed,collectedAt:new Date().toISOString(),clients:[{MAC:'AA:BB:CC:DD:EE:01',IP:'192.168.10.9',band:'5 GHz',signal:window.wifiFailed?undefined:-65}]}}),{headers:{'Content-Type':'application/json'}}));}return window.stateFetch(url,options)};return true;")
|
||||||
|
wait("document.body.innerText.includes('-65 dBm')", 'Wi-Fi client and dBm', 10)
|
||||||
|
js("window.wifiFailed=true;return true;")
|
||||||
|
wait("document.body.innerText.includes('Данные устарели') && document.body.innerText.includes('Не удалось обновить данные Cudy') && document.body.innerText.includes('Нет данных')", 'stale Wi-Fi and missing signal', 10)
|
||||||
assert js("const tab=[...document.querySelectorAll('.n-tabs-tab[data-name]')].find(t=>t.textContent.trim()==='Игра');tab.click();return true;")
|
assert js("const tab=[...document.querySelectorAll('.n-tabs-tab[data-name]')].find(t=>t.textContent.trim()==='Игра');tab.click();return true;")
|
||||||
wait("location.search.includes('tab=game')", 'leave state tab')
|
wait("location.search.includes('tab=game')", 'leave state tab')
|
||||||
calls = js('return window.stateCalls')
|
calls = js('return window.stateCalls')
|
||||||
|
|||||||
@@ -181,6 +181,15 @@ with tempfile.TemporaryDirectory(prefix="venue-https-") as directory:
|
|||||||
assert {(p["protocol"], str(p["published"])) for p in dns["ports"]} == {("udp", "53"), ("tcp", "53")}
|
assert {(p["protocol"], str(p["published"])) for p in dns["ports"]} == {("udp", "53"), ("tcp", "53")}
|
||||||
assert all(p["host_ip"] == "127.0.0.1" for p in dns["ports"])
|
assert all(p["host_ip"] == "127.0.0.1" for p in dns["ports"])
|
||||||
assert any(v["target"] == "/etc/coredns/Corefile" and v["read_only"] for v in dns["volumes"])
|
assert any(v["target"] == "/etc/coredns/Corefile" and v["read_only"] for v in dns["volumes"])
|
||||||
|
collector = https["services"]["cudy_collector"]
|
||||||
|
backend = https["services"]["evening_detective_server"]
|
||||||
|
assert set(collector["networks"]) == {"entrance"} and not collector.get("ports")
|
||||||
|
assert set(backend["networks"]) == {"default"}
|
||||||
|
assert "DATABASE_URL" not in collector["environment"] and "S3_PASSWORD" not in collector["environment"]
|
||||||
|
assert any(v["target"] == "/run/secrets/cudy" and v["read_only"] for v in collector["volumes"])
|
||||||
|
assert any(v["target"] == "/run/cudy" and v["read_only"] for v in backend["volumes"])
|
||||||
|
assert not any(v["target"] == "/run/secrets/cudy" for v in backend["volumes"])
|
||||||
|
assert collector["environment"]["CUDY_SNAPSHOT_FILE"] == server["CUDY_SNAPSHOT_FILE"]
|
||||||
print("PASS: Compose merge, HTTPS addresses, ports, read-only mounts and preserved data volumes")
|
print("PASS: Compose merge, HTTPS addresses, ports, read-only mounts and preserved data volumes")
|
||||||
suffix = uuid.uuid4().hex[:10]
|
suffix = uuid.uuid4().hex[:10]
|
||||||
network = "venue-https-test-" + suffix
|
network = "venue-https-test-" + suffix
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
"""100 virtual clients in disposable prod/venue; does not test Wi-Fi radios."""
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
import statistics
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
sys.dont_write_bytecode = True
|
||||||
|
from flow import API, Stand, export_package, fixture
|
||||||
|
|
||||||
|
|
||||||
|
def check_load(stand):
|
||||||
|
data = fixture(stand, 'online')
|
||||||
|
venue = stand.apis['venue']
|
||||||
|
venue.request('/api/games/import', export_package(stand.apis['prod'], data['game']))
|
||||||
|
game = data['game']
|
||||||
|
teams = [venue.request('/api/teams', {'gameID': game, 'name': f'TEST load {i}', 'participantsCount': 4}) for i in range(25)]
|
||||||
|
venue.request(f'/api/games/{game}/start', {}, 'PUT')
|
||||||
|
samples = {}
|
||||||
|
lock = threading.Lock()
|
||||||
|
barrier = threading.Barrier(101)
|
||||||
|
ids = [str(uuid.uuid4()) for _ in range(100)]
|
||||||
|
connections = [str(uuid.uuid4()) for _ in range(100)]
|
||||||
|
|
||||||
|
def measured(name, action):
|
||||||
|
before = time.perf_counter()
|
||||||
|
result = action()
|
||||||
|
with lock:
|
||||||
|
samples.setdefault(name, []).append(time.perf_counter() - before)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def client(index):
|
||||||
|
public = API(venue.base)
|
||||||
|
team = teams[index // 4]
|
||||||
|
heartbeat = {'deviceID': ids[index], 'connectionID': connections[index], 'teamID': team['ID'], 'password': team['password']}
|
||||||
|
barrier.wait(timeout=30)
|
||||||
|
started = time.monotonic()
|
||||||
|
next_heartbeat = next_story = started
|
||||||
|
moved = False
|
||||||
|
while time.monotonic() - started < 60:
|
||||||
|
now = time.monotonic()
|
||||||
|
if now >= next_heartbeat:
|
||||||
|
result = measured('heartbeat', lambda: public.request('/api/devices/heartbeat', heartbeat))
|
||||||
|
assert result['enabled'] is True
|
||||||
|
next_heartbeat += 20
|
||||||
|
if not moved and index % 4 == 0:
|
||||||
|
for code in ('p1', 'p2'):
|
||||||
|
measured('move', lambda code=code: public.move(team, code))
|
||||||
|
measured('answers', lambda: public.request(f'/api/teams/{team["ID"]}/answers', {'password': team['password'], 'answers': [{'questionCode': 'q1', 'answer': 'Butler'}]}))
|
||||||
|
moved = True
|
||||||
|
if now >= next_story:
|
||||||
|
measured('story', lambda: public.story(team))
|
||||||
|
next_story += 5
|
||||||
|
time.sleep(.05)
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=100) as pool:
|
||||||
|
futures = [pool.submit(client, index) for index in range(100)]
|
||||||
|
barrier.wait(timeout=30)
|
||||||
|
deadline = time.monotonic() + 60
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
snapshot = measured('devices', lambda: venue.request(f'/api/devices?gameID={game}'))
|
||||||
|
assert len(snapshot['devices']) <= 100
|
||||||
|
time.sleep(5)
|
||||||
|
for future in futures:
|
||||||
|
future.result()
|
||||||
|
devices = venue.request(f'/api/devices?gameID={game}')['devices']
|
||||||
|
assert {device['ID'] for device in devices} == set(ids)
|
||||||
|
assert all(device['connected'] and len(device['connections']) == 1 for device in devices)
|
||||||
|
for team in teams:
|
||||||
|
assert len(API(venue.base).story(team)['actions']) == 2
|
||||||
|
answers = venue.request(f'/api/games/{game}/answers')['teams']
|
||||||
|
assert {row['team']['ID'] for row in answers if any(answer.get('answer') == 'Butler' for answer in row.get('answers', []))} == {team['ID'] for team in teams}
|
||||||
|
report = {}
|
||||||
|
for name, values in sorted(samples.items()):
|
||||||
|
values.sort()
|
||||||
|
p95 = values[math.ceil(.95 * len(values)) - 1]
|
||||||
|
report[name] = {'requests': len(values), 'p50_ms': round(statistics.median(values) * 1000), 'p95_ms': round(p95 * 1000), 'max_ms': round(values[-1] * 1000)}
|
||||||
|
assert p95 <= 2, f'{name} exceeds the test budget p95 <= 2s'
|
||||||
|
print('PASS: 100 profiles, 25 teams, 60s, 100 connected devices, 50 moves, 25 answers, zero HTTP errors', flush=True)
|
||||||
|
print(json.dumps(report, sort_keys=True), flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
with tempfile.TemporaryDirectory(prefix='ed029-load-') as directory:
|
||||||
|
stand = Stand(directory)
|
||||||
|
try:
|
||||||
|
stand.start()
|
||||||
|
check_load(stand)
|
||||||
|
finally:
|
||||||
|
stand.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -4,3 +4,5 @@ frontend/dist/
|
|||||||
migrations/
|
migrations/
|
||||||
certificates/
|
certificates/
|
||||||
packages/
|
packages/
|
||||||
|
|
||||||
|
/cudy/
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ x-app: &app
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.backend
|
dockerfile: Dockerfile.backend
|
||||||
environment:
|
environment: &app-environment
|
||||||
APP_MODE: venue
|
APP_MODE: venue
|
||||||
DATABASE_URL: postgresql://${POSTGRES_USER:?}:${POSTGRES_PASSWORD:?}@postgres_evening_detective_server:5432/${POSTGRES_DB:?}?sslmode=disable
|
DATABASE_URL: postgresql://${POSTGRES_USER:?}:${POSTGRES_PASSWORD:?}@postgres_evening_detective_server:5432/${POSTGRES_DB:?}?sslmode=disable
|
||||||
APP_BASE_URL: ${APP_BASE_URL:?}
|
APP_BASE_URL: ${APP_BASE_URL:?}
|
||||||
@@ -64,6 +64,11 @@ services:
|
|||||||
|
|
||||||
evening_detective_server:
|
evening_detective_server:
|
||||||
<<: *app
|
<<: *app
|
||||||
|
environment:
|
||||||
|
<<: *app-environment
|
||||||
|
CUDY_SNAPSHOT_FILE: /run/cudy/state.json
|
||||||
|
volumes:
|
||||||
|
- cudy_state:/run/cudy:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
migrate:
|
migrate:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
@@ -102,6 +107,20 @@ services:
|
|||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
restart: "no"
|
restart: "no"
|
||||||
|
|
||||||
|
cudy_collector:
|
||||||
|
image: evening-detective-venue-app:1
|
||||||
|
command: [/usr/local/bin/evening_detective_server, --collect-cudy]
|
||||||
|
environment:
|
||||||
|
CUDY_URL: ${CUDY_URL:-http://192.168.10.1}
|
||||||
|
CUDY_PASSWORD_FILE: ${CUDY_PASSWORD_FILE:-}
|
||||||
|
CUDY_SNAPSHOT_FILE: /run/cudy/state.json
|
||||||
|
volumes:
|
||||||
|
- ./cudy:/run/secrets/cudy:ro
|
||||||
|
- cudy_state:/run/cudy
|
||||||
|
networks:
|
||||||
|
- entrance
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
evening_detective_frontend:
|
evening_detective_frontend:
|
||||||
image: byjg/static-httpserver:latest
|
image: byjg/static-httpserver:latest
|
||||||
environment:
|
environment:
|
||||||
@@ -129,6 +148,7 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
cudy_state:
|
||||||
# Preserve existing data from the former Compose project.
|
# Preserve existing data from the former Compose project.
|
||||||
postgres_data:
|
postgres_data:
|
||||||
name: evening-detective-local_postgres_data
|
name: evening-detective-local_postgres_data
|
||||||
|
|||||||
Reference in New Issue
Block a user