fix(install,e2e): установка на VPS владельца — четыре дефекта шаблонов и хелперов

Установка на VPS (Debian 13, 1 vCPU/2 ГБ) с внешним прокси на том же хосте
вскрыла дефекты, которые не проявлялись на стенде:

- compose: с --no-turn порты TURN всё равно публиковались, и стек не стартовал
  («failed to bind host port 0.0.0.0:3478/udp» — порт держит чужой coturn).
  Теперь строки портов TURN подставляет установщик (GLCHAT_TURN_PORTS).
- LiveKit: добавлен отдельный адрес публикации медиапортов
  (--livekit-media-bind-addr / GLCHAT_LIVEKIT_MEDIA_BIND): сигналинг остаётся
  внутренним для прокси, а 7881/tcp и 7882/udp доступны клиентам из интернета.
- external-proxy.conf: адреса апстримов берутся из фактической привязки
  (--app-bind-addr/--livekit-bind-addr), а не жёсткого 127.0.0.1.
- при --no-files-subdomain шаблоны внешнего прокси и встроенного Caddy писали
  два блока с одним адресом сайта (Caddy отвергает такой конфиг и в embedded
  режиме установка ломалась целиком): блок пользовательского контента теперь
  появляется только при отдельном домене (GLCHAT_FILES_BLOCK).
- e2e: postWithRetry/repeat429 читают retry_after_ms из конверта ошибки
  ({"error":{...}}) — без этого хелперы сдавались раньше, чем сервер разрешал
  повтор, и регистрация пробных аккаунтов на свежем инстансе падала.
- e2e: voice.spec.ts ищет комнату «Голос» точным именем: пока сайдбар
  дорисовывает сервер, видны комнаты предыдущего («Голос A»/«Голос B»).
This commit is contained in:
2026-09-27 17:40:58 +03:00
parent 1fd9734fff
commit a835903938
6 changed files with 110 additions and 39 deletions
+22 -5
View File
@@ -63,12 +63,22 @@ export async function postWithRetry(
api: APIRequestContext,
path: string,
data: Record<string, unknown>,
attempts = 6,
attempts = 8,
): Promise<Awaited<ReturnType<APIRequestContext['post']>>> {
let response = await api.post(path, { data });
for (let attempt = 1; attempt < attempts && response.status() === 429; attempt += 1) {
const payload = (await response.json().catch(() => ({}))) as { retry_after_ms?: number };
const wait = Math.min(Math.max(payload.retry_after_ms ?? 1_000, 500), 20_000);
// Пауза лежит в конверте ошибки (§8.5: {"error":{"retry_after_ms":…}}), а не
// в корне тела: без `error` хелпер ждал фиксированную секунду и сдавался
// раньше, чем сервер разрешал повтор — на свежем инстансе регистрация
// пробных аккаунтов из-за этого падала (VPS, 27.09.2026).
const payload = (await response.json().catch(() => ({}))) as {
retry_after_ms?: number;
error?: { retry_after_ms?: number };
};
const wait = Math.min(
Math.max(payload.error?.retry_after_ms ?? payload.retry_after_ms ?? 1_000, 500),
20_000,
);
await new Promise((resolve) => setTimeout(resolve, wait + 200));
response = await api.post(path, { data });
}
@@ -359,9 +369,16 @@ export async function repeat429(
): Promise<APIResponse> {
let response = await request();
for (let attempt = 1; attempt < attempts && response.status() === 429; attempt += 1) {
const payload = (await response.json().catch(() => ({}))) as { retry_after_ms?: number };
const payload = (await response.json().catch(() => ({}))) as {
retry_after_ms?: number;
error?: { retry_after_ms?: number };
};
// Ждём столько, сколько просит сервер: лимит — 120 запросов в минуту.
const wait = Math.min(Math.max(payload.retry_after_ms ?? 1_000, 1_000), 30_000);
// Пауза лежит в конверте ошибки (§8.5), поэтому читаем и `error`.
const wait = Math.min(
Math.max(payload.error?.retry_after_ms ?? payload.retry_after_ms ?? 1_000, 1_000),
30_000,
);
await new Promise((resolve) => setTimeout(resolve, wait + 250));
response = await request();
}
+6 -1
View File
@@ -177,7 +177,12 @@ test.describe('голос: два клиента в одной комнате',
await session.page
.getByRole('link', { name: `Открыть сервер ${fixture.guildName}`, exact: true })
.click();
await session.page.getByRole('link', { name: /Открыть комнату Голос/u }).click();
// Имя комнаты — точно: пока сайдбар дорисовывает новый сервер, в нём
// могут быть видны комнаты предыдущего (например, «Голос A»/«Голос B»
// служебных проверок), и нечёткий локатор ловил бы две ссылки.
await session.page
.getByRole('link', { name: 'Открыть комнату Голос', exact: true })
.click();
await joinVoice(session.page);
}