Commit Graph

197 Commits

Author SHA1 Message Date
grendervill 3bac21ad24 fix(instance): публичная информация отдаёт voice_enabled
Поле никогда не заполнялось, поэтому /api/v1/instance всегда сообщал
voice_enabled:false, хотя голос настроен (клиент читает признак из /meta,
но контракт 6.5 обещает его и здесь).
2026-09-22 20:15:12 +03:00
grendervill e5af7b1f83 style: форматирование после правок событий роли и комнат 2026-09-22 20:11:33 +03:00
grendervill d241714a6d fix(realtime): события ролей, комнат и отзыв сессий без перезагрузки
Дефекты, найденные e2e-матрицей AGENT.md 11.6 (пункты 1, 2, 7, 12):

- ROLE_CREATE/ROLE_UPDATE уходили телом роли без guild_id, а клиент ждал
  ссылку {guild_id, role_id} — событие отбрасывалось, роли у второго
  участника не обновлялись. В rolePayload добавлено guild_id, parseRoleEvent
  принимает и тело роли, и старую ссылку;
- выдача роли (MEMBER_UPDATE) и правки ролей не перечитывали список комнат,
  поэтому приватная комната не появлялась в сайдбаре без F5 — теперь клиент
  перечитывает комнаты по этим событиям;
- CHANNEL_CREATE/CHANNEL_UPDATE уходили телом комнаты с нулевым can_view
  (права персональные), и клиент прятал комнату по этому признаку. Сервер
  шлёт тело события без вычисленных прав плюс GUILD_CHANNELS_SYNC, а клиент
  по событию комнаты перечитывает список;
- отзыв сессий (logout-all, смена пароля, админский сброс и «выйти везде»,
  бан, удаление) не доходил до Gateway: второе устройство оставалось в
  приложении. Добавлен gateway.InvalidateUser — клиент получает
  INVALID_SESSION и закрывает соединение; клиент дополнительно обрабатывает
  событие SESSION_INVALIDATED.

Тесты: gateway (отзыв сессий закрывает соединения и не задевает чужого),
web (событие SESSION_INVALIDATED, перечитывание комнат по CHANNEL_CREATE).
2026-09-22 20:10:53 +03:00
grendervill ff2fc878b0 docs(install): подсказка про квоту внешнего TURN-релея
Нагрузочный тест 10 участников упирался в user-quota=12 на coturn: одному
участнику нужно два allocation, а TURN-пользователь у инстанса один.
2026-09-22 20:01:08 +03:00
grendervill 66dd07eb7b feat(ops): генератор нагрузки и подготовка аккаунтов для теста
- cmd/loadgen: держит заданное число WS-клиентов Gateway, шлёт сообщения с
  нужной частотой, измеряет задержку доставки (p50/p95/p99/max), ответы API,
  разрывы соединений и пишет отчёт в JSON;
- glchat create-user (CLI + обёртка): создаёт аккаунты в обход выключенной
  регистрации, пакетно (--count/--prefix), при --sessions сразу выдаёт сессии
  и складывает учётные данные в файл 600 — иначе сотня клиентов не сможет
  войти из-за лимита попыток по IP;
- auth: CreateUserByOperator и IssueSessionForOperator с тестом;
- web/e2e/voice-ten.spec.ts: десять участников в одной голосовой комнате,
  проверка плиток, входящего аудио и слоя 1080p60 у клиента; сессии берутся
  из файла (без формы входа);
- .gitignore: каталоги вывода Playwright test-results*.
2026-09-22 20:00:33 +03:00
grendervill 785af0dc7b feat(guilds): приватные комнаты — права доступа поверх серверных
Сервер:
- store: оверрайды всех комнат сервера одним запросом (без N+1) и снятие
  оверрайда с признаком «был ли он»;
- API: PUT/DELETE /guilds/{id}/channels/{cid}/overwrites/{role|user}/{tid}
  (права именами через `|`, как у ролей), step-up на изменение, проверка что
  роль принадлежит серверу, а участник состоит в нём;
- список комнат отдаёт permission_overwrites; после правки сбрасывается кэш
  прав комнаты, пишется аудит (channel.overwrite_set/delete) и уходит событие
  GUILD_CHANNELS_SYNC — видимость комнаты меняется у всех участников.

Клиент:
- редактор «Доступ к комнате»: приватность одним переключателем (запрет
  VIEW_CHANNEL для @everyone), права просмотра/переписки/входа для ролей и
  участников, подтверждение личности по требованию сервера;
- в настройках комнаты теперь и голосовые комнаты (фон и права), вебхуки —
  только у текстовых; событие GUILD_CHANNELS_SYNC перечитывает список комнат.

