# Вечерний детектив — приложение площадки Статус: программная часть SCREEN-004 реализована; SCREEN-001 и SCREEN-004 остаются IN_PROGRESS до аппаратной приёмки. 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-004; программная часть готова, аппаратная проверка открыта. Новые задачи: `Новая задача: <описание>`. Commit и push выполняет только пользователь. Вложенные `.git` не изменять. ## Запуск проигрывателя Нужны 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. Тестовый режим сохраняет отдельный выбор WAV/видео; игровые пакеты выбираются в разделе «Материалы показа». Полные игровые режимы, интерфейс загрузки игр и авторизация пока не реализованы. Управление телефоном через сервер, подключение игры и авторизация относятся к 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-`. Указатель `current.json` заменяется через временный файл и rename только после проверки. Отмена и ошибки до переключения не затрагивают прежний готовый пакет. После успеха прежняя версия удаляется; заблокированные ОС остатки повторно очищаются при следующем запуске. Сбой после переключения не отменяет новую готовую версию. Повреждённую текущую копию можно восстановить повторной загрузкой или удалить. Симлинки при чтении не обходятся; удаление ограничено каталогом выбранной игры. `screen-app://app/packages//` выдаёт только ресурсы готового пакета, с правильным 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-007. Для ручной проверки с настоящим внешним дисплеем выполните `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.