up
This commit is contained in:
@@ -108,6 +108,10 @@
|
||||
- Следующий шаг: SCREEN-006 — существующая авторизация, выбор игры/скачивание
|
||||
через интерфейс, снимок результатов и получение команд существующего сервера.
|
||||
|
||||
- Исправление по ревизии (2026-10-17): повторный снимок той же таблицы обновляет
|
||||
дату получения без смены ревизии сцены и сброса прокрутки. Изменение вопросов,
|
||||
строк или очистка результатов по-прежнему обновляют сцену. Добавлен регрессионный тест.
|
||||
|
||||
## SCREEN-006 — Подключение игры и синхронизация с сервером
|
||||
|
||||
- Статус: `DONE`
|
||||
@@ -174,3 +178,13 @@
|
||||
не пересобирались. SCREEN-001/SCREEN-004 сохраняют IN_PROGRESS.
|
||||
- Инструкция: docs/SCREEN-006_ACCEPTANCE.md. Следующий этап: SCREEN-007 —
|
||||
устойчивость при отключении дисплея, управление сном и восстановление окон.
|
||||
- Исправления по ревизии (2026-10-17, SCREEN-005/006): последнее полученное
|
||||
назначение звука сохраняется отдельно от исходной отметки команд. Изменение
|
||||
назначения после обрыва сети или повторного открытия показа применяется;
|
||||
неизменное назначение не отменяет локальный mute. Перестановка тех же треков
|
||||
теперь останавливает музыку с требованием повторной подготовки пакета.
|
||||
- Проверка исправлений: 77 тестов (3 новых), type-check, ESLint, Stylelint,
|
||||
сборка, GUI smoke и git diff --check — PASS. Вручную проверен весь diff на
|
||||
применимые требования стандарта; нарушений не найдено. GUI: Linux x64,
|
||||
синтетический дисплей, no-sandbox/mute-audio/disable-gpu; физический звук,
|
||||
проектор и остальные ОС не проверялись, релизные архивы не пересобирались.
|
||||
|
||||
@@ -256,6 +256,8 @@ capturedAt, вопросы и публичные строки таблицы (ID
|
||||
фиктивные результаты в приложении не создаются. Длинная таблица прокручивается
|
||||
снизу вверх, обновления позиций медиа её не перезапускают. Сетевое получение и
|
||||
сохранение снимка реализуются в SCREEN-006; публичного IPC редактирования результатов нет.
|
||||
Повторный снимок с той же таблицей обновляет дату без перезапуска прокрутки;
|
||||
изменение вопросов или строк таблицы начинает показ результатов заново.
|
||||
|
||||
`npm run acceptance` теперь создаёт локальный пакет с обложкой, фоном и восемью
|
||||
шагами; [инструкция SCREEN-005](docs/SCREEN-005_ACCEPTANCE.md) описывает полный
|
||||
|
||||
@@ -225,6 +225,10 @@ SCREEN-009 (LAN-пульт) отменена владельцем; ID не пе
|
||||
git diff --check — PASS. Руководство и чек-лист обновлены. Архивы кандидата
|
||||
не пересобирались; отличие текущих исходников от архивов явно описано в README.
|
||||
Бэкенд, сайт, protobuf и зависимости не менялись.
|
||||
- Ревизия SCREEN-005/006 (2026-10-17): исправлены назначение звука после обрыва,
|
||||
сброс прокрутки неизменной таблицы и пропуск перестановки треков. 77 тестов,
|
||||
типы, линтеры, сборка, GUI smoke и проверка diff/стандарта — PASS; подробности
|
||||
в COMPLETED_TASKS.md. Архивы не пересобирались; аппаратная приёмка остаётся открытой.
|
||||
- Открыто: получить macOS x64/arm64 через workflow после commit/push владельцем;
|
||||
выполнить аппаратную матрицу, включая оба Linux-дистрибутива, час офлайн,
|
||||
проектор/звук/телефон и физические замеры на M1. Статус IN_PROGRESS до получения
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { isDeepStrictEqual } from 'node:util'
|
||||
import type {
|
||||
Catalog,
|
||||
Channel,
|
||||
@@ -348,8 +349,11 @@ export class Controller {
|
||||
const key = this.state.catalog.key
|
||||
if (value !== null && !key) throw new Error('Сначала выберите пакет игры')
|
||||
const snapshot = value === null ? null : parseResults(value, key!)
|
||||
const hasChanged =
|
||||
!isDeepStrictEqual(this.state.results?.questions, snapshot?.questions) ||
|
||||
!isDeepStrictEqual(this.state.results?.teams, snapshot?.teams)
|
||||
this.state.results = snapshot
|
||||
if (this.state.scene.mode === 6) this.invalidateScene()
|
||||
if (hasChanged && this.state.scene.mode === 6) this.invalidateScene()
|
||||
this.broadcast()
|
||||
}
|
||||
reportScene(value: unknown) {
|
||||
|
||||
@@ -415,6 +415,55 @@ test('музыкальная громкость не возвращает ста
|
||||
)
|
||||
})
|
||||
|
||||
test('перестановка прежних треков останавливает музыку до повторной подготовки', async (t) => {
|
||||
const f = await fixture(t)
|
||||
await f.prepare()
|
||||
f.controller.output(true)
|
||||
send(f.controller, { action: 'play', channel: 'music' })
|
||||
const previous = parseRemote({ music: objectMusic(1) })
|
||||
const next = parseRemote({
|
||||
music: { ...objectMusic(2), playlist: { ID: 1, tracks: [{ ID: 2 }, { ID: 1 }] } },
|
||||
})
|
||||
assert.throws(() => applyRemote(f.controller, previous, next), /Музыка изменилась/)
|
||||
assert.equal(f.controller.state.channels.music.desired, 'stopped')
|
||||
assert.deepEqual(f.controller.state.catalog.playlists[0]!.tracks.map((track) => track.id), [1, 2])
|
||||
})
|
||||
|
||||
test('назначение звука восстанавливается после обрыва и открытия окна без повтора команд', async (t) => {
|
||||
const f = await fixture(t)
|
||||
await f.prepare()
|
||||
f.controller.output(true)
|
||||
await f.online.poll()
|
||||
assert.equal(f.controller.state.muted, false)
|
||||
send(f.controller, { action: 'mute', enabled: true })
|
||||
f.model.offline = true
|
||||
await f.online.poll()
|
||||
f.model.offline = false
|
||||
await f.online.poll()
|
||||
assert.equal(f.controller.state.muted, true)
|
||||
|
||||
send(f.controller, { action: 'mute', enabled: false })
|
||||
f.model.offline = true
|
||||
await f.online.poll()
|
||||
f.model.remote.soundEnabled = false
|
||||
f.model.offline = false
|
||||
await f.online.poll()
|
||||
await f.online.poll()
|
||||
assert.equal(f.controller.state.muted, true)
|
||||
assert.equal(f.controller.state.channels.main.desired, 'stopped')
|
||||
|
||||
await f.online.act({ action: 'sound' })
|
||||
f.model.remote.soundEnabled = true
|
||||
await f.online.poll()
|
||||
assert.equal(f.controller.state.muted, false)
|
||||
f.controller.output(false)
|
||||
f.controller.output(true)
|
||||
f.model.remote.soundEnabled = false
|
||||
await f.online.poll()
|
||||
assert.equal(f.controller.state.muted, true)
|
||||
assert.equal(f.controller.state.channels.main.desired, 'stopped')
|
||||
})
|
||||
|
||||
test('истечение JWT не обрывает показ и оставляет доступным повторный вход', async (t) => {
|
||||
const f = await fixture(t)
|
||||
await f.prepare()
|
||||
|
||||
+4
-5
@@ -90,7 +90,7 @@ export class Online {
|
||||
private previous: RemoteState | null = null
|
||||
private pollBusy = false
|
||||
private pollSession = ''
|
||||
private initialSound = true
|
||||
private lastSound: boolean | null = null
|
||||
private lastResults = -Infinity
|
||||
private resultsBusy = false
|
||||
private advanceBusy = false
|
||||
@@ -179,7 +179,6 @@ export class Online {
|
||||
this.screen = c.screen
|
||||
this.state.screenId = c.screen.id
|
||||
this.state.connection = 'waiting'
|
||||
this.initialSound = true
|
||||
}
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT')
|
||||
@@ -221,6 +220,7 @@ export class Online {
|
||||
this.generation++
|
||||
this.screen = null
|
||||
this.previous = null
|
||||
this.lastSound = null
|
||||
this.state.soundEnabled = null
|
||||
this.state.screenId = null
|
||||
this.state.connection = 'disabled'
|
||||
@@ -483,7 +483,6 @@ export class Online {
|
||||
this.screen = s
|
||||
this.state.screenId = s.id
|
||||
this.state.connection = 'waiting'
|
||||
this.initialSound = true
|
||||
await this.persistSecrets()
|
||||
return
|
||||
}
|
||||
@@ -624,10 +623,10 @@ export class Online {
|
||||
this.state.soundEnabled = next.sound
|
||||
if (this.state.connection === 'offline') this.state.error = ''
|
||||
this.state.connection = 'connected'
|
||||
if (!previous && this.initialSound) {
|
||||
this.initialSound = false
|
||||
if (!previous && this.lastSound !== next.sound) {
|
||||
send(this.controller, { action: 'mute', enabled: !next.sound })
|
||||
}
|
||||
this.lastSound = next.sound
|
||||
if (previous) {
|
||||
try {
|
||||
applyRemote(this.controller, previous, next)
|
||||
|
||||
@@ -252,6 +252,34 @@ test('снимок результатов ограничен текущей иг
|
||||
f.c.selectPackage(null)
|
||||
assert.equal(f.c.state.results, null)
|
||||
})
|
||||
test('новая дата тех же результатов сохраняет готовую сцену; изменение таблицы обновляет её', () => {
|
||||
const f = fixture()
|
||||
f.c.output(true)
|
||||
f.send({ action: 'mode', mode: 6 })
|
||||
const data = snapshot(f.key)
|
||||
f.c.setResults(data)
|
||||
const revision = f.c.state.scene.revision
|
||||
f.c.reportScene({ session: f.c.state.session, revision, status: 'ready' })
|
||||
|
||||
data.capturedAt = '2026-10-17T10:00:05Z'
|
||||
f.c.setResults(structuredClone(data))
|
||||
assert.equal(f.c.state.results!.capturedAt, '2026-10-17T10:00:05.000Z')
|
||||
assert.equal(f.c.state.scene.revision, revision)
|
||||
assert.equal(f.c.state.scene.status, 'ready')
|
||||
|
||||
data.teams[0]!.totalScore = 10
|
||||
f.c.setResults(data)
|
||||
assert.equal(f.c.state.scene.revision, revision + 1)
|
||||
assert.equal(f.c.state.results!.teams[0]!.totalScore, 10)
|
||||
data.questions[0]!.text = 'Исправленный вопрос'
|
||||
f.c.setResults(data)
|
||||
assert.equal(f.c.state.scene.revision, revision + 2)
|
||||
f.c.setResults(null)
|
||||
assert.equal(f.c.state.scene.revision, revision + 3)
|
||||
f.c.setResults(null)
|
||||
assert.equal(f.c.state.scene.revision, revision + 3)
|
||||
})
|
||||
|
||||
test('не принимает повреждённые снимки, повторные команды и неизвестные коды ответов', () => {
|
||||
const f = fixture()
|
||||
for (const mutate of [
|
||||
|
||||
@@ -189,7 +189,7 @@ export function applyRemote(controller: Controller, previous: RemoteState, next:
|
||||
if (
|
||||
!playlist ||
|
||||
playlist.tracks.length !== m.trackIds.length ||
|
||||
m.trackIds.some((id) => !playlist.tracks.some((t) => t.id === id))
|
||||
m.trackIds.some((id, index) => playlist.tracks[index]!.id !== id)
|
||||
) {
|
||||
if (controller.state.channels.music.source)
|
||||
send(controller, { action: 'stop', channel: 'music' })
|
||||
|
||||
Reference in New Issue
Block a user