up
This commit is contained in:
@@ -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` не изменять.
|
||||
|
||||
|
||||
@@ -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 до получения
|
||||
|
||||
@@ -22,8 +22,10 @@
|
||||
- [ ] В ОС выбран нужный аудиовыход. Пробный звук слышен физически, mute выключен,
|
||||
громкость основного канала и музыки подходит для помещения.
|
||||
- [ ] После подключения HDMI аудиовыход проверен повторно.
|
||||
- [ ] Для телефона подключено управление выбранной игрой, проверен номер экрана,
|
||||
связь с сервером и новая команда с сайта. Звук назначен нужному экрану.
|
||||
- [ ] Перед подключением телефона проверено название в строке «Игра для управления»;
|
||||
подключено управление этой игрой, проверен номер экрана,
|
||||
связь с сервером и новая команда с сайта. Проверено назначение звука экрану
|
||||
и отдельно локальное выключение звука.
|
||||
- [ ] Перед стартом выбрана нужная сцена, музыка и таймер в ожидаемом состоянии.
|
||||
- [ ] Компьютер подключён к питанию; крышка остаётся открытой.
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
## Команды с телефона
|
||||
|
||||
1. Нажать «Подключить управление выбранной игрой», записать номер экрана.
|
||||
1. Нажать «Подключить управление этой игрой», записать номер экрана.
|
||||
Открыть показ на внешнем дисплее. Дождаться сообщения о связи.
|
||||
2. На телефоне открыть существующую страницу управления этой игрой.
|
||||
Убедиться, что экран появился в её списке. Назначить звук этому экрану
|
||||
|
||||
@@ -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 архива:
|
||||
|
||||
+15
-4
@@ -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 нет.
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
})
|
||||
|
||||
+7
-1
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<OnlineState>({
|
||||
canCancel: false,
|
||||
games: [],
|
||||
screenId: null,
|
||||
soundEnabled: null,
|
||||
connection: 'disabled',
|
||||
error: '',
|
||||
notice: '',
|
||||
@@ -27,6 +28,12 @@ const email = ref('')
|
||||
const password = ref('')
|
||||
const gameId = ref<number | null>(null)
|
||||
const isBusy = ref(false)
|
||||
const gameStatusLabels = new Map([
|
||||
['draft', 'Черновик'],
|
||||
['run', 'Идёт'],
|
||||
['pause', 'Пауза'],
|
||||
['finish', 'Завершена'],
|
||||
])
|
||||
const connectionLabels = {
|
||||
disabled: 'Управление не подключено',
|
||||
waiting: 'Ожидание открытия показа',
|
||||
@@ -134,7 +141,12 @@ onUnmounted(() => clearInterval(timer))
|
||||
</n-space>
|
||||
<n-select
|
||||
v-model:value="gameId"
|
||||
:options="state.games.map((g) => ({ label: `${g.name} · ${g.status}`, value: g.id }))"
|
||||
:options="
|
||||
state.games.map((g) => ({
|
||||
label: `${g.name} · ${gameStatusLabels.get(g.status) ?? 'Неизвестный статус'}`,
|
||||
value: g.id,
|
||||
}))
|
||||
"
|
||||
:disabled="isBusy || state.busy"
|
||||
placeholder="Выберите игру для подготовки"
|
||||
:input-props="{ 'aria-label': 'Игра на сервере' }"
|
||||
@@ -156,11 +168,13 @@ onUnmounted(() => clearInterval(timer))
|
||||
Выбранную игру можно обновить после закрытия показа. Для обновления файлов по прежним
|
||||
адресам загрузите материалы повторно.
|
||||
</p>
|
||||
<p>Выбор игры выше используется для загрузки материалов.</p>
|
||||
<p v-if="selectedKey" data-testid="control-game">Игра для управления: {{ selectedName }}</p>
|
||||
<n-button
|
||||
v-if="selectedKey"
|
||||
:disabled="isBusy || state.busy"
|
||||
@click="run({ action: 'connect' })"
|
||||
>Подключить управление выбранной игрой</n-button
|
||||
>Подключить управление этой игрой</n-button
|
||||
>
|
||||
</template>
|
||||
<p role="status">
|
||||
@@ -168,6 +182,23 @@ onUnmounted(() => clearInterval(timer))
|
||||
}}{{ state.screenId ? ` · Экран №${state.screenId}` : '' }}
|
||||
</p>
|
||||
<p v-if="state.screenId && !outputOpen">Опрос сервера начнётся после открытия показа.</p>
|
||||
<template v-if="state.screenId">
|
||||
<p role="status" data-testid="sound-assignment">
|
||||
{{
|
||||
state.soundEnabled === null
|
||||
? 'Назначение звука пока неизвестно'
|
||||
: state.soundEnabled
|
||||
? 'Экран назначен для звука'
|
||||
: 'Экран не назначен для звука'
|
||||
}}
|
||||
<template
|
||||
v-if="state.soundEnabled !== null && (!outputOpen || state.connection !== 'connected')"
|
||||
>
|
||||
· Последнее подтверждённое состояние; сейчас назначение не проверяется.
|
||||
</template>
|
||||
</p>
|
||||
<p>Переключатель «Выключить весь звук» управляет локальным звуком отдельно.</p>
|
||||
</template>
|
||||
<n-space v-if="state.screenId">
|
||||
<n-button
|
||||
:disabled="isBusy || state.busy || !state.authenticated"
|
||||
@@ -181,6 +212,10 @@ onUnmounted(() => clearInterval(timer))
|
||||
>Отключить управление</n-button
|
||||
>
|
||||
</n-space>
|
||||
<p v-if="state.screenId">
|
||||
«Отключить управление» прекращает управление приложением; привязка экрана на сервере
|
||||
сохраняется.
|
||||
</p>
|
||||
<n-alert v-if="state.error" type="error">{{ state.error }}</n-alert>
|
||||
<n-alert v-if="state.notice" type="info">{{ state.notice }}</n-alert>
|
||||
<template v-if="state.busy">
|
||||
|
||||
@@ -237,6 +237,7 @@ onUnmounted(() => {
|
||||
<connection-panel
|
||||
:output-open="state?.outputOpen ?? false"
|
||||
:selected-key="state?.catalog.key ?? null"
|
||||
:selected-name="state?.catalog.name ?? null"
|
||||
@packages-changed="refreshPackages"
|
||||
/>
|
||||
<n-card title="Материалы показа">
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface OnlineState {
|
||||
busy: boolean
|
||||
canCancel: boolean
|
||||
games: { id: number; name: string; status: string }[]
|
||||
soundEnabled: boolean | null
|
||||
screenId: string | null
|
||||
connection: 'disabled' | 'waiting' | 'connected' | 'offline' | 'expired'
|
||||
error: string
|
||||
|
||||
@@ -2,10 +2,10 @@ import type { ScreenMode, TimerState } from '../types/screen.js'
|
||||
|
||||
export const modeLabels: Record<ScreenMode, string> = {
|
||||
1: 'Обложка',
|
||||
2: 'Вступление',
|
||||
3: 'Завершение вступления',
|
||||
4: 'Заключительное видео',
|
||||
5: 'Завершение показа',
|
||||
2: 'Введение',
|
||||
3: 'Успехов',
|
||||
4: 'Закрывашка',
|
||||
5: 'Спасибо за игру',
|
||||
6: 'Результаты',
|
||||
7: 'Таймер',
|
||||
8: 'Текст',
|
||||
|
||||
Reference in New Issue
Block a user