339 lines
34 KiB
Markdown
339 lines
34 KiB
Markdown
# Вечерний детектив — приложение площадки
|
||
|
||
Статус: 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](PLAN.md). Очередь реализации: [TASKS.md](TASKS.md).
|
||
Эти документы находятся здесь по прямому указанию владельца — исключение из
|
||
общего правила хранения задач в `../docs/agent`. Не дублировать их там.
|
||
Идентификаторы `SCREEN-*` относятся только к этому приложению.
|
||
|
||
Программная часть SCREEN-007 реализована: обработка сбоев показа, пауза таймера,
|
||
предотвращение автоматического сна и восстановление пульта. SCREEN-007 вместе
|
||
с SCREEN-001/SCREEN-004 остаётся IN_PROGRESS до аппаратной приёмки.
|
||
[Инструкция проверки устойчивости](docs/SCREEN-007_ACCEPTANCE.md).
|
||
SCREEN-008: подготовлен кандидат **0.2.0-rc.1**. В `release/` собраны Linux
|
||
x64/ARM64 и Windows x64/ARM64; macOS ожидает сборки на Mac. Архивы проверяются
|
||
после распаковки, рядом сохраняются build-info и контрольные суммы. Старые архивы
|
||
0.1.0 — исторический стенд. Приложение пишет ограниченный локальный журнал
|
||
`userData/logs` с фиксированными событиями без секретов.
|
||
[Установка, обновление и приёмка](docs/SCREEN-008_ACCEPTANCE.md),
|
||
[фактические результаты и открытая матрица](docs/SCREEN-008_RESULTS.md).
|
||
SCREEN-008 остаётся IN_PROGRESS до сборок macOS и аппаратной приёмки.
|
||
|
||
В исходниках после упаковки кандидата согласованы названия режимов с сайтом,
|
||
переведены статусы игр и добавлен отдельный статус назначения звука сервером.
|
||
Игра для подключения управления явно указана рядом с кнопкой. Linux x64 0.2.0-rc.1
|
||
пересобран с этими изменениями, исправлениями ревизии и запуском под XWayland.
|
||
Остальные архивы ещё содержат прежний код; руководство описывает текущие исходники.
|
||
|
||
Новые задачи: `Новая задача: <описание>`.
|
||
Commit и push выполняет только пользователь. Вложенные `.git` не изменять.
|
||
|
||
## Документация
|
||
|
||
- Ведущему: [руководство пользователя](docs/USER_GUIDE.md) и
|
||
[чек-лист площадки](docs/EVENT_CHECKLIST.md).
|
||
- Проверяющему: [установка, обновление и общая приёмка](docs/SCREEN-008_ACCEPTANCE.md),
|
||
[фактические результаты и открытые проверки](docs/SCREEN-008_RESULTS.md).
|
||
- Разработчику: запуск и устройство приложения описаны ниже;
|
||
[план](PLAN.md), [активные задачи](TASKS.md), [завершённые задачи](COMPLETED_TASKS.md).
|
||
|
||
## Запуск проигрывателя
|
||
|
||
Нужны Node.js 24.15+ и ffmpeg с libx264 в PATH.
|
||
В Linux приложение использует X11; для сессии Wayland нужен XWayland, чтобы
|
||
размещать показ на выбранном мониторе.
|
||
Команды npm передают выбор X11 до старта Electron. Обычный запуск Linux-сборки
|
||
без этого флага автоматически перезапускает её с X11 до создания окон.
|
||
Поздний выбор X11 из main.js смешивал Wayland/X11 и оставлял пульт невидимым.
|
||
Отключать GPU или sandbox для запуска не нужно.
|
||
|
||
```sh
|
||
npm ci
|
||
npx --no-install install-electron --no
|
||
npm run media:generate
|
||
npm run dev
|
||
```
|
||
|
||
До запуска подключите второй дисплей в режиме расширенного рабочего стола
|
||
и назначьте экран компьютера основным. В блоке «Экран» выберите
|
||
внешний дисплей и нажмите «Открыть показ». Аудио и видео — синтетические локальные файлы длительностью
|
||
60 секунд; бинарники находятся в игнорируемом fixtures/ и не входят в Git.
|
||
В блоке «Сервер» доступны адрес и вход; в «Игра» — выбор игры и подготовка материалов.
|
||
|
||
Для постоянной связи войдите, выберите игру в серверном списке и нажмите
|
||
«Подключить». Готовый пакет заранее не требуется: сначала сохраняются
|
||
файлы текущего показа и трека, затем вся игра и связанные плейлисты.
|
||
Опрос продолжается при закрытом окне. Окно показа открывается вручную; при открытии
|
||
и восстановлении сети принимается актуальное состояние сервера, включая пуск медиа.
|
||
Новая сцена или музыка включается только после сохранения её файлов на диск;
|
||
до этого прежний канал продолжает работать. Онлайн-команды пульта общие с сервером,
|
||
офлайн-команды остаются локальными и не отправляются позднее.
|
||
[Проверка связи с существующим сервером](docs/SCREEN-006_ACCEPTANCE.md).
|
||
|
||
Токены хранятся в основном процессе и сохраняются средствами ОС отдельно от
|
||
пакетов. Если защищённое хранилище недоступно, вход действует только до закрытия
|
||
приложения. Выход не удаляет подготовленные материалы. Обновление выбранной
|
||
игры полной повторной загрузкой доступно при закрытом показе; проверка описания не замечает замену байтов
|
||
по прежнему URL — используйте повторную загрузку. Результаты доступны после
|
||
завершения игры; последний успешный снимок сохраняется с датой для офлайн-показа.
|
||
|
||
Проверки: npm run type-check, npm run lint, npm run lint:css, npm test, npm run build.
|
||
[Сборки и аппаратная приёмка](docs/SCREEN-001_ACCEPTANCE.md).
|
||
|
||
## Описание пакета 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):
|
||
|
||
```ts
|
||
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](docs/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](docs/SCREEN-005_ACCEPTANCE.md) описывает полный
|
||
офлайн-проход. Автоматический 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](docs/SCREEN-007_ACCEPTANCE.md) на всей матрице.
|
||
|
||
## Генерация клиента API
|
||
|
||
Приложение использует HTTP-клиент из канонического
|
||
`../evening_detective_server/api/main.proto`; сервер и frontend при генерации не меняются.
|
||
Сгенерированные методы и типы находятся в `electron/api/generated` и хранятся в Git.
|
||
Обычная сборка использует готовые файлы и не требует соседнего репозитория или генератора.
|
||
|
||
Для обновления нужны `protoc 36.0`, Go и тот же плагин, что во frontend:
|
||
|
||
```sh
|
||
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`; обращения к этим полям напрямую как к числам не допускаются.
|