Тесты: 2 Go-теста (скрытие и открытие комнаты ролями, проверка цели и прав) и
3 web-теста редактора (маски, step-up, скрытие без MANAGE_ROLES).
2026-09-22 19:14:05 +03:00
grendervill b403f6c894 fix(security): обновление golang.org/x/crypto до v0.57.0
trivy на стенде нашёл в v0.55.0 две MEDIUM-уязвимости x/crypto/ssh
(CVE-2026-56855, CVE-2026-78662) и неподдерживаемый openpgp; сам код их не
вызывает (govulncheck чист), но обновление убирает находки из отчёта AppSec.
2026-09-22 19:03:29 +03:00
grendervill 787f822dc0 feat(instance): глобальный бан пользователя и поиск в админ-панели
Сервер:
- миграция 00017: таблица instance_bans (причина, автор, дата);
- auth: ErrUserBanned, проверка бана в Login (код user.banned, 403) и в
  ResolveSession — забаненный не получает сессию ни по cookie, ни по Bearer,
  ни в Gateway, а прежняя сессия удаляется;
- store: BannedAt/BanReason в модели пользователя, BanInstanceUser,
  UnbanInstanceUser, IsInstanceBanned, поиск и фильтр в ListUsers,
  CountUsersFiltered; мягкое удаление аккаунта убирает и запись о бане;
- API: POST /instance/users/{id}/ban и /unban со step-up (AGENT.md 7.1),
  отзыв сессий и SESSION_INVALIDATED, аудит instance.user_ban с причиной и
  instance.user_unban; себя и инстанс-админа забанить нельзя;
- GET /instance/users: q (логин и отображаемое имя), banned=true, total.

Клиент:
- панель: поиск, фильтр «только забаненные», бейдж бана с причиной, кнопки
  «Забанить» (с причиной) и «Разбанить» через общий шаг подтверждения
  личности; i18n ru/en, включая текст ошибки user.banned;
- keepPreviousData в списке пользователей: без этого поле поиска
  размонтировалось на первом же символе и набор обрывался.

Тесты: 4 Go-теста (бан блокирует вход и сессии, защита админов, поиск,
уборка бана при удалении), web-тест панели, живая проверка на стенде 19/19.
2026-09-22 18:59:53 +03:00
grendervill d2712c4c99 feat(ops): сверка .env инстанса с таблицей профилей
- scripts/test-profiles.sh --env <файл>: профиль берётся из .env, значения
  таблицы 10.10 пересчитываются и сверяются с производными ключами
  (SQLITE_MMAP_SIZE, MAX_UPLOAD_SIZE, APP_MEMORY_LIMIT, SHARE_SOFT_LIMIT,
  BACKUP_KEEP_DAYS и т. д.), offsite-ключи проверяются на наличие.
- make profiles-env — тот же прогон для /opt/glchat/.env.
- На стенде: профиль standard совпал (67 проверок, 0 ошибок); .env дополнен
  ключами offsite, установленный backup.sh обновлён, выгрузка и недоступный
  приёмник проверены вживую.
2026-09-22 18:46:49 +03:00
grendervill 3151327f6e feat(ops): offsite-бэкапы и проверки профилей
- deploy/backup.sh: BACKUP_OFFSITE_TARGET (rsync или rclone:remote:path)
  выгружает готовый архив после локальной ротации; неудача выгрузки только
  предупреждает. Ротация на приёмнике — опционально
  (BACKUP_OFFSITE_KEEP_DAYS, rclone delete --min-age). Имя архива получает
  суффикс, если бэкап запущен дважды в одну секунду.
- deploy/install.sh: offsite-настройки в .env (BACKUP_OFFSITE_TARGET,
  BACKUP_OFFSITE_KEEP_DAYS) отдельным разделом; детектор незаполненных
  плейсхолдеров учитывает цифры в именах (FAIL2BAN_ENABLED); dry-run больше не
  требует прав на /opt/glchat — явность GLCHAT_ETC_DIR фиксируется до
  подключения common.sh, иначе ветка временного каталога была недостижима.
- scripts/test-profiles.sh: сверка таблицы профилей 10.10 и одинакового набора
  PROFILE_* (46 проверок), make profiles-test / ops-test.
- scripts/test-backup.sh: бэкап и offsite на синтетическом инстансе — локальный
  rsync-приёмник, шим rclone, недоступный хост, нечисловой срок (12 проверок),
  make backup-test.
- scripts/test-install.sh: проверка offsite-ключей в .env после установки.
2026-09-22 18:44:50 +03:00
grendervill af03cfb063 feat(security): проверка Origin, строгий CSP и лимиты загрузок
- middleware OriginCheck: state-changing запросы с чужого Origin отклоняются
  (AGENT.md 9.7); запросы без Origin пропускаются — cookie уже SameSite=Lax
- CSP перечисляет директивы явно (script-src/worker-src/manifest-src 'self',
  style-src с inline для React, img-src с внешними https для аватаров вебхуков,
  connect-src с доменом файлов и LiveKit); добавлены Permissions-Policy,
  Cross-Origin-Opener-Policy и X-Permitted-Cross-Domain-Policies
