30 KiB
Вечерний детектив — приложение площадки
Статус: SCREEN-006 реализована — вход, список игр, загрузка через пульт, снимки результатов и команды существующего сервера. SCREEN-001 и SCREEN-004 остаются IN_PROGRESS до аппаратной приёмки. Подготовленная игра восстанавливается офлайн без автоматического открытия показа и запуска звука. Управление с телефона идёт через существующий сервер сайта; прямого LAN-подключения нет.
Цель: ведущий управляет показом на компьютере с macOS, Windows или Linux, а подключённый монитор или проектор показывает материалы игрокам. Все материалы показа заранее находятся на диске; локальная команда не ждёт сеть или ответ сервера.
Стек: Electron + Vue + TypeScript + Vite. Первая версия включает шесть сборок: macOS Intel/Apple Silicon, Windows x64/ARM64 и Linux x64/ARM64. Поддерживаемые версии ОС и условия проверки описаны в PLAN.md. Использовать текущие паттерны фронтенда и переиспользовать подходящие компоненты, не переносить весь сайт в приложение и не выделять общий пакет заранее.
План: PLAN.md. Очередь реализации: TASKS.md.
Эти документы находятся здесь по прямому указанию владельца — исключение из
общего правила хранения задач в ../docs/agent. Не дублировать их там.
Идентификаторы SCREEN-* относятся только к этому приложению.
Программная часть SCREEN-007 реализована: обработка сбоев показа, пауза таймера,
предотвращение автоматического сна и восстановление пульта. SCREEN-007 вместе
с SCREEN-001/SCREEN-004 остаётся IN_PROGRESS до аппаратной приёмки.
Инструкция проверки устойчивости.
SCREEN-008: подготовлен кандидат 0.2.0-rc.1. В release/ собраны Linux
x64/ARM64 и Windows x64/ARM64; macOS ожидает сборки на Mac. Архивы проверяются
после распаковки, рядом сохраняются build-info и контрольные суммы. Старые архивы
0.1.0 — исторический стенд. Приложение пишет ограниченный локальный журнал
userData/logs с фиксированными событиями без секретов.
Установка, обновление и приёмка,
фактические результаты и открытая матрица.
SCREEN-008 остаётся IN_PROGRESS до сборок macOS и аппаратной приёмки.
В исходниках после упаковки кандидата согласованы названия режимов с сайтом, переведены статусы игр и добавлен отдельный статус назначения звука сервером. Игра для подключения управления явно указана рядом с кнопкой. Linux x64 0.2.0-rc.1 пересобран с этими изменениями, исправлениями ревизии и запуском под XWayland. Остальные архивы ещё содержат прежний код; руководство описывает текущие исходники.
Новые задачи: Новая задача: <описание>.
Commit и push выполняет только пользователь. Вложенные .git не изменять.
Документация
- Ведущему: руководство пользователя и чек-лист площадки.
- Проверяющему: установка, обновление и общая приёмка, фактические результаты и открытые проверки.
- Разработчику: запуск и устройство приложения описаны ниже; план, активные задачи, завершённые задачи.
Запуск проигрывателя
Нужны Node.js 24.15+ и ffmpeg с libx264 в PATH. В Linux приложение использует X11; для сессии Wayland нужен XWayland, чтобы размещать показ на выбранном мониторе. Команды npm передают выбор X11 до старта Electron. Обычный запуск Linux-сборки без этого флага автоматически перезапускает её с X11 до создания окон. Поздний выбор X11 из main.js смешивал Wayland/X11 и оставлял пульт невидимым. Отключать GPU или sandbox для запуска не нужно.
npm ci
npx --no-install install-electron --no
npm run media:generate
npm run dev
До запуска подключите второй дисплей в режиме расширенного рабочего стола и назначьте экран компьютера основным. В блоке «Дисплей показа» выберите внешний дисплей и нажмите «Открыть показ». Аудио и видео — синтетические локальные файлы длительностью 60 секунд; бинарники находятся в игнорируемом fixtures/ и не входят в Git. Тестовый режим сохраняет отдельный выбор WAV/видео; игровые пакеты выбираются в разделе «Материалы показа». В блоке «Игра и сервер» доступны вход, список игр и загрузка материалов.
Для команд телефона войдите, подготовьте и выберите игру, подключите управление и откройте показ. Первое состояние не запускает накопленные команды; отправьте новую команду на сайте. После обрыва сети локальный пульт продолжает работать, первое состояние при восстановлении также пропускается. Проверка с существующим сервером и телефоном.
Токены хранятся в основном процессе и сохраняются средствами ОС отдельно от пакетов. Если защищённое хранилище недоступно, вход действует только до закрытия приложения. Выход не удаляет подготовленные материалы. Обновление выбранной игры доступно при закрытом показе; проверка описания не замечает замену байтов по прежнему URL — используйте повторную загрузку. Результаты доступны после завершения игры; последний успешный снимок сохраняется с датой для офлайн-показа.
Проверки: npm run type-check, npm run lint, npm run lint:css, npm test, npm run build. Сборки и аппаратная приёмка.
Описание пакета SCREEN-002
electron/screen-package.ts экспортирует prepareScreenPackage({ server, gameId, token, signal? }) для вызова из основного процесса. Метод делает три GET-запроса:
/api/games/{ID}, /api/games/{ID}/screen-plan, /api/games/{ID}/music.
Авторизация — существующий Bearer JWT; токен используется только в запросах API,
не входит в результат. Редиректы API запрещены, ожидание ограничено 30 секундами;
поддерживается отмена через AbortSignal. Ответы с ошибками не выдают частичный пакет.
Интерфейс входа и загрузки подключён в SCREEN-006; описание используется основным
процессом без передачи токенов в renderer. Бэкенд, frontend и protobuf не изменены.
Результат ScreenPackage содержит formatVersion: 1, metadataVersion, адрес
сервера, ID/название игры, ссылки на материалы, шаги плана и связанные плейлисты.
assets содержит уникальные URL и их идентификаторы (SHA-256 адреса, не файла).
В media и треках используются эти идентификаторы. Фон по умолчанию — обложка.
Плейлисты сценария, игры и текущий выбранный включаются со всеми треками; прочая
библиотека, данные участников и живые команды не сохраняются. Ссылки с секретами
в параметрах и адреса с логином/паролем отклоняются.
metadataVersion — SHA-256 нормализованных метаданных. Он не замечает замену
байтов по прежнему URL. Само описание не означает готовность к показу. В SCREEN-003 размер и SHA-256
вычисляются при скачивании, после чего формируется итоговая версия и пакет
активируется атомарно. Внешним медиа нельзя передавать JWT сервера.
Существующий API не предоставляет атомарного снимка или серверных хешей файлов;
права доступа к играм и публичная выдача файлов остаются прежними.
Локальное хранилище SCREEN-003
Основной процесс открывает PackageStore из electron/package-store.ts в
app.getPath('userData')/packages. Приложение допускает только один экземпляр,
чтобы две копии не изменяли хранилище одновременно. При запуске проверяются
манифесты, размеры и SHA-256 всех файлов; повреждённые пакеты не становятся готовыми.
Программный интерфейс основного процесса (подключён к UI в SCREEN-006):
const store = new PackageStore(packagesDirectory)
await store.initialize()
const description = await prepareScreenPackage({ server, gameId, token })
const ready = await store.download(description, { signal, onProgress })
// Другие операции: list(), cancel(), verify(key), remove(key), localFile(key, assetId).
Скачивание последовательно и потоково. Файл синхронизируется с диском и повторно
читается для проверки хеша. При обрыве текущий файл повторяется целиком: максимум
три попытки с паузами 1 и 3 секунды; завершённые файлы текущей попытки не повторяются.
Таймаут бездействия — 30 секунд. Ошибки 408/429/5xx повторяются; ошибки доступа,
неизвестные форматы и отсутствующие файлы останавливают подготовку. Незнакомый
общий размер в прогрессе обозначается totalBytes: null; итоговый размер известен
после загрузки. Поддерживаются MIME WAV/OGG, используемые текущим сервером.
Медиа запрашиваются без JWT и cookies. Разрешено до пяти проверенных HTTP(S) редиректов; запрещены учётные данные в URL, секретные параметры и понижение HTTPS до HTTP. Контроль места использует statfs перед загрузкой и перед записью очередного блока, оставляя резерв 64 МиБ; фактическая ошибка ENOSPC также обрабатывается.
Каталог игры определяется хешем адреса сервера и ID игры; пути ресурсов — хешами URL, независимо от исходного имени файла. Локальный манифест содержит исходное описание, MIME, размеры и SHA-256 байтов; итоговая версия зависит от метаданных и проверенных файлов. Секреты в манифест не записываются.
Новая версия готовится в staging-*, затем помещается в v-<version>. Указатель
current.json заменяется через временный файл и rename только после проверки.
Отмена и ошибки до переключения не затрагивают прежний готовый пакет. После
успеха прежняя версия удаляется; заблокированные ОС остатки повторно очищаются
при следующем запуске. Сбой после переключения не отменяет новую готовую версию.
Повреждённую текущую копию можно восстановить повторной загрузкой или удалить.
Симлинки при чтении не обходятся; удаление ограничено каталогом выбранной игры.
screen-app://app/packages/<key>/<assetId> выдаёт только ресурсы готового пакета,
с правильным MIME, запретом кеширования и поддержкой Range. Произвольные пути
и сетевые URL в протокол не передаются. Воспроизведение игровых пакетов подключено
в SCREEN-004; fixture-файлы доступны отдельно в тестовом режиме.
npm test включает настоящий временный HTTP-сервер: после скачивания сервер
отключается, новое хранилище проверяет пакет, локальная выдача читает все ресурсы.
npm run smoke -- --no-sandbox --mute-audio --disable-gpu дополнительно готовит
пакет с WAV, H.264 и двумя музыкальными треками в отдельном временном userData,
открывает его повторно через основной процесс и проверяет пульт, Range, команды
и переходы плейлиста. Эти флаги используются только в тесте.
Серверных эталонных хешей нет: локальная проверка подтверждает целостность
скачанной копии, но не неизменность источников во время подготовки.
Локальный проигрыватель SCREEN-004
На пульте выберите готовый пакет или «Тестовый режим SCREEN-001», внешний дисплей и откройте показ. В основном канале доступны вступление и заключительное видео; их выбор останавливает прежнее основное медиа. Музыка воспроизводится отдельным каналом без автоматического приглушения. Звук выводится только окном показа. Начальная громкость каждого канала — 35%; общий mute не меняет сохранённые уровни.
«Пуск с начала» и «Повторить с начала» запускают выбранный ресурс с нуля, «Пауза» сохраняет фактическую позицию, «Продолжить» возобновляет её, «Стоп» сбрасывает позицию. Перемотка сохраняет паузу/воспроизведение и ограничена реальной длительностью файла. Громкость не вызывает перемотку или повторный пуск. Следующий трек сохраняет состояние воспроизведения или паузы; по окончании трека переход выполняется локально. Повтор относится ко всему плейлисту; без повтора последний трек завершает воспроизведение. Смена плейлиста останавливает музыку.
Основной процесс хранит каталог разрешённых локальных ресурсов и состояния
main/music; renderer получает только адреса screen-app. Команды IPC содержат
сессию и возрастающий sequence; повторные/устаревшие номера игнорируются.
Подтверждения содержат сессию, канал и operation, чтобы запоздавшие события
старого ресурса не меняли новый. Позиция обновляется по событиям медиа; подготовка,
успешный play и ошибки подтверждаются проигрывателем. Подготовлены выбранное
основное медиа и один следующий трек, без чтения всех видео в JavaScript.
screenBridge.packages() и selectPackage(key) доступны только пульту;
selectPackage(null) включает тестовый режим. Выбор повторно проверяет пакет.
Закрытие или пересоздание окна и смена пакета создают новую сессию и останавливают
каналы; повторное открытие не возобновляет звук. После перезапуска всего приложения
готовый выбранный пакет восстанавливается механизмом SCREEN-006 без автопуска.
Для ручной проверки с настоящим внешним дисплеем выполните npm run acceptance.
Команда использует синтетические fixtures и отдельный временный userData, создаёт
локальный пакет с двумя тональными музыкальными треками и оставляет пульт открытым
без автоматических команд. Подготовка не требует сети после установки зависимостей.
При отсутствии внешнего дисплея команда завершится с сообщением, синтетический
монитор в ручном режиме не подставляется. Встроенная аппаратная инструкция:
SCREEN-004_ACCEPTANCE.md.
Проверки test и smoke запускайте последовательно: обе команды пересобирают
dist-electron, из которого Electron загружает preload при открытии окна.
Программные замеры в пульте разделены по каналу и текущему виду медиа; они
не измеряют физический звук/первый кадр. Аппаратная приёмка, остальные ОС,
реальные игровые материалы и p95 на M1 пока не подтверждены. Все восемь визуальных режимов доступны в SCREEN-005; авторизация и получение
серверных команд остаются в SCREEN-006.
Пульт и режимы SCREEN-005
Пульт включает готовый пакет, план шагов, переключатель восьми режимов, текст, таймер, предпросмотр и прежние независимые каналы медиа/музыки. План из пакета доступен только для чтения. Выбор шага применяет его параметры без автоматического запуска звука/таймера; окончание медиа не переключает шаг. Пуск основного ресурса показывает соответствующий режим вступления или видео. Уход в другую сцену останавливает основной канал, сохраняя независимую музыку.
Текст и параметры таймера редактируются как черновик; только «Показать текст» / «Показать таймер» публикуют их. Текст отображается как обычный текст, без HTML. Таймер вычисляется в основном процессе по монотонному deadline; интервалы только обновляют снимок. Пауза сохраняет точный остаток, сброс возвращает полную длительность. Таймер продолжает идти за другой сценой и при закрытом окне; смена пакета сбрасывает его. По окончании остаётся 00:00, без сигнала и смены режима. С SCREEN-007 аварийное закрытие (дисплей, сон ОС, fullscreen или renderer) ставит работающий таймер на паузу с точным остатком. После пробуждения требуется вручную открыть показ и продолжить отсчёт; перезапуск приложения не возобновляет таймер.
Обложка и фон читаются через локальный протокол. Новая подготовка использует адрес с идентификатором сцены, исключая устаревший декодированный кеш изображения. Окно подтверждает готовность по загрузке изображений/медиа; запоздалые подтверждения прошлой сцены/сессии игнорируются. Сообщения об ошибках доступны только пульту. На проекторе остаются материалы игры и предусмотренные состояния отсутствия необязательного вступления/видео/результатов, без кнопок и диагностических текстов.
Предпросмотр — кадры настоящего окна через capturePage, максимум два в секунду, шириной не более 960 px. В пульте нет второго audio/video. Захват недоступен окну показа, при закрытом проекторе/скрытом или свёрнутом пульте прекращается; старые кадры другой сцены/сессии не принимаются. Хранится только последний кадр. Системные шрифты с кириллицей не требуют сети.
Controller.setResults(snapshot) принимает снимок текущей игры: packageKey,
capturedAt, вопросы и публичные строки таблицы (ID/название команды, место,
сумма, ходы и баллы ответов). Лишние поля отбрасываются, другой ключ игры,
повторные команды/вопросы и некорректные значения отклоняются. На пульте указан
момент снимка. При его отсутствии показывается «Результаты пока не загружены»;
фиктивные результаты в приложении не создаются. Длинная таблица прокручивается
снизу вверх, обновления позиций медиа её не перезапускают. Сетевое получение и
сохранение снимка реализуются в SCREEN-006; публичного IPC редактирования результатов нет.
Повторный снимок с той же таблицей обновляет дату без перезапуска прокрутки;
изменение вопросов или строк таблицы начинает показ результатов заново.
npm run acceptance теперь создаёт локальный пакет с обложкой, фоном и восемью
шагами; инструкция SCREEN-005 описывает полный
офлайн-проход. Автоматический smoke проверяет также повреждение/восстановление
изображения, отсутствие второго звука, предел частоты кадров предпросмотра,
кириллицу и отрисовку/прокрутку 50 синтетических строк результатов. Эти строки
подаются только в тестовый renderer и не сохраняются в состоянии приложения.
Устойчивость SCREEN-007
Отключение выбранного дисплея, превращение его в основной, смена разрешения, масштаба/ориентации, выход из fullscreen, сон ОС, падение или зависание renderer закрывают показ и останавливают оба медиаканала. Причина видна в пульте; возврат устройства не запускает окно и звук. Для повторного вывода нажмите «Открыть показ». Пульт и показ используют разные сессии Electron; падение renderer показа обрабатывается независимо от пульта. Выбор аудиовыхода остаётся в настройках ОС.
При открытом показе удерживается prevent-display-sleep, включая статичные сцены и паузу. Эта блокировка не отменяет принудительный сон или закрытие крышки. Пульт сохраняет bounds/maximized в userData/windows.json и восстанавливается в пределах основного экрана; внешний дисплей предвыбирается только по совпадающим ID и названию. Ошибка файла настроек не удаляет материалы или секреты.
GUI smoke на стенде без внешнего дисплея моделирует topology/fullscreen/питание; native fullscreen проверяется при наличии реального второго экрана. Реальное предотвращение сна, маршрутизацию звука ОС и физическое переподключение оборудования проверять по инструкции SCREEN-007 на всей матрице.