AGENT.md 11.3 требует фиксировать образы по digest: тег в реестре может быть
перезаписан. Закреплены базовые образы Dockerfile (node, golang, alpine) и
образы установщика (caddy, livekit-server). Digest'ы проверены на стенде:
сборка образа и пересоздание LiveKit проходят, caddy тянется по digest.
- /.well-known/security.txt уходил в SPA-заглушку (200 text/html): добавлена
копия в web/public и явный тип text/plain в статике (RFC 9116, AGENT.md 11.3);
- make security дополнен `npm audit --omit=dev --audit-level=high`;
- новая цель `make sbom` — CycloneDX-отчёт зависимостей (trivy, если есть).
- invalidateCurrentUser перечитывает профиль, а не только удаляет кэш: без
запроса AuthGuard не видит 401 и второе устройство остаётся в приложении
после logout-all или смены пароля (AGENT.md 11.6, пункт 12);
- сброс сессии и уход из последнего сервера инвалидируют список серверов:
пустой снапшот подставлял устаревший REST-ответ, и сервер возвращался в
рейку (видно и после кика);
- joinGuild проверяет бан сервера: публичный сервер больше не обходится
нажатием «Войти» вместо приглашения (AGENT.md 7.17, 7.20), тест расширен;
- e2e/voice-ten приведён к линту, console.log разрешён в e2e (диагностика
прогона), артефакты Playwright исключены из prettier.
Переключение профиля — повторная установка, и без флагов она переписала бы
compose на встроенный прокси. Теперь CLI восстанавливает домен, режим прокси,
адреса привязки, внешний TURN и ignore-ip из .env.
Поле никогда не заполнялось, поэтому /api/v1/instance всегда сообщал
voice_enabled:false, хотя голос настроен (клиент читает признак из /meta,
но контракт 6.5 обещает его и здесь).
Дефекты, найденные 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).
- 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*.
Сервер:
- 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).
trivy на стенде нашёл в v0.55.0 две MEDIUM-уязвимости x/crypto/ssh
(CVE-2026-56855, CVE-2026-78662) и неподдерживаемый openpgp; сам код их не
вызывает (govulncheck чист), но обновление убирает находки из отчёта AppSec.
Сервер:
- миграция 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.
- 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 обновлён, выгрузка и недоступный
приёмник проверены вживую.
- 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 после установки.
- 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, не окно)
- glchat verify-backup: расшифровка age, integrity_check, foreign_key_check,
схема и состав данных, сверка с манифестом; живые данные не трогает
- чистка удаляет файлы на диске, у которых не осталось записи в базе (так
оставались вложения удалённых серверов), и пустые подкаталоги; работает
через os.Root, чтобы символическая ссылка не вывела за каталог файлов
- удаление сервера теперь чистит и содержимое файлов, а не только записи
- install.sh пишет значения .env с пробелами в кавычках: файл подключается
через `set -a && . .env`, иначе имя главного сервера выполнялось как команда
- тесты: Go на уборку файлов без записи (свежие не трогаются, пустые
каталоги удаляются)
- лимиты медиа живут в настройках инстанса (миграция 00016): аватары,
оформление сервера, эмодзи, звуки и галерея; все загрузки берут предел
оттуда, значения по умолчанию — из AGENT.md 7.7
- действия администратора: временный пароль (показывается один раз, сессии
отзываются), выход со всех устройств, мягкое удаление пользователя
(сообщения и аудит остаются), переименование и удаление любого сервера
- админ-панель разбита на вкладки: обзор и здоровье, лимиты, пользователи,
серверы, аудит; смена администраторов подтверждается личностью (step-up)
- тесты: Go (действия администратора, лимит эмодзи из настроек) и Vitest
(вкладки, сброс пароля, выход, удаление, переименование сервера)
- internal/retention: истёкшие сессии, аудит старше RETENTION_AUDIT_DAYS и
файлы без ссылок старше RETENTION_ORPHAN_HOURS; сиротой файл считается
только если на него не ссылается ничего (вложения, аватары, оформление
сервера, комнат, ролей, эмодзи, звуки, вебхуки, приглашения)
- обслуживание запускается в приложении по RETENTION_INTERVAL_SECONDS;
команды glchat cleanup и glchat reindex делают то же вручную
- PWA: манифест с иконками и shortcuts, офлайн-страница, сервис-воркер
(кэш только /assets/*, приватные запросы не кэшируются)
- тесты: Go на чистку (сирота удаляется, используемые файлы и свежие сироты
остаются, истёкшая сессия уходит), Vitest на манифест и регистрацию воркера
- GET /files/{id} без сессии отдаёт только оформление сервера и картинку
приглашения (guild_icon, guild_banner, guild_splash, invite_background):
их показывают до входа (AGENT.md 7.9), остальное по-прежнему требует сессии
- optionalUser резолвит сессию без ответа об ошибке; путь файла проверяется на
принадлежность каталогу данных (AGENT.md 9.2)
- аватар пригласившего показывается только вошедшим
- тесты: Go (splash анонимно, вложение закрыто) и правки живого скрипта
- миграция 00015: переопределение splash у приглашения (AGENT.md 7.9)
- GET /invites/{code} доступен без сессии: гость по ссылке видит карточку
сервера, описание, число участников и пригласившего
- POST/DELETE /invites/{code}/background: своя картинка страницы под правом
создателя или MANAGE_GUILD, файл удаляется при замене и снятии
- клиент: маршрут /invite/:code с фоном (переопределение → splash → баннер),
карточкой сервера, кнопками «Присоединиться»/«Открыть сервер» и входом с
возвратом на страницу (?next=)
- тесты: Go (карточка, загрузка, права, снятие) и Vitest (splash, гость,
присоединение, недействительная ссылка)
- PUT/DELETE /users/@me/blocks/{id}: блокировка снимает дружбу и заявки в обе
стороны, разблокировка возвращает возможность писать (AGENT.md 7.2)
- блокировка запрещает личные сообщения (ошибка dm.blocked), открытие беседы и
заявки в друзья с любой стороны
- клиент: раздел «Заблокированные» в друзьях со снятием блокировки, действие
«Заблокировать» в меню строки друга и в меню модерации участника
- тесты: Go (блокировка, заявки, беседа, сообщение) и Vitest (список, снятие,
блокировка из меню)
- ReadyChannel отдаёт background_file_id: после перезагрузки фон комнаты больше
не теряется до REST-запроса (AGENT.md 7.5)
- шаблон больше не создаёт категории дважды: они перечислены в шаблоне и
раньше создавались ещё и по ссылке из комнаты
- тест: структура «Сообщества» ровно 7 комнат без повторов
- участники сервера отдают список бейджей из badges_json (AGENT.md 7.2):
список расширяемый, назначается только системой
- клиент: каталог бейджей (щит администратора инстанса, корона владельца,
ранний сторонник, проверенный), неизвестные показываются нейтральной меткой
- бейджи видны в ленте, списке участников и друзьях вместо текстовой плашки
- тесты: Go (бейджи в списке участников) и Vitest (каталог и отрисовка)
- миграция 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)
- каталог шаблонов «Пустой», «Сообщество», «Игровой» (AGENT.md 7.3): роли,
категории, текстовые и голосовые комнаты без сообщений
- POST /guilds принимает template_id, неизвестный шаблон отклоняется; аудит
фиксирует выбранный шаблон
- GET /guild-templates отдаёт каталог с предпросмотром структуры
- клиент: выбор шаблона в окне создания сервера
- тесты: Go (структура каждого шаблона) и Vitest (выбор шаблона)
- API: список друзей отдаёт вычисленное оформление «для друзей» одним запросом
на весь список (AGENT.md 7.2)
- клиент: рамка, иконка, цвет и эффект ника в списке друзей, в ленте личных
бесед — из профиля собеседника
- тесты: Go на список друзей, Vitest на отрисовку в друзьях
Владелец сервера или участник с MANAGE_ROLES не мог выдать роль себе: общий
запрет самомодерации (кик, бан, тайм-аут) распространялся и на оформление.
Проверка иерархии для ролей вынесена отдельно: себя менять можно, владельца и
администратора инстанса — по-прежнему нет.
- GET /users/@me/styles отдаёт запись для каждой области, даже если стиль ещё
не задан: иначе клиент не мог выбрать пер-серверную область
- живая проверка ждёт загрузки галереи и вариантов рамок
- уточнён приоритет: эффект роли важнее личного стиля (AGENT.md 7.2)
- миграция 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 статично) в ленте и списке участников
- миграция 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-снапшота
- READY-снапшот больше не теряет banner/splash/accent: шапка сервера
показывает баннер сразу после подключения (AGENT.md 7.4)
- GUILD_UPDATE несёт всё оформление, клиент применяет его без перезагрузки
- кнопка приглашения в шапке сервера со ссылкой и копированием
- встроенные звуки интерфейса на Web Audio с разблокировкой по жесту
- object URL превью через безопасные обёртки (jsdom и урезанные webview)
Фаза 4 (AGENT.md 7.4): интерфейс для иконки, баннера, splash и акцентного цвета.
- секция «Оформление» в настройках сервера: загрузка и удаление иконки,
баннера и splash с превью, проверка типа и размера на клиенте, пресеты
акцентного цвета;
- иконка сервера показывается в рейке (заглушка-буквы остаётся, если файла
нет), акцентный цвет переопределяет `--color-accent` внутри приложения и
снимается при значении 0;
- предпросмотр приглашения показывает splash фоном и иконку сервера;
- ответ сервера обновляет карточку и список серверов — оформление видно без
перезагрузки;
- тесты: превью всех трёх видов, multipart-загрузка, удаление, применение и
снятие акцента, отказ для не-изображения, скрытие секции без MANAGE_GUILD.
Фаза 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 и без сессии, отказ для не-изображения, акцентный цвет.
Банить и исключать нужно того, кто прямо сейчас в сервере, а не только
разбирать готовый список банов (AGENT.md 7.17).
- `MemberModerationMenu`: тайм-аут (1/10/60/1440 минут и снятие), бан с причиной
и исключение; пункты скрыты без прав KICK_MEMBERS/BAN_MEMBERS/TIMEOUT_MEMBERS,
себя модерировать нельзя;
- после действия обновляются списки участников и банов (без F5);
- тесты: бан с причиной, тайм-аут, исключение и скрытие меню без прав.
Проверка иерархии пропускала все запреты для администратора инстанса, включая
запрет на модерацию себя: он мог забанить или исключить самого себя и потерять
доступ к серверу (поймано при проверке интерфейса на стенде — самобан снял
участие в «Главном сервере»).
Теперь проверка `targetID == actor.ID` выполняется до обхода иерархии, для
администратора инстанса остаются доступны любые другие цели. Регрессионный
тест: самобан и самоисключение администратора инстанса дают 403.
Фаза 4 (AGENT.md 7.17, 7.18): модерация из интерфейса.
- `api/moderation.ts`: список банов, бан, разбан, тайм-аут, кик и журнал
аудита с фильтрами;
- «Баны»: причина и автор каждого бана, снятие бана и пресеты тайм-аута
(1/5/10/60/1440 минут) прямо из списка; ответ сервера обновляет кэш, поэтому
строка исчезает без перезагрузки;
- «Журнал аудита»: фильтры по действию, автору и цели уходят в запрос,
кнопки «Показать» и «Сбросить»;
- секции видны только с правами BAN_MEMBERS и VIEW_AUDIT_LOG
(`canBanMembers`, `canViewAuditLog`);
- тесты: список с причиной и автором, снятие бана с обновлением списка,
тайм-аут с проверкой времени, скрытие секций без прав, фильтры в запросе.
Фаза 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`;
- тесты: полный цикл бана (включая запрет входа и разбан), права и иерархия,
блокировка сообщений тайм-аутом, запрет тайм-аута на себя, фильтры аудита.
Постоянная подпись в плитке занимала место и была не нужна: задержка теперь
живёт в подсказке индикатора связи («Отличное соединение, 46 мс — задержка до
голосового сервера»), а плитка осталась компактной. Тесты и e2e проверяют
`title`/`aria-label` индикатора вместо видимого текста.
Два реальных браузера в одной комнате: у каждого в плитке появляется своя
задержка («N мс», измеряется по WebRTC-статистике), а второй участник видит
пинг первого, пришедший по data-каналу LiveKit.
- дашборд инстанса: два ряда полукругов с подписями сверху — «Контейнер
инстанса» (лимиты профиля) и «Сервер целиком» (вся машина);
- голосовая комната: рядом с качеством связи видно пинг участника
(«Отличное соединение, 42 мс») и он обновляется раз в секунду. Свой RTT
измеряется по статистике WebRTC, чужой приходит по data-каналу LiveKit
(`glchat.latency`, lossy), поэтому пинг виден для каждого участника;
- статистика берётся с любой своей дорожки: в аудио-сессии RTT раньше был
недоступен (отчёт читался только с видео);
- в «Аудио-видео» появился тумблер «Показывать пинг в голосовой комнате»:
выключенный не опрашивает SDK и не публикует своё значение;
- тесты: ряд хоста и подписи, пинг в плитке своего и чужого участника,
публикация по служебной теме, выключенный пинг не дёргает SDK.
Дашборд показывал только контейнер (лимиты профиля), поэтому не было видно,
сколько ресурсов у хоста всего.
- `sysinfo.NewHostSampler` и `ReadHostMemory` всегда читают `/proc/stat` и
`/proc/meminfo`, игнорируя cgroup;
- в ответе метрик появились `host_cpu` и `host_memory` (`source: host`);
- недоступные источники попадают в проверку `metrics`, а не ломают ручку.
Ноль — такое же значение нагрузки, поэтому до прихода первой порции данных
индикаторы и блоки диска показывают «—», а не 0 %: иначе интерфейс мигал
нулевой нагрузкой при открытии раздела.
`statfs.Bsize` в Linux — int64, в Darwin — uint32: сборка образа для linux/amd64
падала на приведении к uint32. Приведение к int64 безопасно на обеих
платформах; проверено `GOOS=linux go vet` по всем пакетам.
Вкладка «Инстанс» сверху показывает нагрузку сервера, ниже — диск и базу, внизу
healthcheck с пингом (AGENT.md 7.19).
- `Gauge` — полукруглый индикатор: заполняется по значению, меняет цвет на
порогах 70 % и 90 %, доступен как `role="meter"`;
- процессор и память обновляются раз в секунду (`useInstanceMetrics`), для
простоя вкладки опрос останавливается;
- два блока диска: «занято / всего» и вес базы (файл + WAL); видимых подписей
нет — они раскрываются в тултипе при наведении на значение;
- healthcheck: состояние каждой проверки, общий статус, техническая деталь и
живой пинг — время ответа инстанса на запрос дашборда в миллисекундах;
- переводы ru/en, тесты на индикаторы, формат «занято / всего», тултипы,
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).
Регистрация тестового пользователя выполнялась на любой неудачный вход, из-за
чего при `rate_limited` прогон падал на попытке регистрации. Теперь:
- 401 → пользователя нет, регистрируем; 429 → ждём `retry_after_ms` и повторяем;
- 409 при регистрации (аккаунт уже есть) → просто входим;
- число повторов увеличено до шести.
Прогон зелёный, аккаунт `voiceprobe` переиспользуется.
Если встроенный TURN выключен (`--no-turn`), сертификат LiveKit больше не
нужен: coturn на внешнем узле обновляет свой сертификат сам (D-036).
Повторная установка теперь снимает старый `glchat-cert-refresh.timer`,
юниты и скрипт, чтобы не оставлять в системе неработающую автоматику.
Заодно в .gitignore добавлены артефакты Playwright (`web/test-results/`,
`web/playwright-report/`) — они локальные и не должны попадать в коммиты.