Клиентская часть Фазы 3 (AGENT.md 7.13, 7.14): звуки сервера, локальные
звуки интерфейса и телеметрия медиа.
- саундборд: `api/sounds.ts` (лимиты 512 КБ и 30 на вид, multipart-загрузка,
play, `SOUND_EVENTS`), `lib/soundboard.ts` (blob-кэш `/files/{id}`,
наложение до 8 звуков, громкость = `outputVolume`, тишина при дефене),
попап в панели комнаты (`USE_SOUNDBOARD`), таймер на `rate_limited`,
подпись «включил звук» и защита от двойного воспроизведения;
- звуковая палитра: `SOUNDBOARD_PLAY` и события сервера (вход/выход
участника, упоминание, сообщение, вход/выход из голосовой, свои
микрофон/камера/экран, серверный мьют) → `playEventSound`;
- секция «Звуки» в настройках сервера (`MANAGE_SOUNDS`): два вида со
счётчиками, drag&drop, имя, эмодзи, событие палитры, переименование,
удаление и клиентские проверки файла;
- push-to-talk: режим и клавиша (`event.code`), хук в `AppLayout`, вход в
комнату с выключенным микрофоном, удержание включает публикацию, отпускание
и потеря фокуса выключают, серверный мьют приоритетнее;
- статистика: поповер с разрешением, fps, кодеком, битрейтом, RTT, потерями
и jitter (опрос раз в секунду, остановка при закрытии), выбор качества
публикации low/medium/high, пресеты шаринга экрана 1080p60…480p30;
- данные: `guilds[].sounds` из READY, `GUILD_SOUNDS_UPDATE`, селектор
`useGuildSounds`, тумблеры звуков в «Аудио-видео» (localStorage);
- тесты: `soundboard`, `soundSettings`, `pushToTalk`, `voiceStats` и
расширенный фейковый LiveKit SDK.
- `livekit-client` подключён как ленивый чанк: SDK грузится только при входе в
комнату, стартовый бандл не вырос;
- `src/lib/livekit.ts` — единственное место знания о SDK: подключение, дорожки
микрофона/камеры/экрана, снимки участников (речь, флаги, качество связи),
выбор устройств на лету, разбор exp токена;
- `src/stores/voiceConnection.ts` — сессия голоса: статусы
idle/connecting/connected/reconnecting/error, продление токена за минуту до
истечения с переподключением и повторным применением флагов, серверный мьют и
глушение, громкость участников, перемещение, подсказка «говорите с выключенным
микрофоном» не чаще раза в 5 с;
- UI: состав голосовой комнаты в сайдбаре (речь, флаги, качество), панель
комнаты с плитками и модерацией, меню участника (личная громкость, мьют,
перемещение), drag&drop между голосовыми комнатами, кнопки панели пользователя
управляют реальной сессией, при выключенном голосе — понятная подсказка;
- 368 тестов Vitest (было 330), check/lint/test/build зелёные.
Шаблон compose подставляет переменные из явного списка: LIVEKIT_PUBLIC_URL не
был в нём, поэтому приложение получало пустой адрес LiveKit и голос оставался
выключенным. Добавлены LIVEKIT_API_KEY/SECRET/PUBLIC_URL в рендер compose.
Установщик падал на 'TLS_ENABLED: unbound variable': расчёт LIVEKIT_PUBLIC_URL
стоял выше определения TLS_ENABLED. Значение перенесено после блока TLS,
dry-run проходит.
Позиционирование переведено на прямые стили и класс `gl-anchored-panel`:
React не управляет left/top/max-height, поэтому повторный рендер не сбрасывает
раскладку, а `data-placement` задаёт направление анимации (панель может
развернуться вниз, если сверху мало места). Линтер по хукам чистый, 289 тестов
зелёные.
- стрелки настроек микрофона, камеры и наушников стали прямоугольниками во всю
высоту основной кнопки (32 px) и шириной около трети кнопки: по ним легко
попасть, группы не наслаиваются и не выходят за панель (сайдбар расширен до
256 px, у групп фиксированная ширина 36 px);
- поповеры и меню теперь позиционируются от рабочей области: новая
`useAnchoredPosition` прижимает панель к краям окна, выбирает сторону с
большим запасом места и ограничивает высоту, а содержимое прокручивается —
подписи устройств и слайдер громкости больше не обрезаются;
- тесты: геометрия группы стрелки, открытие меню и ограничение положения
панели в пределах окна (289 тестов Vitest).
- лента оформляет виды сообщений от сервера: action (/me) — имя акцентом и
курсив, whisper — приглушённый блок с бейджем, scream — крупнее и жирнее,
private (/ls) — рамка, замок и бейдж «лично», system — центрированная строка
без аватара и панели действий; реакции, ответы, правка и удаление работают
как раньше, группировка не сливает нестандартные виды с обычными;
- автокомплиты в поле ввода с навигацией ↑/↓, Enter/Tab, Escape и мышью:
@ — участники сервера (в DM собеседник), # — видимые текстовые комнаты,
: — словарь на 113 эмодзи, / — команды с описанием; подсказки не появляются
внутри слова и в блоках кода, вставка восстанавливает каретку и не расходится
со слоем подсветки;
- `<#id>` рендерится как #имя комнаты из снапшота (иначе #id);
- 287 тестов Vitest (было 262), check/lint/test/build зелёные.
AGENT.md 7.6:
- команды разбираются на сервере: /me (блок действия), /whisper и /wisper
(шёпот), /scream (громкий блок), «//» экранирует слэш, неизвестная команда
остаётся обычным текстом, пустой текст у команды — 422;
- /ls <логин> <текст> — личное сообщение: тип private, адресат в mentions,
получатель проверяется на участие в сервере и доступ к комнате; сообщение не
отдаётся посторонним ни в истории, ни в поиске (и FTS, и подстрочном) и
доставляется в Gateway только автору и адресатам;
- лимиты: 10 правок в минуту, 20 уникальных реакций на сообщение, 50
закреплений на комнату, не более 10 упоминаний, @everyone/@here только с
MENTION_EVERYONE;
- тесты: команды и экранирование, изоляция личного сообщения (история, поиск,
третий участник), лимит правок и @everyone.
Правки пользователя:
- в «Профиль → Основной» сверху квадратный аватар слева, справа от него
настройки имени, «О себе», состояния и эмодзи во всю высоту; смена аватара —
клик по нему, форматы (JPEG, PNG, WebP, GIF до 8 МБ) — в подсказке;
- убраны дубли: часовой пояс только в «Язык и регион», статус — только в
мини-профиле панели;
- панель пользователя переехала в нижний левый угол под сайдбар, подсказка о
выходе из сервера — на её прежнее место; панель ужата под 240 px;
- шестерёнка сервера открывает новый пункт настроек «Сервер» (список серверов,
общее, участники, роли, опасная зона), а «Профиль → Сервер» оставлен только
для ника и «Применить ко всем серверам»;
- окно настроек получило статический размер 70% рабочей области со скроллом;
- ID пользователей скрыты везде, кроме раздела «Инстанс» у администратора;
- список друзей свёрнут под кнопкой «Друзья (N)» и раскрывается снизу вверх;
- 249 тестов Vitest (было 239), check/lint/test/build зелёные.
Раздел «Друзья» по запросу пользователя:
- первая иконка рейки — «Друзья», серверы ниже за разделителем; личные беседы
не засоряют список серверов;
- сайдбар друзей: беседы с точками статуса и бейджами непрочитанного,
список друзей с кликом на беседу;
- контент: пустое состояние «Добавь друзей для общения» с поиском, поиск по
логину (debounce), добавление в друзья, входящие и исходящие заявки,
меню «…» (удалить/заблокировать), поповер последнего входа в часовом поясе
пользователя (по умолчанию МСК), блок «Присоединиться по приглашению»
с предпросмотром сервера;
- личный чат на существующих компонентах: права по can_send канала, заголовок
с именем и статусом собеседника, поиск, пины, ack и typing работают;
- realtime: READY dm_channels, события DM_CHANNEL_CREATE, RELATIONSHIP_UPDATE,
PRESENCE_UPDATE и MESSAGE_CREATE в беседах;
- новый пользователь: «У вас пока нет серверов», создание сервера и вход по
приглашению (автовступления нет);
- 239 тестов Vitest (было 219), тест паритета ключей i18n (527 ключей).
По правкам пользователя:
- вход: поле «Код 2FA или резервный код» видно сразу, принимает 14-символьные
резервные коды (раньше влезало 6 цифр), пустое значение не ломает вход;
- нижняя панель: одна подложка, слева аватар и имя с мини-профилем и выбором
статуса, справа микрофон, камера, наушники (переключатели, стрелка с выбором
устройства и громкости, переход в «Аудио-видео») и шестерёнка;
- кнопка «Покинуть сервер» из сайдбара убрана (выход — в «Опасной зоне»);
- настройки открываются модальным окном со списком пунктов; «Профиль» имеет
вкладки «Основной» и «Сервер» (список серверов, ник и «Применить ко всем
серверам»), аватар загружается и удаляется, есть часовой пояс (МСК по
умолчанию);
- markdown: добавлены ==выделение==, __жирный__, ***оба***, ||спойлер||, #канал;
- двойной клик по сообщению ставит ответ (не срабатывает на ссылках, кнопках и
при выделенном тексте);
- 219 тестов Vitest (было 176), check/lint/test/build зелёные.
Полноценный текстовый чат в веб-клиенте:
- стор сообщений по комнатам (кэш 200, оптимистичная отправка с nonce,
применение всех событий Gateway, пагинация курсором, непрочитанные и
упоминания, сброс при логауте и INVALID_SESSION);
- лента: группировка подряд идущих сообщений одного автора, разделители дат,
разделитель «новые сообщения», подгрузка истории вверх, пустые состояния;
- разметка: жирный/курсив/код/блок кода/зачёркнутый/цитата/ссылки (только
http(s)) и упоминания <@id> — безопасный рендер React-узлами без
dangerouslySetInnerHTML;
- реакции чипами с подсветкой своих, быстрые эмодзи и пикер, панель действий и
контекстное меню (ответ, реакция, закрепление, правка, удаление, копирование);
- ответы с переходом к оригиналу, inline-правка, «печатает…» с POST /typing раз
в 3 с, пины в модалке, поиск по комнате с подсветкой и переходом;
- композер: автовысота, Enter/Shift+Enter, лимит 4000, drag&drop и скрепка
(POST /channels/{id}/files → attachment_ids), обработка 429 с таймером и
perm.denied с причиной, read-only без SEND_MESSAGES;
- бейджи непрочитанных и упоминаний в сайдбаре, авто-POST /ack при активном окне;
- 176 тестов Vitest (было 100), check/lint/test/build зелёные.
- анимации на чистом CSS: появление страниц (gl-page), модалки с обратным
закрытием, поповеры, каскад списков (gl-stagger), индикаторы активной комнаты
и сервера, скелетоны с shimmer; prefers-reduced-motion отключает всё;
- нижне-левое меню пользователя: аватар и шестерёнка открывают меню вверх с
пунктами «Настройки аккаунта», «Настройки серверов», сменой статуса и
выходом (role=menu, стрелки, Escape, возврат фокуса);
- настройки разделены на два соседних раздела с общим внешним видом:
/settings/account/* (профиль, безопасность, внешний вид, инстанс) и
/settings/servers/:guildId (общее, свой ник, участники, роли, опасная зона)
с панелью выбора сервера — иконка и имя, выбор сохраняется в URL;
- старые пути /settings/{profile,security,appearance,instance} работают
редиректами; шестерёнка настроек сервера — в шапке сервера рядом с «+»
и только для владельца/ADMINISTRATOR/MANAGE_GUILD;
- 100 тестов Vitest (было 66), check/lint/test/build зелёные.
- упоминания в сообщении увеличивают счётчик упоминаний комнаты у упомянутых
(store.BumpMentionCount) и отправляют им READ_STATE_UPDATE на все устройства;
- отправка сообщения отмечает его прочитанным для автора;
- подтверждение прочтения сбрасывает упоминания;
- суточная квота 10 приглашений на пользователя (AGENT.md 8.6), администратор
инстанса её обходит; httpx.NewRateLimiterWindow для произвольных окон;
- тест на рост и сброс счётчика упоминаний.
- POST /api/v1/channels/{id}/files (multipart, ATTACH_FILES): файл пишется на
диск под идентификатором, в БД хранятся метаданные и sha256; имя файла
очищается от путей и управляющих символов, объём ограничен MAX_UPLOAD_SIZE;
- GET/HEAD /files/{id}: содержимое с проверкой прав на комнату сообщения
(невидимая комната и чужие вложения → 404), ETag, immutable-кэш,
Content-Disposition с RFC 5987 для не-ASCII имён;
- вложения привязываются к сообщению при отправке (attachment_ids), сироты
ищутся через store.ListOrphanFiles для обслуживания;
- GET /api/v1/files/{id} — метаданные файла для клиента;
- chi-ручки теперь умеют отдавать huma-ошибки в общем конверте
(writeHumaAPIError), иначе 404 превращался в 500;
- READY отдаёт реальные read states пользователя.
Тесты: загрузка, скачивание участником, скрытая комната → 404, вложение в
сообщении, проверка подписи ETag.
Клиент React 19 + TanStack Query + Zustand:
- вход (поле TOTP появляется на `auth.2fa_required`), регистрация с проверками
и учётом `registration_enabled`, онбординг первого входа с возможностью
пропустить, редирект `/` и страница `/status`;
- защита маршрутов: неавторизованных — на `/login`, без онбординга — на
`/onboarding`;
- оболочка `/app`: рейка серверов, сайдбар категорий и комнат, шапка сервера,
панель пользователя, модалки создания сервера и комнаты, экран «нет серверов»
со вступлением в главный сервер, заглушка комнаты до Фазы 2;
- настройки: профиль, безопасность (step-up, сессии, logout-all, 2FA с
локальным QR и кодами восстановления), внешний вид, админ-раздел инстанса;
- Gateway-клиент: HELLO/IDENTIFY/RESUME, heartbeat с ожиданием ACK,
экспоненциальное переподключение, разбор `INVALID_SESSION {reason,resumable}`;
- диспетчер событий: READY/RESUMED/USER_UPDATE/GUILD_UPDATE/GUILD_DELETE/
CHANNEL_* применяются к стору, GUILD_CREATE догружается по REST,
MEMBER_*/ROLE_* инвалидируют запросы участников, ролей и карточки сервера;
- i18n ru/en, 66 тестов Vitest, `check`/`lint`/`test`/`build` зелёные.
Серверные правки под клиент:
- `totp_enabled` в профиле (`GET /users/@me`, вход, регистрация) — клиенту
нужно знать, требовать ли код при step-up;
- `POST /auth/2fa/setup` отдаёт `otpauth_url` и `qr_png` (data-URI): QR-код
рисуется локально, внешние сервисы генерации QR не используются;
- права в ответах — имена (`VIEW_CHANNEL|SEND_MESSAGES`), клиент сверяет имена.
2FA обязательна для инстанс-администраторов, но до её включения вход закрыт —
получался замкнутый круг. Добавлены команды обслуживания:
- `glchat totp-setup --email <admin>`: создаёт секрет, подтверждает его кодом,
печатает секрет, otpauth-ссылку и 8 резервных кодов (каждый одноразовый);
- `glchat totp-reset --email <admin>`: удаляет секрет при потере устройства;
- код `auth.2fa_enrollment_required` с подсказкой, какую команду выполнить;
- установщик: флаг `--admin-2fa` для автоматического включения (по умолчанию
печатает подсказку, чтобы секреты не оседали в логах установки).
Проверено сквозным прогоном локально: bootstrap → 403 на входе без 2FA →
totp-setup → вход с TOTP-кодом → профиль, серверы, комнаты, роли, участники,
аудит, создание сервера админом, 429 на шестой попытке входа, секретов в логах
нет.