fix(ui): настройка прокси исчезала с экрана

Владелец не нашёл настройку. Она была в разметке, но её не было на экране.

arrangeSettingsPanels пересобирает настройки по жёсткому списку
идентификаторов, переносит перечисленные строки в новые карточки, а исходную
удаляет целиком — вместе со всем, чего в списке нет. Новое поле попало под
удаление и просто перестало существовать.

Добавлена группа «Сеть и доступ» с полем прокси. Название уточнено до
«Прокси / VPN для провайдеров»: владелец называет это впном, и искать он будет
по этому слову.

Устройство, которое так теряет настройки, тоже исправлено: строки, не попавшие
ни в одну группу, собираются в карточку «Прочие настройки», а не выбрасываются.
Забыть настройку в списке всё ещё можно, потерять её с экрана — уже нет.
Обращение к отсутствующему элементу защищено: опечатка в списке больше не
роняет сборку экрана целиком.

Четыре теста закрывают это: каждый перечисленный идентификатор существует в
разметке, поле прокси сгруппировано, остаток забирается до удаления карточки.

704 passed, 2 skipped; ruff чисто; релизный гейт пройден.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hermes Team 2026-09-02 00:30:47 +07:00
parent 90aeceb9fd
commit e431e39915
6 changed files with 82 additions and 3 deletions

View file

@ -18,7 +18,7 @@ namespace HermesHubSetup
// Подставляется сборщиком из фактического git-коммита. Раньше здесь
// жил зашитый "8cddc9f", то есть манифест сообщал неправду о том, из
// какого кода собран установщик.
public const string BuildCommit = "fa7bbef";
public const string BuildCommit = "90aeceb";
public const string MIN_HERMES_VERSION = "0.20.0";
public const string MAX_TESTED_HERMES = "0.20.4";

Binary file not shown.

Binary file not shown.

View file

@ -457,7 +457,7 @@
</div>
<div class="setting-row">
<div class="setting-info">
<label class="setting-label" for="setting-provider-proxy-url">Прокси для провайдеров</label>
<label class="setting-label" for="setting-provider-proxy-url">Прокси / VPN для провайдеров</label>
<div class="setting-desc">Google отказывает по местоположению: «not currently available in your location». Выход через разрешённую страну снимает отказ. Пример: <code>socks5://127.0.0.1:1080</code>. Пусто — без прокси. Отдельному аккаунту можно задать свой адрес.</div>
</div>
<div class="setting-control"><input id="setting-provider-proxy-url" class="input-text" type="text" placeholder="Н/Д: загрузка" aria-label="Адрес прокси для обращений провайдеров"></div>

View file

@ -229,14 +229,32 @@ function arrangeSettingsPanels() {
['Общие настройки',['setting-default-role','setting-theme']],
['Управление квотами',['setting-account-check-interval','setting-quota-interval','setting-quota-threshold-percent','setting-quota-threshold-action']],
['Безопасность и API',['setting-server-host','setting-server-token-input','setting-email-masking-mode']],
['Сеть и доступ',['setting-provider-proxy-url']],
];
for (const [title,ids] of groups) {
const card = document.createElement('section'); card.className='settings-card';
const heading=document.createElement('h2'); heading.className='settings-group-title'; heading.textContent=title; card.append(heading);
for (const id of ids) card.append(document.getElementById(id).closest('.setting-row'));
for (const id of ids) {
const row = document.getElementById(id)?.closest('.setting-row');
if (row) card.append(row);
}
if (title === 'Управление квотами') card.append(document.getElementById('btn-save-hub-settings').closest('.settings-actions'));
view.insertBefore(card, first);
}
// Строки, которых нет ни в одной группе, раньше удалялись вместе с исходной
// карточкой: новая настройка просто пропадала с экрана, и найти её было
// нельзя. Так исчезло поле прокси. Теперь остаток не выбрасывается, а
// собирается отдельной карточкой — забыть настройку в списке всё ещё можно,
// но потерять её уже нет.
const leftovers = Array.from(first.querySelectorAll('.setting-row'));
if (leftovers.length) {
const card = document.createElement('section'); card.className='settings-card';
const heading=document.createElement('h2'); heading.className='settings-group-title';
heading.textContent='Прочие настройки'; card.append(heading);
for (const row of leftovers) card.append(row);
view.insertBefore(card, first);
}
first.remove();
}
document.addEventListener('DOMContentLoaded', arrangeSettingsPanels);

View file

@ -0,0 +1,61 @@
"""Экран настроек не должен молча терять поля.
Владелец не нашёл настройку прокси. Она была в разметке, но исчезала при
открытии экрана: arrangeSettingsPanels пересобирает настройки по жёсткому
списку идентификаторов, а исходную карточку удаляет целиком вместе со всем,
чего в списке нет.
Поле существовало в файле, но его не было на экране, и никакая проверка этого
не ловила.
"""
from __future__ import annotations
import re
from pathlib import Path
import pytest
STATIC = Path(__file__).resolve().parent.parent / "src" / "antigravity_provider" / "router" / "web" / "static"
@pytest.fixture(scope="module")
def markup() -> str:
return (STATIC / "index.html").read_text(encoding="utf-8")
@pytest.fixture(scope="module")
def workspace() -> str:
return (STATIC / "workspace.js").read_text(encoding="utf-8")
def _grouped_ids(workspace: str) -> list[str]:
block = workspace.split("const groups = [")[1].split("];")[0]
return re.findall(r"'(setting-[a-z0-9-]+)'", block)
def test_every_grouped_id_exists_in_the_markup(markup, workspace):
for element_id in _grouped_ids(workspace):
assert f'id="{element_id}"' in markup, f"{element_id} перечислен в группах, но его нет в разметке"
def test_proxy_setting_is_reachable(markup, workspace):
assert 'id="setting-provider-proxy-url"' in markup
assert "setting-provider-proxy-url" in _grouped_ids(workspace), (
"негруппированная строка удаляется вместе с исходной карточкой"
)
def test_unlisted_rows_are_kept_not_deleted(workspace):
"""Забыть настройку в списке можно, потерять её с экрана — нет."""
body = workspace.split("function arrangeSettingsPanels")[1].split("function ")[0]
assert "leftovers" in body, "остаток строк должен собираться, а не выбрасываться"
assert body.index("leftovers") < body.index("first.remove()"), (
"остаток надо забрать до удаления исходной карточки"
)
def test_missing_element_does_not_break_the_screen(workspace):
"""Опечатка в списке не должна ронять сборку экрана целиком."""
body = workspace.split("function arrangeSettingsPanels")[1].split("function ")[0]
assert "?.closest" in body, "обращение к возможному null должно быть защищено"