diff --git a/brandbook/README.md b/brandbook/README.md index eb9229d..c862953 100644 --- a/brandbook/README.md +++ b/brandbook/README.md @@ -1,12 +1,12 @@ # Вечерний детектив — брендбук -Единый рабочий брендбук из трёх блоков: «Логотип», «Шрифты», «Цвета». Блок логотипа занимает две страницы, шрифты и цвета — по одной. Страницы белые; в основном документе оставлены только примеры и краткие правила. Технические сведения и источники сохранены в этом описании. В страницах оставлены логотипы, шрифты и палитра. Образцы диплома, бланков и фактур временно убраны из документа; исходные материалы сохранены в комплекте. Используется обновлённый логотип — детектив в шляпе с золотой круглой рамкой и прозрачным внешним фоном. Основная палитра: чёрный #111111, белый #FFFFFF и золотой #FCBF02. Бронза исключена, серый используется только как служебный цвет интерфейса; типографика закреплена за тремя гарнитурами с кириллицей и SIL OFL 1.1: Oswald 600 для логотипа и заголовков, Podkova 600 для других фирменных надписей, Noto Sans 400/700 для основного текста и выделений. Пробковая доска развивает оформление афиш и обложек. Сайт и приложения не изменены. +Единый рабочий брендбук из четырёх блоков: «Логотип», «Шрифты», «Цвета», «Тон общения». Документ открывает отдельная обложка. Блок логотипа занимает две страницы, шрифты, цвета и тон общения — по одной. Страницы белые; в основном документе оставлены только примеры и краткие правила. Технические сведения и источники сохранены в этом описании. На страницах представлены логотипы, шрифты, палитра и краткие правила общения с примерами. Образцы диплома, бланков и фактур временно убраны из документа; исходные материалы сохранены в комплекте. Используется обновлённый логотип — детектив в шляпе с золотой круглой рамкой и прозрачным внешним фоном. Основная палитра: чёрный #111111, белый #FFFFFF и золотой #FCBF02. Бронза исключена, серый используется только как служебный цвет интерфейса; типографика закреплена за тремя гарнитурами с кириллицей и SIL OFL 1.1: Oswald 600 для логотипа и заголовков, Podkova 600 для других фирменных надписей, Noto Sans 400/700 для основного текста и выделений. Пробковая доска развивает оформление афиш и обложек. Сайт и приложения не изменены. Все изменения вносятся в этот комплект. PDF и `brandbook.zip` перезаписываются; старые версии и отдельные концепции не сохраняются. ## Комплект -- `brandbook.pdf` — 4 страницы A4 для просмотра и передачи. +- `brandbook.pdf` — 6 страниц A4 для просмотра и передачи. - `index.html`, `brandbook.css` — редактируемый исходник без сборки и сетевых ресурсов. - `assets/logos/logo.png` — обновлённая на основе исходной эмблемы растровая версия, 1254 × 1254 px, RGBA. Внешний фон прозрачный, белые области внутри знака сохранены. Изображение обновлено с помощью imagegen. - `assets/fonts/Podkova.ttf` — фирменный вариативный шрифт, насыщенности 400–800; `Podkova-OFL.txt` — его лицензия SIL OFL 1.1. Исходный файл Google Fonts скопирован без изменения байтов, с простым локальным именем. @@ -36,11 +36,11 @@ pdfinfo brandbook.pdf pdftotext -layout brandbook.pdf - ``` -При отсутствии соседнего проекта используйте ручную печать в браузере. Просмотр не требует Electron. Экспортёр проверяет загрузку шрифтов и изображений, отсутствие сетевых запросов, 4 HTML-страницы и отсутствие выхода содержимого за поля и границы примеров. После изменений нужен визуальный просмотр PDF. +При отсутствии соседнего проекта используйте ручную печать в браузере. Просмотр не требует Electron. Экспортёр проверяет загрузку шрифтов и изображений, отсутствие сетевых запросов, 6 HTML-страниц и отсутствие выхода содержимого за поля и границы примеров. После изменений нужен визуальный просмотр PDF. ## Проверки и ограничения -Проверены три блока и 4 страницы A4, локальные ресурсы и прозрачность PNG эмблемы. Новая основная палитра закреплена в брендбуке; её перенос в `src/assets/base.css` пока не выполнен; текст правил очной игры — с актуальным `GAME_RULES_OFFLINE.md`. Шрифты встроены в PDF. +Проверены четыре блока и 6 страниц A4, локальные ресурсы и прозрачность PNG эмблемы. Новая основная палитра закреплена в брендбуке; её перенос в `src/assets/base.css` пока не выполнен; текст правил очной игры — с актуальным `GAME_RULES_OFFLINE.md`. Шрифты встроены в PDF. Oswald, Podkova и Noto Sans разрешено использовать, встраивать и распространять по SIL OFL 1.1. При распространении шрифтов сохраняйте соответствующие лицензии и сведения авторства. При передаче файла шрифта сохраняйте `Podkova-OFL.txt` с copyright. Источник: https://github.com/google/fonts/tree/main/ofl/podkova ; файл: https://raw.githubusercontent.com/google/fonts/main/ofl/podkova/Podkova%5Bwght%5D.ttf . Файл `assets/fonts/LICENSE.txt` относится к Noto. Отдельные файлы a_OldTyper и его NOTICE удалены из комплекта. diff --git a/brandbook/brandbook.css b/brandbook/brandbook.css index 0210009..b583d3a 100644 --- a/brandbook/brandbook.css +++ b/brandbook/brandbook.css @@ -50,3 +50,9 @@ th{font-size:10px;letter-spacing:1px;text-transform:uppercase} @page{size:A4;margin:0} @media print{body{background:transparent}.page{margin:0}*{-webkit-print-color-adjust:exact;print-color-adjust:exact}} @media screen and (max-width:820px){body{overflow-x:auto}.page{margin:0 0 16px}} + +.cover{text-align:center} +.cover-logo{display:block;width:480px;height:570px;object-fit:contain;margin:48px auto 24px} +.cover h1{font-size:64px;margin-bottom:14px} +.cover-caption{font-size:13px;letter-spacing:2px} +.cover .folio{text-align:left} diff --git a/brandbook/brandbook.pdf b/brandbook/brandbook.pdf index 5343215..47ad742 100644 Binary files a/brandbook/brandbook.pdf and b/brandbook/brandbook.pdf differ diff --git a/brandbook/brandbook.zip b/brandbook/brandbook.zip index 78832d5..7bbfff2 100644 Binary files a/brandbook/brandbook.zip and b/brandbook/brandbook.zip differ diff --git a/brandbook/export.cjs b/brandbook/export.cjs index dec803b..d47beaa 100644 --- a/brandbook/export.cjs +++ b/brandbook/export.cjs @@ -32,8 +32,8 @@ app.whenReady().then(async () => { return { blocks: pages.map(page => page.id), pages: pages.length, overflow, familiesLoaded: ['Podkova', 'Oswald', 'Noto'].every(family => [...document.fonts].some(face => face.family === family && face.status === 'loaded')), fonts: ['14px Noto', '700 14px Noto', '600 48px Oswald', '600 32px Podkova'].every(font => document.fonts.check(font)), images: [...document.images].length, unloaded: [...document.images].filter(image => !image.naturalWidth).length }; })()`); - assert.equal(result.pages, 4, 'Expected four pages across three brandbook blocks'); - assert.deepEqual(result.blocks, ['logo', 'logo-mono', 'fonts', 'colors']); + assert.equal(result.pages, 6, 'Expected cover and five content pages'); + assert.deepEqual(result.blocks, ['cover', 'logo', 'logo-mono', 'fonts', 'colors', 'voice']); assert.deepEqual(result.overflow, [], 'Content reaches the page footer'); assert.equal(result.fonts, true, 'Local fonts must load'); assert.equal(result.familiesLoaded, true, 'All three bundled font families must load'); diff --git a/brandbook/index.html b/brandbook/index.html index 908e586..a77b302 100644 --- a/brandbook/index.html +++ b/brandbook/index.html @@ -2,13 +2,20 @@ Вечерний детектив — Брендбук
+
+
Вечерний детективФирменный стиль
+ +

