diff --git a/README.md b/README.md
index 6369731..0071ed5 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,12 @@ x64/ARM64 и Windows x64/ARM64; macOS ожидает сборки на Mac. Ар
[фактические результаты и открытая матрица](docs/SCREEN-008_RESULTS.md).
SCREEN-008 остаётся IN_PROGRESS до сборок macOS и аппаратной приёмки.
+В исходниках после упаковки кандидата согласованы названия режимов с сайтом,
+переведены статусы игр и добавлен отдельный статус назначения звука сервером.
+Игра для подключения управления явно указана рядом с кнопкой. Архивы 0.2.0-rc.1
+не пересобирались и пока не содержат этих исправлений; руководство описывает
+текущие исходники. Для проверки изменений используйте запуск из исходников ниже.
+
Новые задачи: `Новая задача: <описание>`.
Commit и push выполняет только пользователь. Вложенные `.git` не изменять.
diff --git a/TASKS.md b/TASKS.md
index 3422107..9c80195 100644
--- a/TASKS.md
+++ b/TASKS.md
@@ -207,6 +207,24 @@ SCREEN-009 (LAN-пульт) отменена владельцем; ID не пе
по ролям и однозначный выбор внешнего дисплея. Названия кнопок и поведение
сверены с текущим UI; относительные ссылки и git diff --check — PASS.
Код не менялся, новые аппаратные испытания не выполнялись.
+- Согласование интерфейса с сервером и сайтом (2026-10-17): названия режимов
+ приведены к сайту («Введение», «Успехов», «Закрывашка», «Спасибо за игру»),
+ статусы игр переведены. Рядом с подключением указано имя локальной игры;
+ выбор серверной игры для загрузки её не меняет. Добавлено пояснение, что
+ отключение управления не удаляет серверную привязку.
+- Назначение звука: OnlineState.soundEnabled (boolean | null) содержит последнее
+ подтверждение сервера отдельно от локального mute. До подтверждения неизвестно,
+ без опроса помечается как последнее состояние; при новой сессии/отключении
+ сбрасывается. Отрицательное значение означает «Экран не назначен для звука»:
+ наличие другого назначенного экрана серверный флаг не доказывает.
+- Проверки согласования: type-check, ESLint, Stylelint, 74 теста, сборка и GUI smoke —
+ PASS. GUI проверил переведённый статус, независимые выборы игр и статус звука;
+ юнит-тест — смену назначения, mute, обрыв связи и сброс сессии. Два ранних
+ GUI-прогона остановились на селекторе нового теста; исправлен выбор видимого
+ элемента Naive UI, итоговый прогон PASS. Ссылки документации (28) и
+ git diff --check — PASS. Руководство и чек-лист обновлены. Архивы кандидата
+ не пересобирались; отличие текущих исходников от архивов явно описано в README.
+ Бэкенд, сайт, protobuf и зависимости не менялись.
- Открыто: получить macOS x64/arm64 через workflow после commit/push владельцем;
выполнить аппаратную матрицу, включая оба Linux-дистрибутива, час офлайн,
проектор/звук/телефон и физические замеры на M1. Статус IN_PROGRESS до получения
diff --git a/docs/EVENT_CHECKLIST.md b/docs/EVENT_CHECKLIST.md
index 5bc1447..7d691f6 100644
--- a/docs/EVENT_CHECKLIST.md
+++ b/docs/EVENT_CHECKLIST.md
@@ -22,8 +22,10 @@
- [ ] В ОС выбран нужный аудиовыход. Пробный звук слышен физически, mute выключен,
громкость основного канала и музыки подходит для помещения.
- [ ] После подключения HDMI аудиовыход проверен повторно.
-- [ ] Для телефона подключено управление выбранной игрой, проверен номер экрана,
- связь с сервером и новая команда с сайта. Звук назначен нужному экрану.
+- [ ] Перед подключением телефона проверено название в строке «Игра для управления»;
+ подключено управление этой игрой, проверен номер экрана,
+ связь с сервером и новая команда с сайта. Проверено назначение звука экрану
+ и отдельно локальное выключение звука.
- [ ] Перед стартом выбрана нужная сцена, музыка и таймер в ожидаемом состоянии.
- [ ] Компьютер подключён к питанию; крышка остаётся открытой.
diff --git a/docs/SCREEN-006_ACCEPTANCE.md b/docs/SCREEN-006_ACCEPTANCE.md
index fd352c3..a258809 100644
--- a/docs/SCREEN-006_ACCEPTANCE.md
+++ b/docs/SCREEN-006_ACCEPTANCE.md
@@ -19,7 +19,7 @@
## Команды с телефона
-1. Нажать «Подключить управление выбранной игрой», записать номер экрана.
+1. Нажать «Подключить управление этой игрой», записать номер экрана.
Открыть показ на внешнем дисплее. Дождаться сообщения о связи.
2. На телефоне открыть существующую страницу управления этой игрой.
Убедиться, что экран появился в её списке. Назначить звук этому экрану
diff --git a/docs/SCREEN-008_RESULTS.md b/docs/SCREEN-008_RESULTS.md
index 5cd1304..bff7150 100644
--- a/docs/SCREEN-008_RESULTS.md
+++ b/docs/SCREEN-008_RESULTS.md
@@ -64,6 +64,25 @@ macOS нет. Этот стенд не подтверждает Ubuntu/Debian,
Физический звук, час офлайн, телефон, сон, переподключение и замеры не выполнены.
Этот Manjaro-стенд не заменяет Ubuntu/Debian из обязательной матрицы.
+## Проверка согласования интерфейса (2026-10-17)
+
+Проверены текущие исходники после упаковки кандидата. Архивы 0.2.0-rc.1
+не пересобирались и не включают следующие изменения интерфейса.
+
+- Названия восьми режимов согласованы с сайтом, статусы игр показаны по-русски.
+- Подключение явно указывает локально выбранную игру, независимо от игры
+ в серверном списке загрузки; отключение не обещает отвязку на сервере.
+- Отдельно отображается подтверждённое сервером назначение звука.
+ Локальный mute его не изменяет; отсутствие текущего опроса явно обозначено.
+- Type-check, ESLint, Stylelint, 74 теста, сборка, GUI smoke — PASS.
+ Новый GUI-сценарий выбирает серверную игру для подготовки при другом локальном
+ пакете, проверяет неизменность цели управления и перевод статуса. После выбора
+ нового локального пакета цель меняется; серверное назначение звука видно в UI.
+- Проверены 28 ссылок документации и git diff --check — PASS.
+
+Прежние ограничения GUI-стенда сохраняются: Linux x64, синтетический внешний
+дисплей, тестовые no-sandbox/mute-audio/disable-gpu. Новых аппаратных результатов нет.
+
## Отчёт одного стенда
- Дата, версия приложения и SHA-256 архива:
diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md
index f5a28a5..5780a91 100644
--- a/docs/USER_GUIDE.md
+++ b/docs/USER_GUIDE.md
@@ -5,6 +5,10 @@
Телефон отправляет команды через существующий сервер сайта; без сети
управляйте показом с компьютера.
+Руководство описывает текущие исходники. Ранее собранные архивы 0.2.0-rc.1
+ещё содержат прежние подписи режимов и кнопки подключения и не отображают
+новый отдельный статус назначения звука. Для этих исправлений нужна новая сборка.
+
Перед мероприятием используйте [чек-лист площадки](EVENT_CHECKLIST.md).
## Версия и установка
@@ -58,6 +62,7 @@ Windows 11, macOS 13+, Ubuntu 24.04 LTS или Debian 13. Эти требова
используйте «Отменить загрузку». Не выезжайте с незавершённой загрузкой.
4. В отдельном блоке **«Материалы показа» выберите подготовленную игру**.
Загрузка и выбор игры на сервере сами по себе не выбирают пакет для показа.
+ Статусы игр отображаются как «Черновик», «Идёт», «Пауза», «Завершена».
Сообщение «Тестовый режим: синтетические аудио и видео» означает, что игровая
копия не выбрана.
5. Откройте показ, проверьте изображения, вступление, видео и плейлисты.
@@ -78,7 +83,7 @@ Windows 11, macOS 13+, Ubuntu 24.04 LTS или Debian 13. Эти требова
В «План показа» выберите шаг, используйте «Предыдущий шаг» / «Следующий шаг»
либо переключатель «Режим показа». Доступны восемь режимов: «Обложка»,
-«Вступление», «Завершение вступления», «Заключительное видео», «Завершение показа»,
+«Введение», «Успехов», «Закрывашка», «Спасибо за игру»,
«Результаты», «Таймер» и «Текст». Переход к шагу или режиму сам не запускает
медиа и таймер. Дождитесь готовности; отправленная команда ещё не означает
успешного воспроизведения — смотрите статус и фактическое изображение/звук.
@@ -123,19 +128,25 @@ Windows 11, macOS 13+, Ubuntu 24.04 LTS или Debian 13. Эти требова
## Управление с телефона
1. Войдите и выберите готовую игру в «Материалы показа».
-2. Нажмите «Подключить управление выбранной игрой»; запишите показанный номер экрана.
+2. Проверьте строку «Игра для управления»: это название локально выбранного
+ пакета, независимо от игры в списке загрузки выше. Нажмите «Подключить управление
+ этой игрой»; запишите показанный номер экрана.
Откройте показ и дождитесь «Связь с сервером есть».
3. На телефоне откройте существующую страницу управления этой игрой на сайте.
При необходимости назначьте звук экрану через сайт или кнопку
«Назначить звук этому экрану». Это отключает звук других экранов игры
- по существующим правилам сервера.
+ по существующим правилам сервера. Статус назначения звука показывается отдельно
+ от локального переключателя «Выключить весь звук». До подтверждения сервером
+ назначение неизвестно; при закрытом показе или потере связи отображается
+ последнее подтверждённое состояние.
4. Отправьте новую команду. Первое состояние после подключения, открытия окна
или восстановления связи не повторяет старые команды.
Опрос сервера идёт раз в секунду при открытом показе. Быстрые команды между
опросами могут объединяться. Локальные команды компьютера действуют только
на его показ и не рассылаются другим экранам. Новая команда телефона может
-изменить локальный показ. «Отключить управление» прекращает серверное управление.
+изменить локальный показ. «Отключить управление» прекращает управление приложением,
+но сохраняет привязку экрана на сервере. Для отвязки используйте сайт.
При отсутствии интернета пользуйтесь локальным пультом; прямого подключения
телефона к приложению по LAN нет.
diff --git a/electron/online.test.ts b/electron/online.test.ts
index 823509c..825cdb4 100644
--- a/electron/online.test.ts
+++ b/electron/online.test.ts
@@ -541,3 +541,38 @@ test('вход без прав управления не сохраняет то
code: 'ENOENT',
})
})
+
+test('назначение звука подтверждается сервером отдельно от локального mute и сбрасывается с сессией', async (t) => {
+ const f = await fixture(t)
+ await f.prepare()
+ assert.equal(f.online.state.soundEnabled, null)
+ f.controller.output(true)
+ f.model.remote.soundEnabled = false
+ await f.online.poll()
+ assert.equal(f.online.state.soundEnabled, false)
+ send(f.controller, { action: 'mute', enabled: false })
+ assert.equal(f.online.state.soundEnabled, false)
+ await f.online.act({ action: 'sound' })
+ assert.equal(f.online.state.soundEnabled, true)
+ f.model.remote.soundEnabled = true
+ await f.online.poll()
+ send(f.controller, { action: 'mute', enabled: true })
+ assert.equal(f.online.state.soundEnabled, true)
+ f.model.offline = true
+ await f.online.poll()
+ assert.equal(f.online.state.connection, 'offline')
+ assert.equal(f.online.state.soundEnabled, true)
+ f.model.offline = false
+ f.model.remote.soundEnabled = false
+ await f.online.poll()
+ assert.equal(f.online.state.soundEnabled, false)
+ await f.online.act({ action: 'connect' })
+ assert.equal(f.online.state.soundEnabled, null)
+ await f.online.poll()
+ await f.online.act({ action: 'disconnect' })
+ assert.equal(f.online.state.soundEnabled, null)
+ assert.equal(
+ f.requests.some((r) => r.path.endsWith('/unbind')),
+ false,
+ )
+})
diff --git a/electron/online.ts b/electron/online.ts
index e7b796e..bd285d4 100644
--- a/electron/online.ts
+++ b/electron/online.ts
@@ -73,6 +73,7 @@ export class Online {
canCancel: false,
games: [],
screenId: null,
+ soundEnabled: null,
connection: 'disabled',
error: '',
notice: '',
@@ -220,6 +221,7 @@ export class Online {
this.generation++
this.screen = null
this.previous = null
+ this.state.soundEnabled = null
this.state.screenId = null
this.state.connection = 'disabled'
}
@@ -489,13 +491,16 @@ export class Online {
this.detach()
await this.persistSecrets()
return
- case 'sound':
+ case 'sound': {
+ const generation = this.generation
if (!this.screen) throw new Error('Сначала подключите управление')
await this.api(`/api/screens/${this.screen.id}/sound`, {
screenID: this.screen.id,
soundEnabled: true,
})
+ if (generation === this.generation) this.state.soundEnabled = true
return
+ }
case 'results':
return this.refreshResults()
default:
@@ -616,6 +621,7 @@ export class Online {
if (previous?.music && next.music && next.music.version < previous.music.version)
next = { ...next, music: previous.music }
this.previous = next
+ this.state.soundEnabled = next.sound
if (this.state.connection === 'offline') this.state.error = ''
this.state.connection = 'connected'
if (!previous && this.initialSound) {
diff --git a/scripts/online-smoke.cjs b/scripts/online-smoke.cjs
index 2f55e9c..09e128d 100644
--- a/scripts/online-smoke.cjs
+++ b/scripts/online-smoke.cjs
@@ -106,17 +106,60 @@ module.exports = async function onlineSmoke({
async () => !(await evaluate('window.screenBridge.onlineState()')).busy,
'login complete',
)
+ await poll(
+ () =>
+ evaluate(
+ `Array.from(document.querySelectorAll('.n-base-selection')).some(e => e.textContent.includes('Выберите игру для подготовки'))`,
+ ),
+ 'server game selector',
+ )
+ await evaluate(
+ `Array.from(document.querySelectorAll('.n-base-selection')).find(e => e.textContent.includes('Выберите игру для подготовки')).click()`,
+ )
+ await poll(
+ () =>
+ evaluate(
+ `Array.from(document.querySelectorAll('.n-base-select-option')).some(e => e.textContent.includes('HTTP smoke · Завершена'))`,
+ ),
+ 'translated game status',
+ )
+ await evaluate(
+ `Array.from(document.querySelectorAll('.n-base-select-option')).find(e => e.textContent.includes('HTTP smoke · Завершена')).click()`,
+ )
+ const selectedBeforeDownload = (await state()).catalog.name
+ await poll(
+ () =>
+ evaluate(
+ `document.querySelector('[data-testid="control-game"]')?.textContent.includes(${JSON.stringify(selectedBeforeDownload)})`,
+ ),
+ 'control target stays on local package despite server game selection',
+ )
+ assert.notEqual(selectedBeforeDownload, 'HTTP smoke')
await act({ action: 'download', gameId: 6 })
const packages = await evaluate('window.screenBridge.packages()')
const pkg = packages.find((p) => p.name === 'HTTP smoke')
assert.ok(pkg)
await evaluate(`window.screenBridge.selectPackage(${JSON.stringify(pkg.key)})`)
+ await poll(
+ () =>
+ evaluate(
+ `document.querySelector('[data-testid="control-game"]')?.textContent.includes('HTTP smoke')`,
+ ),
+ 'control target follows selected local package',
+ )
await act({ action: 'connect' })
await evaluate(`window.screenBridge.show(${displayId})`)
await poll(
async () => (await evaluate('window.screenBridge.onlineState()')).connection === 'connected',
'HTTP baseline',
)
+ await poll(
+ () =>
+ evaluate(
+ `document.querySelector('[data-testid="sound-assignment"]')?.textContent.includes('Экран назначен для звука')`,
+ ),
+ 'server sound assignment shown in UI',
+ )
assert.equal((await state()).channels.main.desired, 'stopped')
version++
await poll(
diff --git a/src/components/ConnectionPanel.vue b/src/components/ConnectionPanel.vue
index c3df6c1..9df7621 100644
--- a/src/components/ConnectionPanel.vue
+++ b/src/components/ConnectionPanel.vue
@@ -3,7 +3,7 @@ import { NAlert, NButton, NCard, NInput, NSelect, NSpace, useMessage } from 'nai
import { onMounted, onUnmounted, ref } from 'vue'
import type { OnlineIntent, OnlineState } from '../types/online'
-defineProps<{ outputOpen: boolean; selectedKey: string | null }>()
+defineProps<{ outputOpen: boolean; selectedKey: string | null; selectedName: string | null }>()
const emit = defineEmits<{ packagesChanged: [] }>()
const bridge = window.screenBridge
const message = useMessage()
@@ -16,6 +16,7 @@ const state = ref
Выбор игры выше используется для загрузки материалов.
+Игра для управления: {{ selectedName }}
@@ -168,6 +182,23 @@ onUnmounted(() => clearInterval(timer)) }}{{ state.screenId ? ` · Экран №${state.screenId}` : '' }}
Опрос сервера начнётся после открытия показа.
+ ++ {{ + state.soundEnabled === null + ? 'Назначение звука пока неизвестно' + : state.soundEnabled + ? 'Экран назначен для звука' + : 'Экран не назначен для звука' + }} + + · Последнее подтверждённое состояние; сейчас назначение не проверяется. + +
+Переключатель «Выключить весь звук» управляет локальным звуком отдельно.
++ «Отключить управление» прекращает управление приложением; привязка экрана на сервере + сохраняется. +