136 lines
12 KiB
Markdown
136 lines
12 KiB
Markdown
# Вечерний детектив — приложение площадки
|
||
|
||
Статус: SCREEN-001 в работе; технический стенд реализован, аппаратная приёмка открыта.
|
||
SCREEN-002 и SCREEN-003 завершены: описание пакета через существующий API,
|
||
загрузка, проверка, восстановление и локальная выдача файлов.
|
||
Управление с телефона идёт через существующий сервер сайта; desktop получает
|
||
обновления с сервера. Прямое LAN-подключение не используется.
|
||
|
||
Цель: ведущий управляет показом на компьютере с 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-001`.
|
||
Новые задачи: `Новая задача: <описание>`.
|
||
Commit и push выполняет только пользователь. Вложенные `.git` не изменять.
|
||
|
||
## Запуск стенда SCREEN-001
|
||
|
||
Нужны Node.js 24.15+ и ffmpeg с libx264 в PATH.
|
||
В Linux приложение использует X11; для сессии Wayland нужен XWayland, чтобы
|
||
размещать показ на выбранном мониторе.
|
||
|
||
```sh
|
||
npm ci
|
||
npx --no-install install-electron --no
|
||
npm run media:generate
|
||
npm run dev
|
||
```
|
||
|
||
До запуска подключите второй дисплей в режиме расширенного рабочего стола
|
||
и назначьте экран компьютера основным. В пульте выберите
|
||
его и откройте показ. Аудио и видео — синтетические локальные файлы длительностью
|
||
60 секунд; бинарники находятся в игнорируемом fixtures/ и не входят в Git.
|
||
Стенд поддерживает пуск с начала, паузу, остановку и перемотку. Полные игровые
|
||
режимы, интерфейс загрузки игр и авторизация пока не реализованы.
|
||
|
||
Управление телефоном через сервер, подключение игры и авторизация относятся к
|
||
SCREEN-006 и в техническом стенде ещё не реализованы. Интернет нужен для получения
|
||
команд телефона; локальное воспроизведение и пульт компьютера работают без него.
|
||
|
||
Проверки: 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; сейчас это программный
|
||
контракт, новых кнопок в стенде нет. Бэкенд, 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-пакет в отдельном временном userData, открывает его повторно через основной
|
||
процесс и проверяет Range и воспроизведение. Эти флаги используются только в тесте.
|
||
Серверных эталонных хешей нет: локальная проверка подтверждает целостность
|
||
скачанной копии, но не неизменность источников во время подготовки.
|