34 KiB
Вечерний детектив — приложение площадки
Статус: SCREEN-006 реализована — вход, список игр, загрузка через пульт, снимки результатов и команды существующего сервера. SCREEN-001 и SCREEN-004 остаются IN_PROGRESS до аппаратной приёмки. Подготовленная игра восстанавливается офлайн без автоматического открытия показа и запуска звука. Приложение экрана получает обновления через существующий сервер сайта.
SCREEN-012: постоянная синхронизация с сервером и автоматический локальный кеш материалов; установочные архивы эти изменения пока не содержат.
SCREEN-010 реализована: в пульте доступен редактор общего серверного плана экранов с локальной копией для офлайн-переходов. Подробности — в руководстве. Установочные архивы кандидата эти изменения пока не содержат.
Цель: ведущий управляет показом на компьютере с 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. В блоке «Сервер» доступны адрес и вход; в «Игра» — выбор игры и подготовка материалов.
Для постоянной связи войдите, выберите игру в серверном списке и нажмите «Подключить». Готовый пакет заранее не требуется: сначала сохраняются файлы текущего показа и трека, затем вся игра и связанные плейлисты. Опрос продолжается при закрытом окне. Окно показа открывается вручную; при открытии и восстановлении сети принимается актуальное состояние сервера, включая пуск медиа. Новая сцена или музыка включается только после сохранения её файлов на диск; до этого прежний канал продолжает работать. Онлайн-команды пульта общие с сервером, офлайн-команды остаются локальными и не отправляются позднее. Проверка связи с существующим сервером.
Токены хранятся в основном процессе и сохраняются средствами ОС отдельно от пакетов. Если защищённое хранилище недоступно, вход действует только до закрытия приложения. Выход не удаляет подготовленные материалы. Обновление выбранной игры полной повторной загрузкой доступно при закрытом показе; проверка описания не замечает замену байтов по прежнему 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
Подключите игру в блоке «Игра», выберите внешний дисплей и откройте показ. В активном шаге «Введение» или «Закрывашка» доступны управление воспроизведением, громкость, перемотка и статус основного канала. Файл определяется режимом шага; отдельного блока выбора медиа нет. Смена шага останавливает прежнее основное медиа. Музыка воспроизводится отдельным каналом без автоматического приглушения. Звук выводится только окном показа. Начальная громкость каждого канала — 100%; общий mute не меняет сохранённые уровни. При явном подключении игры звук назначается этому экрану, mute снимается, оба канала устанавливаются на 100%. Дальнейшие команды сервера имеют приоритет; автоматическое восстановление связи громкость не сбрасывает. В «План музыки» доступны плейлисты, загрузка MP3 до 64 МиБ и порядок треков ↑ / ↓. «Сохранить порядок» публикует перестановку, «Использовать в игре» назначает плейлист.
Кнопка «Запуск» меняется на «Пауза» при воспроизведении и «Продолжить» на паузе. «Сначала» запускает ресурс с нуля, «Стоп» останавливает и сбрасывает позицию. Перемотка сохраняет паузу/воспроизведение и ограничена реальной длительностью файла. Громкость не вызывает перемотку или повторный пуск. Следующий трек сохраняет состояние воспроизведения или паузы; по окончании трека переход выполняется локально. Повтор относится ко всему плейлисту; без повтора последний трек завершает воспроизведение. Смена плейлиста останавливает музыку.
Основной процесс хранит каталог разрешённых локальных ресурсов и состояния
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.
API и загрузка материалов используют сетевой стек Chromium (net.fetch Electron)
с системными настройками прокси. Собственный прокси приложение не запускает;
воспроизведение остаётся локальным. Ошибки DNS, подключения, TLS и прокси
показываются с безопасным кодом и контекстом запроса.
Пульт и режимы 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 на всей матрице.
Генерация клиента API
Приложение использует HTTP-клиент из канонического
../evening_detective_server/api/main.proto; сервер и frontend при генерации не меняются.
Сгенерированные методы и типы находятся в electron/api/generated и хранятся в Git.
Обычная сборка использует готовые файлы и не требует соседнего репозитория или генератора.
Для обновления нужны protoc 36.0, Go и тот же плагин, что во frontend:
go install go.einride.tech/protoc-gen-typescript-http@v0.10.0
npm run proto:gen
Добавьте каталог Go bin в PATH. Команда проверяет версии и пишет только в screen;
повторная генерация при неизменном proto должна давать тот же результат.
Генерируемые файлы не редактировать и не форматировать вручную.
Транспорт остаётся в основном процессе Electron: авторизация, обновление токена,
отмена, таймауты и безопасная диагностика общие для вызовов клиента. Медиа скачиваются
отдельно и воспроизводятся с локального диска. Генератор задаёт методы и TypeScript-типы,
но не проверяет и не преобразует JSON. В частности, его декларация number для int64
не меняет строковый формат gateway: входящие версии проходят проверку и сравниваются
через BigInt; обращения к этим полям напрямую как к числам не допускаются.