Брендбук

+

Логотип · Шрифты · Цвета · Тон общения

+
ВЕЧЕРНИЙ ДЕТЕКТИВ01 / 06
+
+
Вечерний детектив / Брендбук01 / Логотип · продолжение

Монохром и размещение

@@ -16,7 +23,7 @@
Знак отдельно, светлый фон
Знак отдельно · светлый фон
Знак отдельно, тёмный фон
Знак отдельно · тёмный фон
Название справа, светлый фон
Название справа · светлый фон
Название справа, тёмный фон
Название справа · тёмный фон
Название снизу, светлый фон
Название снизу · светлый фон
Название снизу, тёмный фон
Название снизу · тёмный фон

Свободное поле

Не меньше ¼ диаметра знака. В готовых файлах поле уже включено.

Размер и контраст

Знак — от 48 px. С названием — от 240 px по ширине. Фон спокойный, без пёстрого рисунка.

Не менять пропорции, цвета и расположение элементов. Не добавлять тени.

Все варианты логотипа →

-
ЛОГОТИП · ШРИФТЫ · ЦВЕТА02 / 04
+
Вечерний детектив / Брендбук02 / Шрифты

Шрифты

@@ -35,7 +42,7 @@

Шрифты включены в комплект. Лицензии SIL OFL 1.1: Oswald, Podkova, Noto Sans. Передавайте шрифты вместе с лицензиями.

-
ЛОГОТИП · ШРИФТЫ · ЦВЕТА03 / 04
+
ЛОГОТИП · ШРИФТЫ · ЦВЕТА · ТОН ОБЩЕНИЯ04 / 06
Вечерний детектив / Брендбук03 / Цвета
@@ -49,6 +56,27 @@

Служебные цвета

Серый — для элементов интерфейса. Цвета статусов сопровождаются текстом.

Контраст

Белый на чёрном; чёрный на белом и золотом. Не набирать мелкий текст золотым на белом.

Для печати — цветопроба. Источники и лицензии →

-
ЛОГОТИП · ШРИФТЫ · ЦВЕТА04 / 04
+
ЛОГОТИП · ШРИФТЫ · ЦВЕТА · ТОН ОБЩЕНИЯ05 / 06
+
+
+
Вечерний детектив / Брендбук04 / Тон общения
+

Тон общения

+

Приглашаем в расследование: дружелюбно, с интригой и уважением к игроку.

+
+

На равных

Обращаемся на «вы», со строчной буквы. Поддерживаем команду, не оцениваем способности игроков.

+

Интрига без спойлеров

Даём завязку и повод строить версии. Не раскрываем решения и ключевые улики.

+

Ясно и по делу

Короткие фразы и конкретные действия. Время, место, цена и правила — без загадок.

+

Детективный характер

«Дело», «улика», «версия», «расследование» — к месту. Без пафоса, давления и избытка восклицаний.

+
+ + + + + + +
СитуацияКак говорим
АнонсУ каждого свидетеля своя версия. Соберите команду и выясните, что произошло.
ПризывВыберите игру и зарегистрируйте команду.
ПодсказкаСравните время в показаниях. Все ли детали сходятся?
ОшибкаНе удалось отправить ответ. Попробуйте ещё раз.
ФиналДело закрыто. Спасибо за ваши версии и командную работу!
+

Избегаем

«Только гений раскроет это дело!»
«Вы опять ошиблись».
«Срочно! Последний шанс!!!»

Вместо этого

«Внимание к деталям поможет найти ответ».
«Попробуйте проверить другую версию».
Указываем реальный срок регистрации.

+

В анонсах создаём атмосферу. В правилах, оплате и поддержке прежде всего помогаем разобраться.