- лимиты по AGENT.md 8.6: загрузки 10/мин и 100/сутки (вложения и аватары),
  реакции 20/мин; администратор инстанса лимиты обходит
- step-up: смена прав роли (в теле PATCH) и удаление сервера (перед вызовом
  /auth/step-up) требуют свежего подтверждения личности
- тесты: Origin (свой/чужой/GET), состав CSP, лимит реакций, step-up на права
  роли; исправлен вызов NewRateLimiter (второй аргумент — burst, не окно)
2026-09-22 00:40:10 +03:00
grendervill 1cb949a7a4 feat(ops): проверка бэкапа и уборка файлов без записи
- glchat verify-backup: расшифровка age, integrity_check, foreign_key_check,
  схема и состав данных, сверка с манифестом; живые данные не трогает
- чистка удаляет файлы на диске, у которых не осталось записи в базе (так
  оставались вложения удалённых серверов), и пустые подкаталоги; работает
  через os.Root, чтобы символическая ссылка не вывела за каталог файлов
- удаление сервера теперь чистит и содержимое файлов, а не только записи
- install.sh пишет значения .env с пробелами в кавычках: файл подключается
  через `set -a && . .env`, иначе имя главного сервера выполнялось как команда
- тесты: Go на уборку файлов без записи (свежие не трогаются, пустые
  каталоги удаляются)
2026-09-21 23:33:04 +03:00
grendervill ef871bcd96 feat(instance): админ-панель с лимитами медиа и действиями над пользователями
- лимиты медиа живут в настройках инстанса (миграция 00016): аватары,
  оформление сервера, эмодзи, звуки и галерея; все загрузки берут предел
  оттуда, значения по умолчанию — из AGENT.md 7.7
- действия администратора: временный пароль (показывается один раз, сессии
  отзываются), выход со всех устройств, мягкое удаление пользователя
  (сообщения и аудит остаются), переименование и удаление любого сервера
- админ-панель разбита на вкладки: обзор и здоровье, лимиты, пользователи,
  серверы, аудит; смена администраторов подтверждается личностью (step-up)
- тесты: Go (действия администратора, лимит эмодзи из настроек) и Vitest
  (вкладки, сброс пароля, выход, удаление, переименование сервера)
2026-09-21 22:37:47 +03:00
grendervill dd4432452c fix(web): манифест PWA отдаётся с правильным типом
Go определяет .webmanifest как text/plain, из-за чего браузер мог отказаться
ставить приложение: тип задаём явно вместе с js/css/svg.
2026-09-21 19:45:25 +03:00
grendervill 5afcfee744 feat(ops): чистка данных, reindex и PWA с офлайн-режимом
- internal/retention: истёкшие сессии, аудит старше RETENTION_AUDIT_DAYS и
  файлы без ссылок старше RETENTION_ORPHAN_HOURS; сиротой файл считается
  только если на него не ссылается ничего (вложения, аватары, оформление
  сервера, комнат, ролей, эмодзи, звуки, вебхуки, приглашения)
- обслуживание запускается в приложении по RETENTION_INTERVAL_SECONDS;
  команды glchat cleanup и glchat reindex делают то же вручную
