Вечерний детектив — приложение площадки
Статус: 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. Очередь реализации: TASKS.md.
Эти документы находятся здесь по прямому указанию владельца — исключение из
общего правила хранения задач в ../docs/agent. Не дублировать их там.
Идентификаторы SCREEN-* относятся только к этому приложению.
Для начала: Выполнить задачу: SCREEN-001.
Новые задачи: Новая задача: <описание>.
Commit и push выполняет только пользователь. Вложенные .git не изменять.
Запуск стенда SCREEN-001
Нужны Node.js 24.15+ и ffmpeg с libx264 в PATH. В Linux приложение использует X11; для сессии Wayland нужен XWayland, чтобы размещать показ на выбранном мониторе.
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. Сборки и аппаратная приёмка.
Описание пакета 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):
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 и воспроизведение. Эти флаги используются только в тесте.
Серверных эталонных хешей нет: локальная проверка подтверждает целостность
скачанной копии, но не неизменность источников во время подготовки.