fix(web): браузер отдавал старый app.js, интерфейс выглядел не обновлённым

Владелец сообщил, что в маршрутизации не переставляются блоки, нигде нет
выбора аккаунта для роли и на «Обзоре» ничего нельзя изменить. На скриншоте
при этом видна кнопка «Изменить цепочку», которой в коде уже нет: A24 её
удалил вместе с renderTeam и renderProviders.

Причина не в логике. FileResponse отдавал app.js и style.css без заголовка
Cache-Control, поэтому браузер применял эвристическое кэширование и держал
скрипт от прошлой сборки. index.html при навигации перепроверялся и был
свежим — отсюда смесь нового текста подсказки со старыми кнопками, а
перетаскивание и выбор модели просто отсутствовали в загруженном коде.

Действие reorder_chain при этом исправно: проверено вызовом, порядок
цепочки меняется и сохраняется в router_profiles.yaml.

Заодно подключённые аккаунты выводятся первыми, а пустые слоты
(«не подключён») — в конце группы: рабочие карточки были разбросаны
между пустыми и их приходилось выискивать.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hermes Team 2026-08-25 00:32:42 +07:00
parent c49e6eac31
commit 158210dc01
2 changed files with 34 additions and 3 deletions

View file

@ -199,18 +199,32 @@ _STATIC_DIR = Path(__file__).resolve().parent / "static"
if _STATIC_DIR.is_dir(): if _STATIC_DIR.is_dir():
app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static") app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static")
# Ассеты обязаны перепроверяться при каждой загрузке.
# Без Cache-Control браузер применяет эвристическое кэширование и может
# неделями отдавать старый app.js: интерфейс выглядит обновлённым (index.html
# перепроверяется при навигации), а поведение остаётся от прошлой сборки.
_NO_CACHE = {"Cache-Control": "no-cache, must-revalidate"}
@app.get("/") @app.get("/")
@app.get("/index.html") @app.get("/index.html")
def index(): def index():
return FileResponse(str(_STATIC_DIR / "index.html")) return FileResponse(str(_STATIC_DIR / "index.html"), headers=_NO_CACHE)
@app.get("/app.js") @app.get("/app.js")
def _app_js(): def _app_js():
return FileResponse(str(_STATIC_DIR / "app.js"), media_type="application/javascript") return FileResponse(
str(_STATIC_DIR / "app.js"),
media_type="application/javascript",
headers=_NO_CACHE,
)
@app.get("/style.css") @app.get("/style.css")
def _style_css(): def _style_css():
return FileResponse(str(_STATIC_DIR / "style.css"), media_type="text/css") return FileResponse(
str(_STATIC_DIR / "style.css"),
media_type="text/css",
headers=_NO_CACHE,
)
@app.get("/snapshot.example.json") @app.get("/snapshot.example.json")
def _fixture(): def _fixture():

View file

@ -434,6 +434,23 @@ function renderAccountsView() {
return matchesSearch && matchesHealth; return matchesSearch && matchesHealth;
}); });
// Подключённые аккаунты идут первыми: владелец жаловался, что рабочие
// карточки разбросаны между пустыми слотами и их приходится выискивать.
// Пустые слоты ('не подключён') — всегда в конце группы.
filtered.sort((a, b) => {
const rank = (p) => {
if (p.health_state === 'not_configured') return 5;
if (p.is_cold_spare || !p.enabled || p.health_state === 'disabled') return 4;
if (p.health_state === 'not_tested') return 3;
if (p.health_state === 'auth_required' || p.health_state === 'auth_expired') return 2;
if (p.health_state === 'healthy') return 0;
return 1;
};
const d = rank(a) - rank(b);
if (d !== 0) return d;
return (a.display_name || '').localeCompare(b.display_name || '', 'ru');
});
if (filtered.length === 0) continue; if (filtered.length === 0) continue;
visibleProfiles += filtered.length; visibleProfiles += filtered.length;