- PWA: манифест с иконками и shortcuts, офлайн-страница, сервис-воркер
  (кэш только /assets/*, приватные запросы не кэшируются)
- тесты: Go на чистку (сирота удаляется, используемые файлы и свежие сироты
  остаются, истёкшая сессия уходит), Vitest на манифест и регистрацию воркера
2026-09-21 19:42:12 +03:00
grendervill 72537e9ff9 fix(invites): картинки оформления отдаются без сессии для страницы приглашения
- GET /files/{id} без сессии отдаёт только оформление сервера и картинку
  приглашения (guild_icon, guild_banner, guild_splash, invite_background):
  их показывают до входа (AGENT.md 7.9), остальное по-прежнему требует сессии
- optionalUser резолвит сессию без ответа об ошибке; путь файла проверяется на
  принадлежность каталогу данных (AGENT.md 9.2)
- аватар пригласившего показывается только вошедшим
- тесты: Go (splash анонимно, вложение закрыто) и правки живого скрипта
2026-09-21 19:30:25 +03:00
grendervill 566e1cf981 feat(invites): страница приглашения со splash и переопределением
- миграция 00015: переопределение splash у приглашения (AGENT.md 7.9)
- GET /invites/{code} доступен без сессии: гость по ссылке видит карточку
  сервера, описание, число участников и пригласившего
- POST/DELETE /invites/{code}/background: своя картинка страницы под правом
  создателя или MANAGE_GUILD, файл удаляется при замене и снятии
- клиент: маршрут /invite/:code с фоном (переопределение → splash → баннер),
  карточкой сервера, кнопками «Присоединиться»/«Открыть сервер» и входом с
  возвратом на страницу (?next=)
- тесты: Go (карточка, загрузка, права, снятие) и Vitest (splash, гость,
  присоединение, недействительная ссылка)
2026-09-21 19:25:06 +03:00
grendervill ed98c78c9e feat(social): блокировка пользователей
- PUT/DELETE /users/@me/blocks/{id}: блокировка снимает дружбу и заявки в обе
  стороны, разблокировка возвращает возможность писать (AGENT.md 7.2)
- блокировка запрещает личные сообщения (ошибка dm.blocked), открытие беседы и
  заявки в друзья с любой стороны
- клиент: раздел «Заблокированные» в друзьях со снятием блокировки, действие
  «Заблокировать» в меню строки друга и в меню модерации участника
- тесты: Go (блокировка, заявки, беседа, сообщение) и Vitest (список, снятие,
  блокировка из меню)
2026-09-21 19:21:00 +03:00
grendervill afcdf9c1c4 fix(guilds): фон комнаты в READY и без дублей категорий в шаблонах
- ReadyChannel отдаёт background_file_id: после перезагрузки фон комнаты больше
  не теряется до REST-запроса (AGENT.md 7.5)
- шаблон больше не создаёт категории дважды: они перечислены в шаблоне и
  раньше создавались ещё и по ссылке из комнаты
- тест: структура «Сообщества» ровно 7 комнат без повторов
2026-09-21 18:16:18 +03:00
grendervill b43452aa04 feat(profile): системные бейджи рядом с ником
- участники сервера отдают список бейджей из badges_json (AGENT.md 7.2):
  список расширяемый, назначается только системой
- клиент: каталог бейджей (щит администратора инстанса, корона владельца,
  ранний сторонник, проверенный), неизвестные показываются нейтральной меткой
- бейджи видны в ленте, списке участников и друзьях вместо текстовой плашки
- тесты: Go (бейджи в списке участников) и Vitest (каталог и отрисовка)
2026-09-21 18:10:47 +03:00
grendervill 0f0060e8a4 feat(channels): фон комнаты и статичные превью при reduced-motion
- миграция 00014: background_file_id у комнаты (AGENT.md 7.5, 7.7)
- API: POST/DELETE /channels/{id}/background под MANAGE_CHANNEL_BACKGROUND,
  фон отдаётся в списке комнат и рассылается событием CHANNEL_UPDATE
- клиент: фон рисуется за лентой с затемнением, блок «Фон комнаты» в настройках
  комнаты (загрузка, замена, удаление)
- AnimatedImage: при «уменьшить движение» показывает статичный первый кадр,
  нарисованный на canvas, вместо анимации
- тесты: Go (загрузка, права, снятие) и Vitest (лента, настройки, reduced-motion)
2026-09-21 18:08:21 +03:00
grendervill 91f415e36d feat(guilds): встроенные шаблоны серверов
- каталог шаблонов «Пустой», «Сообщество», «Игровой» (AGENT.md 7.3): роли,
  категории, текстовые и голосовые комнаты без сообщений
- POST /guilds принимает template_id, неизвестный шаблон отклоняется; аудит
  фиксирует выбранный шаблон
- GET /guild-templates отдаёт каталог с предпросмотром структуры
- клиент: выбор шаблона в окне создания сервера
- тесты: Go (структура каждого шаблона) и Vitest (выбор шаблона)
2026-09-21 18:04:52 +03:00
grendervill a36ee9527b feat(cosmetics): оформление «для друзей» в друзьях и личных беседах
- API: список друзей отдаёт вычисленное оформление «для друзей» одним запросом
  на весь список (AGENT.md 7.2)
- клиент: рамка, иконка, цвет и эффект ника в списке друзей, в ленте личных
  бесед — из профиля собеседника
- тесты: Go на список друзей, Vitest на отрисовку в друзьях
2026-09-21 01:48:56 +03:00
grendervill 75b71da7b7 fix(roles): роли на себя выдавать можно, самомодерация запрещена
Владелец сервера или участник с MANAGE_ROLES не мог выдать роль себе: общий
запрет самомодерации (кик, бан, тайм-аут) распространялся и на оформление.
Проверка иерархии для ролей вынесена отдельно: себя менять можно, владельца и
администратора инстанса — по-прежнему нет.
2026-09-21 01:40:17 +03:00
grendervill 0646faae2a fix(cosmetics): область стиля в ответе и ожидание данных в проверке
- GET /users/@me/styles отдаёт запись для каждой области, даже если стиль ещё
  не задан: иначе клиент не мог выбрать пер-серверную область
- живая проверка ждёт загрузки галереи и вариантов рамок
- уточнён приоритет: эффект роли важнее личного стиля (AGENT.md 7.2)
2026-09-21 01:32:48 +03:00
grendervill 48e30a1897 feat(cosmetics): оформление профиля — галерея, роли и личные стили
- миграция 00013: guild_cosmetics (рамки и иконки) и user_styles («для друзей»
  и пер-серверные стили) (AGENT.md 7.2, 7.4)
- store: CRUD галереи, личные стили и вычисление оформления поэлементно
  (роль → пер-серверный стиль → «для друзей» → дефолт) с откатом, когда роль
  больше не выдаёт элемент
- API: галерея сервера под MANAGE_ROLES, оформление роли в create/update,
  GET/PUT/DELETE /users/@me/styles с доступными элементами по областям
- клиент: редактор личного стиля с предпросмотром, галерея оформления в
  настройках сервера, оформление роли в разделе «Роли»
- рендер: рамка под аватаром, иконка рядом с ником, цвет и встроенные эффекты
  ника (CSS, при reduced-motion статично) в ленте и списке участников
2026-09-21 01:28:48 +03:00
grendervill 2e038a1d3f feat(webhooks): вебхуки комнат — бэкенд, клиент и настройки комнаты
- миграция 00012: таблица webhooks, снимок имени и аватара в messages (AGENT.md 7.11)
- API: список/создание/правка/удаление и пересоздание токена (MANAGE_WEBHOOKS,
  step-up при создании, аудит), загрузка аватара отдельной multipart-ручкой
- исполнение POST /webhooks/{id}/{token} без сессии: content, username,
  avatar_url, файлы создателя вебхука, лимит 30/мин на вебхук
- клиент: пункт настроек «Комната» со списком вебхуков, копированием ссылки,
  пересозданием токена и удалением
- сообщения вебхуков: имя, аватар и значок в ленте вместо «неизвестного автора»
- fix: неполный ответ REST больше не затирает данные READY-снапшота
2026-09-21 00:26:50 +03:00
grendervill 4ea2965892 fix(web): баннер сервера из снапшота, приглашение, встроенные звуки
- READY-снапшот больше не теряет banner/splash/accent: шапка сервера
  показывает баннер сразу после подключения (AGENT.md 7.4)
- GUILD_UPDATE несёт всё оформление, клиент применяет его без перезагрузки
- кнопка приглашения в шапке сервера со ссылкой и копированием
- встроенные звуки интерфейса на Web Audio с разблокировкой по жесту
- object URL превью через безопасные обёртки (jsdom и урезанные webview)
2026-09-21 00:04:49 +03:00
grendervill 6882516340 feat(web): оформление сервера в настройках, рейке и приглашении
Фаза 4 (AGENT.md 7.4): интерфейс для иконки, баннера, splash и акцентного цвета.

- секция «Оформление» в настройках сервера: загрузка и удаление иконки,
  баннера и splash с превью, проверка типа и размера на клиенте, пресеты
  акцентного цвета;
- иконка сервера показывается в рейке (заглушка-буквы остаётся, если файла
  нет), акцентный цвет переопределяет `--color-accent` внутри приложения и
  снимается при значении 0;
- предпросмотр приглашения показывает splash фоном и иконку сервера;
- ответ сервера обновляет карточку и список серверов — оформление видно без
  перезагрузки;
- тесты: превью всех трёх видов, multipart-загрузка, удаление, применение и
  снятие акцента, отказ для не-изображения, скрытие секции без MANAGE_GUILD.
2026-09-20 23:34:05 +03:00
grendervill 02a5b99418 feat(guilds): оформление сервера — иконка, баннер, splash и акцентный цвет
Фаза 4 (AGENT.md 7.4): сервер можно оформить, поля в схеме были с Фазы 1, но
ручек и отдачи в API не было.

- `POST/DELETE /guilds/{id}/appearance/{icon|banner|splash}` (multipart,
  MANAGE_GUILD, только изображения до 8 МБ): файл сохраняется назначением
  `guild_icon|guild_banner|guild_splash`, предыдущий удаляется, пишется аудит
  и уходит `GUILD_UPDATE`; ответ — обновлённый сервер;
- `PATCH /guilds/{id}` принимает `accent_color` (0 — цвет темы);
- оформление отдаётся в READY, в списке серверов, в детальной карточке и в
  предпросмотре приглашения — клиенту не нужен отдельный запрос;
- анимированные GIF/APNG/WebP сохраняются как есть: изображения не
  перекодируются, поэтому анимация не теряется;
- тесты: загрузка и очистка всех трёх видов, замена файла, запрет без
  MANAGE_GUILD и без сессии, отказ для не-изображения, акцентный цвет.
2026-09-20 23:26:46 +03:00
grendervill f1a195ec9d feat(web): меню модерации участника в списке сервера
Банить и исключать нужно того, кто прямо сейчас в сервере, а не только
разбирать готовый список банов (AGENT.md 7.17).

- `MemberModerationMenu`: тайм-аут (1/10/60/1440 минут и снятие), бан с причиной
  и исключение; пункты скрыты без прав KICK_MEMBERS/BAN_MEMBERS/TIMEOUT_MEMBERS,
  себя модерировать нельзя;
- после действия обновляются списки участников и банов (без F5);
- тесты: бан с причиной, тайм-аут, исключение и скрытие меню без прав.
2026-09-20 23:04:35 +03:00
grendervill dff4a25332 fix(moderation): администратор инстанса тоже не может модерировать себя
Проверка иерархии пропускала все запреты для администратора инстанса, включая
запрет на модерацию себя: он мог забанить или исключить самого себя и потерять
доступ к серверу (поймано при проверке интерфейса на стенде — самобан снял
участие в «Главном сервере»).

Теперь проверка `targetID == actor.ID` выполняется до обхода иерархии, для
администратора инстанса остаются доступны любые другие цели. Регрессионный
тест: самобан и самоисключение администратора инстанса дают 403.
2026-09-20 22:57:45 +03:00
grendervill 8613989b21 feat(web): разделы «Баны» и «Журнал аудита» в настройках сервера
Фаза 4 (AGENT.md 7.17, 7.18): модерация из интерфейса.

- `api/moderation.ts`: список банов, бан, разбан, тайм-аут, кик и журнал
  аудита с фильтрами;
- «Баны»: причина и автор каждого бана, снятие бана и пресеты тайм-аута
  (1/5/10/60/1440 минут) прямо из списка; ответ сервера обновляет кэш, поэтому
  строка исчезает без перезагрузки;
- «Журнал аудита»: фильтры по действию, автору и цели уходят в запрос,
  кнопки «Показать» и «Сбросить»;
- секции видны только с правами BAN_MEMBERS и VIEW_AUDIT_LOG
  (`canBanMembers`, `canViewAuditLog`);
- тесты: список с причиной и автором, снятие бана с обновлением списка,
  тайм-аут с проверкой времени, скрытие секций без прав, фильтры в запросе.
2026-09-20 22:43:56 +03:00
grendervill 4fb3509128 feat(moderation): баны сервера, тайм-ауты и фильтры журнала аудита
Фаза 4 (AGENT.md 7.17, 7.18): модерация участников.

- миграция 00011: таблица `guild_bans` (бан переживает исключение участника);
- `PUT /guilds/{id}/bans/{user_id}` — бан с причиной и права `BAN_MEMBERS`:
  исключает участника, отключает его от голосовой комнаты, пишет системное
  сообщение, аудит и события `MEMBER_REMOVE`/`GUILD_DELETE`;
- `DELETE /guilds/{id}/bans/{user_id}` — снятие бана, `GET .../bans` — список
  с автором и причиной (только модераторам);
- вход по приглашению для забаненного отклоняется с `guild.banned`;
- тайм-аут уже был в схеме, но не работал: кэш прав комнат не сбрасывался при
  изменениях сервера, поэтому участник с тайм-аутом продолжал писать. Ключ
  кэша комнат теперь включает сервер, `InvalidateGuild` чистит и его;
- тайм-аут уходит в `MEMBER_UPDATE` (и снимается событием), истёкшие
  тайм-ауты не показываются в профиле участника;
- журнал аудита получил фильтры `action`, `actor_id`, `target_id`, `before_id`;
- тесты: полный цикл бана (включая запрет входа и разбан), права и иерархия,
  блокировка сообщений тайм-аутом, запрет тайм-аута на себя, фильтры аудита.
2026-09-20 22:38:48 +03:00
grendervill cc16c18b13 fix(web): пинг участника показывается только при наведении
Постоянная подпись в плитке занимала место и была не нужна: задержка теперь
живёт в подсказке индикатора связи («Отличное соединение, 46 мс — задержка до
голосового сервера»), а плитка осталась компактной. Тесты и e2e проверяют
`title`/`aria-label` индикатора вместо видимого текста.
2026-09-20 22:30:24 +03:00
grendervill 96acc40031 test(web): e2e проверяет пинг участника в плитке
Два реальных браузера в одной комнате: у каждого в плитке появляется своя
задержка («N мс», измеряется по WebRTC-статистике), а второй участник видит
пинг первого, пришедший по data-каналу LiveKit.
2026-09-20 22:15:39 +03:00
grendervill 3fe634c9bc feat(web): второй ряд дашборда и пинг участников в голосовой комнате
- дашборд инстанса: два ряда полукругов с подписями сверху — «Контейнер
  инстанса» (лимиты профиля) и «Сервер целиком» (вся машина);
- голосовая комната: рядом с качеством связи видно пинг участника
  («Отличное соединение, 42 мс») и он обновляется раз в секунду. Свой RTT
  измеряется по статистике WebRTC, чужой приходит по data-каналу LiveKit
  (`glchat.latency`, lossy), поэтому пинг виден для каждого участника;
- статистика берётся с любой своей дорожки: в аудио-сессии RTT раньше был
  недоступен (отчёт читался только с видео);
- в «Аудио-видео» появился тумблер «Показывать пинг в голосовой комнате»:
  выключенный не опрашивает SDK и не публикует своё значение;
- тесты: ряд хоста и подписи, пинг в плитке своего и чужого участника,
  публикация по служебной теме, выключенный пинг не дёргает SDK.
2026-09-20 22:10:17 +03:00
grendervill 5668d779df feat(instance): метрики всей машины вторым рядом дашборда
Дашборд показывал только контейнер (лимиты профиля), поэтому не было видно,
сколько ресурсов у хоста всего.

- `sysinfo.NewHostSampler` и `ReadHostMemory` всегда читают `/proc/stat` и
  `/proc/meminfo`, игнорируя cgroup;
- в ответе метрик появились `host_cpu` и `host_memory` (`source: host`);
- недоступные источники попадают в проверку `metrics`, а не ломают ручку.
2026-09-20 22:10:09 +03:00
grendervill 3cfab89965 fix(web): до первого ответа метрик показывать прочерк вместо нулей
Ноль — такое же значение нагрузки, поэтому до прихода первой порции данных
индикаторы и блоки диска показывают «—», а не 0 %: иначе интерфейс мигал
нулевой нагрузкой при открытии раздела.
2026-09-20 21:37:42 +03:00
grendervill ce6a566f83 fix(sysinfo): переносимое приведение размера блока statfs
`statfs.Bsize` в Linux — int64, в Darwin — uint32: сборка образа для linux/amd64
падала на приведении к uint32. Приведение к int64 безопасно на обеих
платформах; проверено `GOOS=linux go vet` по всем пакетам.
2026-09-20 21:28:08 +03:00
grendervill e1107600ec feat(web): дашборд инстанса в настройках с живыми метриками
Вкладка «Инстанс» сверху показывает нагрузку сервера, ниже — диск и базу, внизу
healthcheck с пингом (AGENT.md 7.19).

- `Gauge` — полукруглый индикатор: заполняется по значению, меняет цвет на
  порогах 70 % и 90 %, доступен как `role="meter"`;
- процессор и память обновляются раз в секунду (`useInstanceMetrics`), для
  простоя вкладки опрос останавливается;
- два блока диска: «занято / всего» и вес базы (файл + WAL); видимых подписей
  нет — они раскрываются в тултипе при наведении на значение;
- healthcheck: состояние каждой проверки, общий статус, техническая деталь и
  живой пинг — время ответа инстанса на запрос дашборда в миллисекундах;
- переводы ru/en, тесты на индикаторы, формат «занято / всего», тултипы,
  healthcheck и отсутствие раздела у обычного пользователя.
2026-09-20 21:26:25 +03:00
grendervill 4c3736b5fb feat(instance): метрики и healthcheck инстанса для дашборда
Раздел «Инстанс» получает живые показатели сервера (AGENT.md 7.19).

- `internal/sysinfo`: загрузка процессора по cgroup v2 (`cpu.stat`/`cpu.max`)
  с откатом на `/proc/stat`, память по лимиту контейнера либо `/proc/meminfo`,
  место на разделе данных и размер базы вместе с журналом WAL;
- `GET /api/v1/instance/metrics` (только администратор инстанса): метрики,
  состояние (`ok|warn|failed`), проверки app/database/disk/storage/voice/
  gateway, версия и время работы; недоступные источники не ломают ответ, а
  попадают в проверку `metrics`;
- тяжёлые проверки (БД, SFU, запись) кэшируются на 5 секунд: ручку можно
  опрашивать раз в секунду;
- `voice.IssueRoomList` и `AdminClient.Ping` проверяют доступность RoomService
  и валидность ключей LiveKit (та же связка, что ломала модерацию);
- у метрик свой лимит частоты, они исключены из общего лимита API
  (`RateLimiter.MiddlewareExcept`), иначе открытый раздел съедал бы половину
  бюджета запросов;
- тесты: `internal/sysinfo` (cgroup, /proc, память, диск, размеры файлов) и
  `internal/server/api_metrics_test.go` (доступ, состав метрик, кэш, 429).
2026-09-20 21:26:17 +03:00
grendervill 8ae8ca1a1d test(web): e2e голоса различает отсутствие пользователя и лимит частоты
Регистрация тестового пользователя выполнялась на любой неудачный вход, из-за
чего при `rate_limited` прогон падал на попытке регистрации. Теперь:
- 401 → пользователя нет, регистрируем; 429 → ждём `retry_after_ms` и повторяем;
- 409 при регистрации (аккаунт уже есть) → просто входим;
- число повторов увеличено до шести.
Прогон зелёный, аккаунт `voiceprobe` переиспользуется.
2026-09-20 20:57:54 +03:00
grendervill 197c3d1a82 chore: отключать таймер сертификата TURN при внешнем релее
Если встроенный TURN выключен (`--no-turn`), сертификат LiveKit больше не
нужен: coturn на внешнем узле обновляет свой сертификат сам (D-036).
Повторная установка теперь снимает старый `glchat-cert-refresh.timer`,
юниты и скрипт, чтобы не оставлять в системе неработающую автоматику.
Заодно в .gitignore добавлены артефакты Playwright (`web/test-results/`,
`web/playwright-report/`) — они локальные и не должны попадать в коммиты.
2026-09-20 20:52:41 +03:00
grendervill 8b477ac314 feat(deploy): TURN/TLS анонсируется именем хоста с сертификатом
Браузеры отклоняют TLS-соединение к TURN по IP, если сертификат выписан на
домен: релей с публичным адресом отдавал `turns:<ip>:5349`, и этот эндпоинт был
нерабочим.

- флаг `--turn-relay-tls-host` (по умолчанию `turn.<домен>`, если он резолвится,
  иначе адрес релея) попадает в `rtc.turn_servers` как TLS-эндпоинт;
- значение сохраняется в `.env` и переиспользуется при `--reconfigure`;
- `scripts/test-install.sh` проверяет, что в `livekit.yaml` попали и UDP-адрес,
  и TLS-имя релея.
2026-09-20 20:48:32 +03:00
grendervill 4a0b70ab24 test(web): e2e голоса переиспользует тестового пользователя и терпит лимиты
- постоянный аккаунт `voiceprobe` вместо регистрации нового на каждый прогон
  (API удаления пользователей пока нет — это Фаза 5);
- `postWithRetry` и повтор входа в браузере учитывают `rate_limited` и
  `retry_after_ms`: защита от флуда входа обязана работать, а тест не должен
  падать из-за неё;
- проверено два прогона подряд: оба зелёные, в базе стенда остаются только
  аккаунт администратора, тестовый пользователь и серверы владельца.
2026-09-20 20:07:10 +03:00
grendervill 4ba449670a fix(voice): читать ответ GetParticipant в формате LiveKit 1.x
RoomService отвечает объектом участника на верхнем уровне, а не в обёртке
`{"participant": …}`: дорожка микрофона не находилась и серверный мьют молча
пропускался (AGENT.md 7.14). Обёртку тоже принимаем — на случай смены формы
ответа в новых версиях.

- `participantEnvelope` разбирает оба варианта;
- тесты `internal/voice/admin_test.go`: выбор дорожки микрофона (не камеры),
  тело MutePublishedTrack, отсутствие микрофона, обёрнутый ответ,
  RemoveParticipant и выключенный клиент; в каждом вызове проверяется
  Bearer-токен, привязанный к комнате.
2026-09-20 19:59:04 +03:00
grendervill a6d23a295e fix(voice): административный токен RoomService привязан к комнате
LiveKit принимает право `roomAdmin` только вместе с именем комнаты: без
`video.room` RoomService отвечает 401 `permissions denied` — серверный мьют и
отключение участника не доходили до SFU, хотя API отвечал 200 (AGENT.md 7.14).

- `voice.IssueAdmin(room)` кладёт в грахт `room` и `roomAdmin` и отклоняет
  пустое имя комнаты;
- `AdminClient.call` выпускает токен под конкретную комнату каждого вызова
  (GetParticipant, MutePublishedTrack, RemoveParticipant, ListParticipants);
- тест на состав грахта и отказ при пустой комнате;
- e2e: администратор и гость работают в отдельных api-контекстах (код 2FA
  одноразовый), нажатие «Войти» в комнату не ждёт стабильности исчезающей
  кнопки.
2026-09-20 19:54:21 +03:00
grendervill 912a067c9a test(web): e2e-стенд Playwright для голосовой комнаты
Проверка голоса двумя реальными браузерами (AGENT.md 11.2): два контекста с
фейковыми медиа-устройствами входят в одну комнату, инструментируется
`RTCPeerConnection` и по `inbound-rtp` подтверждается, что аудио реально
доходит; затем серверный мьют блокирует микрофон у гостя, а отключение
участника убирает его плитку у обоих клиентов.

- `playwright.config.ts`: прогон против развёрнутого инстанса (`GLCHAT_URL`),
  трейс при падении, флаги фейковых медиа-устройств;
- `e2e/voice.spec.ts`: подготовка через REST (сервер, комната, инвайт, гость),
  завершение онбординга, уборка тестовых серверов и включение только по
  `GLCHAT_E2E_VOICE=1`;
- `tsconfig.e2e.json` и запись в eslint для новых файлов.
2026-09-20 19:30:46 +03:00
grendervill 619bd91df4 feat(voice): саундборд, звуковая палитра, push-to-talk и статистика соединения
Клиентская часть Фазы 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.
2026-09-20 19:30:43 +03:00