+
diff --git a/docs/agent/COMPLETED_TASKS.md b/docs/agent/COMPLETED_TASKS.md index 035d1c5..13af0b0 100644 --- a/docs/agent/COMPLETED_TASKS.md +++ b/docs/agent/COMPLETED_TASKS.md @@ -2,6 +2,97 @@ Архив задач, переведённых в статус `DONE`. +### ED-058 — Добавить карту и лист оплаты в материалы ведущего + +- Статус: `DONE` +- Цель: добавить два согласованных исходных PNG в `Для ведущего/` каждого печатного ZIP. +- Решение: встроенные ассеты бэкенда, исходные имена и байты; существующие проверки отмены и размера архива. +- Критерии: оба файла есть в ZIP, в том числе без команд, без преобразования в PDF. +- Проверка: тесты наличия и точного совпадения байтов, `gofmt`, `go test ./...`, `git diff --check`, ревью Go-стандарта. +- Ограничения: без обработки изображений, изменений API/UI, физической печати, commit/push. +- Результат: оба исходных PNG добавляются в `Для ведущего/` с исходными именами для каждой игры. Тесты подтверждают наличие и точные байты файлов, включая игру без команд; копии ассетов сверены с исходниками. `gofmt`, `go test ./...` и `git diff --check` выполнены успешно; diff проверен по Go-стандарту. +- Ограничения проверки: физическая печать и рабочий сервер не проверялись. Commit/push не выполнялись. + + +### ED-057 — Заменить дипломы универсальным бланком + +- Статус: `DONE` +- Цель: заменить три диплома одним пустым бланком из согласованного PNG. +- Решение: одностраничный A4 с исходной картинкой целиком, без обрезки и надписей; `Для ведущего/Дипломы/Диплом.pdf`. +- Критерии: один диплом в ZIP, включая игру без команд; все поля заполняются вручную. +- Проверка: тесты PDF и архива, визуальная проверка, `go test ./...`, Go-стандарт и `git diff --check`. +- Ограничения: без автозаполнения, новых зависимостей, изменений API/UI, физической печати, commit/push. +- Результат: три прежних диплома заменены одним пустым бланком; исходный PNG встроен без изменений. PDF визуально проверен: рамка целиком, поля пустые. `gofmt`, `go test ./...` и `git diff --check` выполнены успешно; diff проверен по Go-стандарту. +- Ограничения проверки: физическая печать и рабочий сервер не проверялись; исходник около 128 dpi на A4. Предпросмотр: `/tmp/ed057-diploma.pdf`. Commit/push не выполнялись. + + +### ED-056 — Редактор и офлайн-воспроизведение групп в screen + +- Статус: `DONE` +- Репозиторий: `evening_detective_screen`. +- Цель: редактирование групп только в режиме редактирования, запуск групп в обычном режиме и полное воспроизведение подготовленных групп без сервера. +- Результат: в редактор добавлены создание, переименование, удаление без удаления треков, режим завершения, перестановка групп и треков внутри группы, перенос треков. Операции используют API ED-055; учитываются запреты активной/приостановленной группы. В обычном режиме — названия и режимы групп, текущая группа, «Играть группу» и возврат к целому плейлисту. Полей и кнопок изменения групп вне редактирования нет. +- Результат: черновики сохраняются при ошибках, потере связи, внешнем изменении и удалении группы; закрытие с изменениями требует явной отмены. Подтверждённое удаление собственной группы отменяет её черновик. Смена сервера сбрасывает контекст редактора. +- Результат: локальный пакет, каталог и IPC содержат группы и принадлежность треков; состояние канала хранит groupId. Формат пакета остаётся 1; отсутствующие новые поля не меняют старую форму данных и отпечатки. Метаданные групп входят в новые отпечатки и обнаружение обновлений материалов. Руководство пользователя обновлено. +- Результат: единая функция переходов используется контроллером, подготовкой следующего файла, командами и офлайн-продолжением. Поддержаны повтор одного/нескольких треков, остановка, следующая непустая группа и остановка в конце списка. При отсутствии файла или неподготовленной группе музыка безопасно останавливается с ошибкой; выход в общий список не выполняется. После восстановления связи применяется серверное состояние даже с прежней версией; офлайн-команды не отправляются задним числом. +- Результат: обработчики загрузки и завершения защищены от старых операций и сессий. Ожидающий ответ по старой группе не блокирует окончание новой короткой группы. Пауза, продолжение, громкость и ручной выбор сохраняют корректную группу и позицию. +- Проверено: `npm run type-check`, `npm run lint`, `npm run lint:css`, `npm test` (все 241 тест), `npm run build`, `git diff --check`; после сохранения исходной проверки отмены загрузки дополнительно пройден финальный online.test.js. Проверены парсер, переходы, сохранение/чтение пакетов, отпечатки, API редактора, предзагрузка, ошибки файлов, восстановление связи, устаревшие ошибки и конкурентные окончания. +- Проверено: Electron — редактор на 1280/375 px, отсутствие элементов редактирования в обычном режиме, запуск, запреты активной группы, режимы, порядок, ошибка создания и повтор, перенос трека, удаление группы с сохранением треков, сохранение черновика офлайн и при удалении извне, подтверждение отмены, офлайн-запуск и отсутствие горизонтального переполнения. Реальный короткий WAV через Controller + MediaPlayer без сервера: повтор, остановка, переход через пустую группу и остановка последней группы. Стенды и снимки находятся в `/tmp/ed056-ui`, аудиостенд — `/tmp/ed056-audio.cjs`. +- Проверено: ручное ревью целевого diff по существующим паттернам screen, границам IPC/API, сохранению данных и совместимости; независимое статическое ревью ядра, редактора и обработчиков ошибок. Обнаруженные гонки исправлены и покрыты тестами; блокирующих замечаний не осталось. +- Ограничения: редактирование требует сервера с API ED-055; офлайн используются подготовленные группы и файлы. Новые зависимости, backend API и миграции не добавлялись. +- Не проверено: звук на физическом оборудовании площадки и установленный релизный пакет. Развёртывание, commit и push не выполнялись; для использования изменений требуется обновить screen. + +### ED-055 — Группы музыкального плейлиста + +- Статус: `DONE` +- Репозитории: `evening_detective_server`, `evening_detective_frontend`; совместимость `evening_detective_screen`. +- Цель: именованные группы внутри плейлиста с порядком групп и треков, режимами повтор / остановка / следующая группа. +- Результат: группы создаются, переименовываются, переставляются и удаляются без удаления их треков; треки назначаются группе или остаются без группы. Новая группа повторяется по умолчанию. «Играть группу» сразу запускает первый трек; переход пропускает пустые группы и останавливается в конце списка. Старое воспроизведение всего плейлиста сохранено. +- Результат: сервер защищает активные и приостановленные группы во всех использующих их играх. Перестановка групп заблокирована до остановки плейлиста; переименование доступно. Выбор следующего трека выполняется под блокировками плейлиста и состояния, запоздавшие/повторные события окончания не перезаписывают новую команду. Переупорядочивание и удаление других треков сохраняют текущий файл и позицию. +- Результат: редактор сохраняет черновики при обновлении и удалении группы извне; исправлено пересоздание редакторов позиционными обёртками NSpace. Строковые int64 из protobuf JSON учитываются при сравнении версий и вычислении позиции паузы. Добавлены protobuf API и миграция `20261027120000_playlist_groups.sql`; группы, режимы, порядок и переназначенные ID переносятся экспортом/импортом. Старые пакеты без групп поддерживаются. +- Результат: нативный плеер сохраняет выбранную группу при паузе, продолжении, громкости и повторном запуске трека; повтор единственного трека перезапускает аудио. Управление треками и режимом группы выполняется через сайт. При потере связи выбранная группа останавливается вместо перехода в общий список; отдельный нативный редактор и автономный каталог групп не добавлялись. +- Проверено: генерация protobuf в трёх репозиториях; `gofmt`; полный `go test ./...` с отдельной PostgreSQL через `TEST_DATABASE_URL`, включая конкурентные окончания, права/ошибки, защиту редактирования, экспорт/импорт и старые пакеты; frontend `type-check`, `lint`, `lint:css`, `build`; native `type-check`, `lint`, `lint:css` и все 218 тестов; `git diff --check`. +- Проверено: Firefox на 1280/375 px — режим по умолчанию, запуск/пауза/продолжение, запреты редактирования, порядок групп, ошибка создания и повтор, перенос трека, сохранение удалённого извне черновика и отсутствие горизонтального переполнения. Реальный короткий WAV в браузере — повтор одного трека, остановка, переход через пустую группу, повтор запроса после временной ошибки; строковые версии 9→10 и 9007199254740995 передаются точно. Скриншоты и временные стенды оставлены в `/tmp`. +- Проверено: ручное ревью diff по `GO_STANDARD.md` и `FRONTEND_CODE_STANDARD.md`; независимое статическое ревью блокировок, согласованности каталога и сохранения черновиков. Блокирующих замечаний не осталось. +- Не проверено: звук на физическом оборудовании площадки и развёртывание. Production-миграция не выполнялась; для включения функции нужны миграция БД и обновление сервера/клиентов. Commit/push не выполнялись. + +### ED-053 — Инструкция перед QR команды + +- Статус: `DONE` +- Цель: в PDF и чеках сначала краткая инструкция Wi-Fi, затем QR, ниже название команды; предпросмотр соответствует чеку. +- Критерии: текст помещается при предельных длинах, QR сохраняет размер и читаемость; без Wi-Fi печатаются QR и название. API и настройки не меняются. +- Проверка: тесты рендеринга, просмотр образцов и декодирование QR; `gofmt`, `go test ./...`, ручное ревью по GO_STANDARD.md. +- Результат: общий текст сокращён до подключения к сети, пароля, напоминания остаться без интернета и сканирования QR. В PDF, чеках и предпросмотре инструкция сверху, затем QR, ниже название команды. При пустой сети — QR и название; размеры QR сохранены. +- Проверено: `gofmt`, `go test ./...`, целевые тесты печати (включая предельные длины и регрессию порядка QR/названия), `git diff --check`; ручное ревью diff по GO_STANDARD.md. Образцы PDF и чека просмотрены; три QR из PDF/PNG декодированы `zbarimg`. +- Не проверено: физическая печать и сканирование телефоном на площадке. Commit/push не выполнялись. + + +### ED-052 — Печатать инструкцию подключения к Wi-Fi рядом с QR команды + +- Статус: `DONE` +- Цель: название сети и пароль задаются в настройках каждой игры и печатаются в PDF и чеках, включая предпросмотр. +- Решение: при пустом названии сети инструкция отсутствует; пустой пароль означает открытую сеть. Печать доступна онлайн и на площадке. Настройки переносятся в пакете игры и доступны для редактирования только управляющим игрой. +- Критерии: настройки сохраняются и очищаются; старые клиенты не стирают их; старые пакеты импортируются; текст помещается, QR читается. PDF с Wi-Fi содержит шесть карточек на листе. +- Проверка: тесты хранения, прав, экспорта/импорта, валидации и рендеринга; `go test ./...`; генерация protobuf; frontend type-check, lint, lint:css и build; ручное ревью стандартов. +- Ограничения: без Wi-Fi QR, новых зависимостей, commit и push. Проверка физического принтера и телефонов отдельно на площадке. +- Результат: добавлены поля игры, миграция `20261023120000_game_wifi.sql`, optional-поля обновления и форма с маскированным паролем. Параметры возвращаются только управляющим игрой; игрокам и в списки не выдаются. Старый клиент сохраняет настройки, явные пустые значения очищают их. +- Результат: PDF содержит 6 карточек при заданной сети, чек и его предпросмотр включают одинаковую инструкцию. Текст переносится, длинное название команды на чеке уменьшается при необходимости; размер QR сохранён. Пустая сеть отключает инструкцию, пустой пароль обозначает открытую сеть. Экспорт/импорт переносит параметры без изменения версии пакета; старые пакеты совместимы. +- Проверено: ручное ревью всего diff по `GO_STANDARD.md` и `FRONTEND_CODE_STANDARD.md`, `gofmt`, `git diff --check`; `make generate`, `npm run proto:gen`; `go test ./...` с отдельной PostgreSQL (включая реальные миграции, сохранение/очистку, старых клиентов и экспорт→импорт); `npm run type-check`, `npm run lint`, `npm run lint:css`, `npm run build` — успешно. +- Проверено: браузерный smoke на 390 и 1440 px — загрузка полей, маскировка пароля, валидация, сохранение пробелов пароля, ошибка сервера, защита от повторной отправки и очистка. Образцы PDF и чеков просмотрены, 8 QR из PDF и PNG успешно декодированы `zbarimg`; предельные длины покрыты тестами. +- Ограничения проверки: физический принтер, телефон и подключение к реальному Wi-Fi не проверялись. Рабочая БД и серверы не обновлялись; при запуске нужна миграция. Для изменения уже подготовленного офлайн-пакета требуется новый экспорт. Commit/push не выполнялись. + + +### ED-051 — Перенести идеи пульта во вкладку «Экраны» + +- Статус: `DONE` +- Цель: перенести согласованные идеи `evening_detective_screen` на вкладку «Экраны» страницы игры. +- Результат: предпросмотр и подключение экранов расположены сверху; ниже адаптивные колонки плана экранов и музыки. Добавлены миниатюры всех шагов, выделение текущего шага, компактные кнопки и ползунки громкости; перемотка удалена. +- Результат: редактор музыки отделён от управления воспроизведением. Создание, переименование, загрузка MP3, удаление и перестановка треков используют ID редактируемого плейлиста. Назначение музыки игре выполняется явно; черновики сохраняются при поллинге и ошибках, конфликты внешних изменений показываются пользователю. +- Результат: пауза, продолжение и громкость основного медиа сохраняют позицию; повтор текущего музыкального трека обрабатывается проигрывателем. Устаревшие ответы поллинга музыки не перезаписывают команды. Добавлены проверки ввода таймера, текста и MP3, блокировка повторных запросов и сообщения об ошибках. +- Проверено: `node tests/screens-control.mjs`, `npm run type-check`, `npm run lint`, `npm run lint:css`, `npm run build-only`, `git diff --check` — успешно. Diff вручную проверен по FRONTEND_CODE_STANDARD.md. +- Проверено: Firefox на 1280 и 375 px, отсутствие горизонтального переполнения и автозапуска медиа миниатюрами, таймер и музыка, громкость мышью и клавиатурой, черновик плана после поллинга, независимый выбор плейлиста, ошибка сохранения порядка и повтор, переименование, отключение экрана и ошибка API. Браузерные проверки выполнены на локальных синтетических ответах API. +- Ограничения: сервер, protobuf, Electron и зависимости не изменялись. Реальный внешний дисплей и production не проверялись; предпросмотр отражает данные API, без точного прогресса и подтверждённого состояния медиапроигрывателя. Commit/push не выполнялись. + ### ED-049 — Оптимизировать медиа пакета локальной игры - Статус: `DONE` @@ -418,3 +509,36 @@ - Автоматические проверки: `go test ./...`; фронтенд — `npm run type-check`, `npm run lint`, `npm run lint:css`, `npm run build`; приложение экрана — те же проверки, сборка и `npm test` (203/203). Миграция проверена дважды на временных таблицах PostgreSQL: надписи, порядок, текущий шаг, неизменность остальных шагов и повторный запуск корректны. - GUI: целевой проход восьми шагов Electron завершился `ED050_TEXT_SCREENS_PASS [1,2,8,4,8,6,7,8]`; обе надписи проверены в DOM. Общий smoke не завершился: `Timeout: volume slider keyboard update`; устаревшие ожидания автопаузы музыки в нём исправлены, код проигрывателя не менялся. - Ограничения: production-миграция не запускалась; сервер, сайт и приложение экрана требуют согласованного обновления. Коммиты и push не выполнялись. + +### ED-054 — QR-экран, список команд и типы таймера + +- Статус: `DONE` +- Результат: добавлены режимы QR (ссылка HTTP/HTTPS и необязательный текст) и списка команд; три типа таймера с исходной надписью и сохранением пользовательского текста при смене типа. Настройки доступны в веб-панели и приложении экрана, работают предпросмотры и браузерный вывод `/screen`. +- Команды: только названия активных регистраций, по русскому алфавиту; обновление каждые пять секунд, сохранение последнего снимка по ключу игры в приложении экрана, восстановление без сети после перезапуска. При ошибке предыдущий список сохраняется; при смене игры очищается. +- Совместимость: старые планы и хеши офлайн-пакетов сохранены, таймеры без типа означают «Перерыв» без замены собственного текста. Исправлена инициализация общего состояния таймера при первом подключении к шагу без длительности. Новый `GetScreenTeams` передаёт токен в теле POST; ответ содержит только ID и название. +- Миграция: `20261023130000_screen_qr_timer_type.sql` добавлена в сервер и `prod/migrations`; на рабочей БД не применялась. +- Проверки: штатная генерация protobuf во всех трёх приложениях; `go test ./...`; интеграционные тесты PostgreSQL для экранов, миграций и команд; 208 тестов приложения экрана; type-check, lint, lint:css и сборки обоих Vue-приложений; `node tests/screens-control.mjs`; Electron smoke предпросмотров 1280×720 / 1024×768 с QR и 32 длинными названиями; QR декодирован со снимка; Firefox smoke QR на ширинах 1280/375/160, списка и реального `/screen` с тестовыми API-ответами. Проверены ошибки сети, старые пакеты, сохранение снимка и изоляция игр. Diff вручную проверен по стандартам Go и фронтенда, `gofmt` и `git diff --check` — без замечаний. +- Ограничения: новые анимации отложены по согласованию; реальный проектор не проверялся. Публикация, commit и push не выполнялись. + +### ED-059 — Отдельная страница плейлистов + +- Статус: `DONE` +- Цель: `/playlists` для авторов и организаторов с текущими операциями; редактор в игре сохраняется и переиспользуется. +- Критерии: общий редактор и API-действия; защита черновиков; состояния загрузки, ошибки, пустого списка и отсутствия доступа; пункт меню. +- Проверка: type-check, lint, lint:css, build; smoke операций и регрессии музыки в игре. +- Ограничения: без новых зависимостей, API и предпросмотра звука; без commit/push. +- Результат: добавлены `/playlists` и пункт меню для авторов/организаторов; редактор и API-действия общие со страницей игры. Сохранены операции, MP3-валидация, блокировки, синхронизация и черновики; добавлено подтверждение ухода. API и зависимости не менялись. +- Проверено: `npm run type-check`, `npm run lint`, `npm run lint:css`, `npm run build`, `git diff --check`; ручное ревью diff по FRONTEND_CODE_STANDARD.md. `python3 tests/venue/playlists.py` прошёл: ошибка/повтор, пустой список, создание/выбор/переименование/удаление, пустой MP3 и загрузка, порядок и группы, отказ сохранения с сохранением черновика, роли, route guard, клавиатура, 1280/375 px. Отдельный браузерный smoke музыки в игре прошёл: play/pause/resume, активная группа, режимы, порядок, ошибки, черновик удалённой группы, 1280/375 px. +- Ограничения проверки: браузерные тесты используют изолированный тестовый API; рабочий сервер и реальное воспроизведение MP3 не проверялись. Commit/push не выполнялись. + +### ED-060 — Загружать MP3 сразу в нужную группу + +- Статус: `DONE` +- Цель: кнопка загрузки в каждой группе общего редактора; трек сразу сохраняется в выбранной группе. +- Критерии: единый запрос и транзакция вставки с groupID; проверка принадлежности и активности группы; совместимость старых запросов без groupID; обе страницы. +- Проверка: Go-тесты, интеграционная проверка PostgreSQL, генерация API, type-check, lint, lint:css, build и браузерный smoke. +- Ограничения: без новых зависимостей, миграций, пакетной загрузки и commit/push. +- Результат: «Добавить MP3» находится в каждой группе, включая «Без группы», на странице плейлистов и внутри игры. `groupID = 5` передаётся одним запросом; repository проверяет группу и её использование под существующей блокировкой и атомарно вставляет трек с группой. Запросы без поля сохраняют прежнюю загрузку без группы; очистка файла при отказе остаётся в существующем service flow. Обновлены все callers и generated API. +- Проверено: `make generate`, `npm run proto:gen`, `gofmt`, `go test ./...`; полный набор тестов `playlists_repo` с `TEST_DATABASE_URL` на отдельной локальной PostgreSQL прошёл, включая пустую/заполненную/чужую/удалённую/активную группу и отсутствие вставки при отказе. `npm run type-check`, `npm run lint`, `npm run lint:css`, `npm run build`, `git diff --check` прошли; diff проверен по Go- и frontend-стандартам. +- Браузер: `python3 tests/venue/playlists.py` — прямое назначение группы одним запросом, отказ сервера с сохранением черновика, операции редактора и 375/1280 px. Дополнительный игровой smoke `/tmp/ed060-game-upload-browser.py` — загрузка в группу через GameMusicPlanPanel, блокировка активной группы, доступность другой группы и регрессия воспроизведения/групп/черновиков. +- Ограничения проверки: браузер использует изолированный API; рабочее файловое хранилище и звучание MP3 не проверялись. Новому фронтенду для назначения группы нужен обновлённый бэкенд. Commit/push не выполнялись. diff --git a/evening_detective_screen b/evening_detective_screen index 06af1e0..6e83c01 160000 --- a/evening_detective_screen +++ b/evening_detective_screen @@ -1 +1 @@ -Subproject commit 06af1e047d67d84d0fc8161af5cb1827a76f1b08 +Subproject commit 6e83c014e55b2da7f756c94524d9c89376c79827 diff --git a/prod b/prod index f4ad26f..34ad58a 160000 --- a/prod +++ b/prod @@ -1 +1 @@ -Subproject commit f4ad26f7ffe54ee815134926b1118109426c3bea +Subproject commit 34ad58a2494bad8457c86ed8009b175d06132f93 diff --git a/reproxy b/reproxy index 00bd14b..7aaa424 160000 --- a/reproxy +++ b/reproxy @@ -1 +1 @@ -Subproject commit 00bd14bac1ce007f05056ac663b352ad09023064 +Subproject commit 7aaa4245c8bdd05c28b02c4594d8193f5226796e diff --git a/tests/venue/README.md b/tests/venue/README.md index f4cbb26..23cca6e 100644 --- a/tests/venue/README.md +++ b/tests/venue/README.md @@ -223,3 +223,10 @@ Smoke проверяет три ширины, клавиатурные вкла `python3 tests/venue/clean.py` проверяет очистку venue с подставным Docker: порядок команд, повторный вызов, ошибки и сохранность настроек и посторонних томов. Реальные данные не удаляются. + +### Страница плейлистов + +После сборки фронтенда: `python3 tests/venue/playlists.py`. Нужен Firefox; тест +поднимает изолированный API, проверяет загрузку сразу в группу одним запросом, отказ загрузки без потери +черновика, операции редактора, роли, +клавиатуру и мобильную ширину. Рабочий сервер и настоящие MP3 не используются. diff --git a/tests/venue/playlists.py b/tests/venue/playlists.py new file mode 100644 index 0000000..592fcbe --- /dev/null +++ b/tests/venue/playlists.py @@ -0,0 +1,178 @@ +"""Build the frontend, then run: python3 tests/venue/playlists.py. Requires Firefox.""" +import base64, json, mimetypes, pathlib, socket, subprocess, tempfile, threading, time +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from urllib.parse import urlparse +DIST=pathlib.Path(__file__).resolve().parents[2]/'evening_detective_frontend/dist' +state={'playlists':[], 'calls':[], 'roles':['organizer'], 'failList':False, 'failGroup':False, 'failUpload':False} +class Handler(BaseHTTPRequestHandler): + def log_message(self,*args):pass + def respond(self,data): + self.send_response(200);self.send_header('Content-Type','application/json');self.end_headers();self.wfile.write(json.dumps(data,ensure_ascii=False).encode()) + def do_GET(self):self.handle_request() + def do_POST(self):self.handle_request() + def do_PUT(self):self.handle_request() + def do_DELETE(self):self.handle_request() + def handle_request(self): + path=urlparse(self.path).path + if path.startswith('/__mock'): + path=path.removeprefix('/__mock');payload=json.loads(self.rfile.read(int(self.headers.get('Content-Length',0))) or '{}') + if self.command!='GET':state['calls'].append((path,payload)) + if path=='/api/users/me':return self.respond({'user':{'ID':1,'username':'Тест','roles':state['roles']}}) + if path=='/api/ui/permissions':return self.respond({'permissions':['games_page'],'environment':'prod'}) + if path=='/api/playlists': + if self.command=='GET':return self.respond({'code':13,'message':'Ошибка списка'} if state['failList'] else {'playlists':state['playlists']}) + p={'ID':max((p['ID'] for p in state['playlists']),default=0)+1,'name':payload['name'],'groups':[],'tracks':[]};state['playlists'].append(p) + return self.respond({'playlist':p}) + if path.startswith('/api/playlists/'): + ident=int(path.split('/')[3]);p=next(p for p in state['playlists'] if p['ID']==ident) + if path.endswith('/tracks') and self.command=='POST': + assert payload['data'] and payload['filename'].endswith('.mp3') + if state['failUpload']:return self.respond({'code':9,'message':'Группа используется'}) + if payload.get('groupID') and not any(g['ID']==payload['groupID'] for g in p['groups']):return self.respond({'code':5,'message':'Группа не найдена'}) + p['tracks'].append({'ID':max((t['ID'] for t in p['tracks']),default=0)+1,'name':payload['name'],'filename':payload['filename'],'groupId':payload.get('groupID',0)}) + elif '/tracks/' in path and self.command=='DELETE':p['tracks']=[t for t in p['tracks'] if t['ID']!=int(path.split('/')[-1])] + elif len(path.split('/'))==4 and self.command=='DELETE':state['playlists'].remove(p) + elif path.endswith('/groups/order'): + p['groups'].sort(key=lambda g:payload['groupIDs'].index(g['ID'])) + elif path.endswith('/groups'): + if state['failGroup']:return self.respond({'code':13,'message':'Тест: группа не сохранена'}) + if payload.get('groupID'): + g=next(g for g in p['groups'] if g['ID']==payload['groupID']);g.update(name=payload['name']) + else: + g={'ID':max((g['ID'] for g in p['groups']),default=0)+1,'name':payload['name'],'position':len(p['groups'])};p['groups'].append(g) + if payload.get('endMode') and payload['endMode']!='PLAYLIST_GROUP_END_MODE_REPEAT':g['endMode']=payload['endMode'] + else:g.pop('endMode',None) + elif path.endswith('/group'): + t=next(t for t in p['tracks'] if t['ID']==int(path.split('/')[5]));t['groupId']=payload.get('groupID',0) + elif '/groups/' in path and self.command=='DELETE': + groupID=int(path.split('/')[-1]);p['groups']=[g for g in p['groups'] if g['ID']!=groupID] + for t in p['tracks']: + if t.get('groupId')==groupID:t['groupId']=0 + elif 'trackIDs' in payload: + p['tracks'].sort(key=lambda t:payload['trackIDs'].index(t['ID'])) + elif 'name' in payload:p['name']=payload['name'] + return self.respond({'playlist':p}) + return self.respond({}) + f=DIST/path.lstrip('/') + if f.is_file():body=f.read_bytes();mime=mimetypes.guess_type(f.name)[0] or 'application/octet-stream' + else: + inject='''''' + body=(DIST/'index.html').read_text().replace('',''+inject).encode();mime='text/html' + self.send_response(200);self.send_header('Content-Type',mime);self.end_headers();self.wfile.write(body) +server=ThreadingHTTPServer(('127.0.0.1',0),Handler);threading.Thread(target=server.serve_forever,daemon=True).start() +profile=pathlib.Path(tempfile.mkdtemp(prefix='ed059-playlists-')) +with socket.socket() as s:s.bind(('127.0.0.1',0));port=s.getsockname()[1] +(profile/'user.js').write_text(f'user_pref("marionette.port", {port});\nuser_pref("browser.shell.checkDefaultBrowser", false);') +log=(profile/'firefox.log').open('w');firefox=subprocess.Popen(['firefox','--headless','--no-remote','--profile',str(profile),'--marionette','--remote-allow-system-access','about:blank'],stdout=log,stderr=log);sock=None +try: + for _ in range(100): + try:sock=socket.create_connection(('127.0.0.1',port),timeout=1);break + except OSError:time.sleep(.1) + assert sock is not None,(profile/'firefox.log').read_text() + sock.settimeout(20) + def read(): + size=b'' + while not size.endswith(b':'):size+=sock.recv(1) + data=b'' + while len(data)b.textContent.trim()==={json.dumps(label)}||b.getAttribute('aria-label')==={json.dumps(label)});if(!b||b.disabled)return false;b.click();return true" + wait(f"[...{scope}.querySelectorAll('button')].some(b=>(b.textContent.trim()==={json.dumps(label)}||b.getAttribute('aria-label')==={json.dumps(label)})&&!b.disabled)") + assert js(code),label + time.sleep(.15) + def input_value(selector,value): + assert js(f'const e=document.querySelector({json.dumps(selector)});if(!e)return false;e.value={json.dumps(value)};e.dispatchEvent(new Event("input",{{bubbles:true}}));return true') + command('WebDriver:NewSession',{'capabilities':{'alwaysMatch':{}}});command('Marionette:SetContext',{'value':'chrome'});js('document.documentElement.style.minWidth="0";document.documentElement.setAttribute("minwidth","0");return true');command('Marionette:SetContext',{'value':'content'}) + + def choose(selector,label): + assert js(f'const e=document.querySelector({json.dumps(selector)});if(!e)return false;e.click();return true'),selector + wait('!!document.querySelector(".n-base-select-option")') + assert js(f'const e=[...document.querySelectorAll(".n-base-select-option")].find(e=>e.textContent.trim()==={json.dumps(label)});if(!e)return false;e.click();return true'),label + def navigate(): + command('WebDriver:Navigate',{'url':f'http://127.0.0.1:{server.server_port}/playlists'}) + def upload(name, content="fake-mp3", group="Без группы"): + assert js(f'const e=document.querySelector({json.dumps('section[aria-label="'+group+'"] input[type=file]')});const d=new DataTransfer();d.items.add(new File([{json.dumps(content)}],{json.dumps(name)},{{type:"audio/mpeg"}}));e.files=d.files;e.dispatchEvent(new Event("change",{{bubbles:true}}));return true') + command('WebDriver:SetWindowRect',{'width':1280,'height':1000}) + state['failList']=True + navigate();wait('document.body.innerText.includes("Ошибка списка")') + state['failList']=False;click('Повторить загрузку') + wait('document.body.innerText.includes("Плейлистов пока нет")') + assert not js('return document.body.innerText.includes("Использовать в игре")') + input_value('input[placeholder="Новый плейлист"]','Первый') + click('Создать');wait('!!document.querySelector("#music-playlist-name")') + wait('document.querySelector("#music-playlist-name").value==="Первый"') + wait(r'document.querySelector("input[placeholder=\"Новый плейлист\"]").value===""') + input_value('#music-playlist-name','Переименованный');click('Сохранить название') + wait('!document.querySelector("#music-playlist-name").disabled') + assert state['playlists'][0]['name']=='Переименованный' + input_value('input[placeholder="Новая группа"]','Группа') + state['failGroup']=True;click('Добавить группу');wait('document.body.innerText.includes("Тест: группа не сохранена")') + assert js(r'return document.querySelector("input[placeholder=\"Новая группа\"]").value')=='Группа' + state['failGroup']=False;click('Добавить группу');wait('!!document.querySelector("section[aria-label=Группа]")') + wait('!document.querySelector("#music-playlist-name").disabled') + upload('empty.mp3','');wait('document.body.innerText.includes("Выберите непустой MP3 размером до 64 МиБ")') + assert not state['playlists'][0]['tracks'] + input_value('#music-playlist-name','Черновик загрузки') + state['failUpload']=True;upload('rejected.mp3',group='Группа');wait('document.body.innerText.includes("Группа используется")') + assert js('return document.querySelector("#music-playlist-name").value')=='Черновик загрузки' + assert not state['playlists'][0]['tracks'] + state['failUpload']=False;input_value('#music-playlist-name','Переименованный') + upload('grouped.mp3',group='Группа');wait('document.querySelectorAll(".track-row").length===1') + assert state['playlists'][0]['tracks'][0]['groupId']==1 + assert not any(path.endswith('/group') for path,payload in state['calls']), 'upload must assign the group in one request' + wait('!document.querySelector("#music-playlist-name").disabled') + upload('first.mp3');wait('document.querySelectorAll(".track-row").length===2') + wait('!document.querySelector("#music-playlist-name").disabled') + upload('second.mp3');wait('document.querySelectorAll(".track-row").length===3') + wait('!document.querySelector("#music-playlist-name").disabled') + click('Поднять трек 2 в группе Без группы');click('Сохранить порядок') + wait('!document.querySelector("#music-playlist-name").disabled') + assert [t['name'] for t in state['playlists'][0]['tracks'] if not t['groupId']]==['second.mp3','first.mp3'] + choose('section[aria-label="Без группы"] .track-row .n-base-selection','Группа') + wait('document.querySelector("section[aria-label=Группа] .track-row")!==null') + wait('!document.querySelector("#music-playlist-name").disabled') + assert state['playlists'][0]['tracks'][1]['groupId']==1 + input_value('#music-playlist-name','Черновик') + js('window.allowDiscard=false;return true') + click('Главная');time.sleep(.2) + assert js('return location.pathname')=='/playlists' + assert js('return document.querySelector("#music-playlist-name").value')=='Черновик' + state['failList']=True + # A fresh visit tests the initial load failure again without altering production data. + js('window.allowDiscard=true;return true') + navigate();wait('document.body.innerText.includes("Ошибка списка")') + state['failList']=False;click('Повторить загрузку');wait('!!document.querySelector("#music-playlist-name")') + for width in [1280,375]: + command('WebDriver:SetWindowRect',{'width':width,'height':1000});time.sleep(.3) + assert js('return document.documentElement.scrollWidth<=innerWidth'), 'horizontal overflow' + image=command('WebDriver:TakeScreenshot',{})['value'];pathlib.Path(f'/tmp/ed059-playlists-{width}.png').write_bytes(base64.b64decode(image)) + js('document.querySelector("#music-playlist-name").focus();return true') + command('WebDriver:PerformActions',{'actions':[{'type':'key','id':'keyboard','actions':[{'type':'keyDown','value':'\ue004'},{'type':'keyUp','value':'\ue004'}]}]}) + assert js('return document.activeElement.tagName')=='BUTTON' + click('Удалить', 'document.querySelector(".track-row")');click('Подтвердить') + wait('document.querySelectorAll(".track-row").length===2') + click('Удалить группу');click('Подтвердить');wait('!document.querySelector("section[aria-label=Группа]")') + click('Удалить плейлист');click('Подтвердить');wait('document.body.innerText.includes("Плейлистов пока нет")') + state['roles']=['author'];navigate();wait('!!document.querySelector("[data-testid=music-editor]")') + state['roles']=['user'];navigate();wait('document.body.innerText.includes("Нет доступа к плейлистам")') + assert not js('return !!document.querySelector("[data-testid=music-editor]")') + assert js('return testErrors')==[],js('return testErrors') + print('PLAYLISTS_PASS: direct group upload and rejection, load error/retry, empty/create/select/rename, group error/retry, upload/order/membership/delete, dirty route guard, author/organizer access, keyboard, desktop/mobile') +finally: + if sock:sock.close() + firefox.terminate() + try:firefox.wait(timeout=10) + except subprocess.TimeoutExpired:firefox.kill();firefox.wait() + log.close();server.shutdown();server.server_close()