fix(web): мастер не говорил, участвует ли слот в маршрутизации
Владелец подключил аккаунт, увидел его в «Аккаунтах» — и не нашёл ни в «Обзоре», ни в «Маршрутизации». Поведение верное: эти два экрана показывают цепочки ролей, а в умолчаниях в цепочках участвуют только ag-orch-fallback и ag-w1..w4. Слоты ag-cold-* и ag-spare-2 не входят никуда, поэтому подключённый в них аккаунт там и не появится. Но мастер показывал все десять слотов одинаково, как «свободен», и выбор был вслепую — а результат выглядел как пропажа аккаунта. Теперь в списке слотов видна роль: «ag-w1 — свободен · coder-primary (primary)» против «ag-spare-2 — свободен · не участвует в маршрутизации». Участвующие идут первыми. После подключения слота вне цепочек показывается пояснение, где его добавить. Признак участия берётся из состава самих цепочек, а не из assigned_roles: холодный резерв и ag-spare-2 значатся с ролью "spare", которой среди шести маршрутизируемых ролей нет, так что проверка по названию роли давала бы неверный ответ. Проверено в браузере: шесть слотов показаны с настоящими ролями, четыре — с пометкой о неучастии; состав совпадает с разбором снапшота по цепочкам. 432 passed, ruff чисто. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
681c899cfd
commit
d4b99a49ef
1 changed files with 54 additions and 8 deletions
|
|
@ -2228,25 +2228,52 @@ async function pollDeviceAuth(providerId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Профили, реально участвующие в цепочках маршрутизации.
|
||||||
|
//
|
||||||
|
// Судить по assigned_roles нельзя: холодный резерв и ag-spare-2 значатся с
|
||||||
|
// ролью "spare", которой среди шести маршрутизируемых ролей нет. Поэтому
|
||||||
|
// берём состав самих цепочек — это точно и не зависит от названий ролей.
|
||||||
|
function profilesInRouting() {
|
||||||
|
const routing = (currentSnapshot || {}).routing || {};
|
||||||
|
const ids = new Set();
|
||||||
|
Object.values(routing).forEach((pipeline) => {
|
||||||
|
((pipeline && pipeline.nodes) || []).forEach((n) => {
|
||||||
|
if (n && n.profile_id) ids.add(n.profile_id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
// Список слотов провайдера из снимка: занятые помечены, свободные идут первыми.
|
// Список слотов провайдера из снимка: занятые помечены, свободные идут первыми.
|
||||||
function buildSlotOptions(providerId) {
|
function buildSlotOptions(providerId) {
|
||||||
const profiles = ((currentSnapshot || {}).profiles_by_provider || {})[providerId] || [];
|
const profiles = ((currentSnapshot || {}).profiles_by_provider || {})[providerId] || [];
|
||||||
if (!profiles.length) {
|
if (!profiles.length) {
|
||||||
return '<option value="">Список слотов ещё не получен</option>';
|
return '<option value="">Список слотов ещё не получен</option>';
|
||||||
}
|
}
|
||||||
|
// Роль слота показываем прямо в списке. Без этого выбор вслепую: слоты
|
||||||
|
// ag-spare-* и ag-cold-* не входят ни в одну цепочку, поэтому подключённый
|
||||||
|
// в них аккаунт честно не появляется ни в «Обзоре», ни в «Маршрутизации» —
|
||||||
|
// и выглядит это как пропажа.
|
||||||
|
const routed = profilesInRouting();
|
||||||
const free = [];
|
const free = [];
|
||||||
const used = [];
|
const used = [];
|
||||||
|
const idle = [];
|
||||||
profiles.forEach((p) => {
|
profiles.forEach((p) => {
|
||||||
const isFree = p.health_state === 'not_configured';
|
const isFree = p.health_state === 'not_configured';
|
||||||
|
const inRouting = routed.has(p.profile_id);
|
||||||
|
const roles = (p.assigned_roles || []).join(', ');
|
||||||
const who = p.email || p.account_identity || '';
|
const who = p.email || p.account_identity || '';
|
||||||
const label = isFree
|
const state = isFree ? 'свободен' : `занят${who ? ': ' + who : ''}`;
|
||||||
? `${p.profile_id} — свободен`
|
const label = inRouting
|
||||||
: `${p.profile_id} — занят${who ? ': ' + who : ''}`;
|
? `${p.profile_id} — ${state} · ${roles || 'в маршрутизации'}`
|
||||||
(isFree ? free : used).push(
|
: `${p.profile_id} — ${state} · не участвует в маршрутизации`;
|
||||||
`<option value="${escapeHtml(p.profile_id)}">${escapeHtml(label)}</option>`
|
const opt = `<option value="${escapeHtml(p.profile_id)}">${escapeHtml(label)}</option>`;
|
||||||
);
|
if (!inRouting) idle.push(opt);
|
||||||
|
else if (isFree) free.push(opt);
|
||||||
|
else used.push(opt);
|
||||||
});
|
});
|
||||||
return free.concat(used).join('');
|
// Свободные слоты с ролью — первыми: именно они дают работающий маршрут.
|
||||||
|
return free.concat(used, idle).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
let _redirectAuthTimer = null;
|
let _redirectAuthTimer = null;
|
||||||
|
|
@ -2292,6 +2319,7 @@ async function startRedirectAuth(providerId) {
|
||||||
const d = res.data || {};
|
const d = res.data || {};
|
||||||
window._wiz_redirect_session = d.session_id;
|
window._wiz_redirect_session = d.session_id;
|
||||||
window._wiz_redirect_provider = providerId;
|
window._wiz_redirect_provider = providerId;
|
||||||
|
window._wiz_redirect_slot_id = d.profile_id;
|
||||||
|
|
||||||
const pastesUrl = d.paste_kind !== 'code';
|
const pastesUrl = d.paste_kind !== 'code';
|
||||||
const label = pastesUrl
|
const label = pastesUrl
|
||||||
|
|
@ -2377,7 +2405,7 @@ async function submitRedirectCallback() {
|
||||||
|
|
||||||
if (res && res.ok) {
|
if (res && res.ok) {
|
||||||
stopRedirectAuthPolling();
|
stopRedirectAuthPolling();
|
||||||
status.innerHTML = '<span style="color:var(--status-healthy); font-weight:600;">Аккаунт подключён</span>';
|
status.innerHTML = '<span style="color:var(--status-healthy); font-weight:600;">Аккаунт подключён</span>' + redirectSlotRoleNote();
|
||||||
showToast('Аккаунт подключён', 'success');
|
showToast('Аккаунт подключён', 'success');
|
||||||
fetchSnapshot();
|
fetchSnapshot();
|
||||||
return;
|
return;
|
||||||
|
|
@ -2387,6 +2415,24 @@ async function submitRedirectCallback() {
|
||||||
status.innerHTML = `<span style="color:var(--status-error);">${escapeHtml((res && res.message) || 'Не удалось завершить вход')}</span>`;
|
status.innerHTML = `<span style="color:var(--status-error);">${escapeHtml((res && res.message) || 'Не удалось завершить вход')}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Если слот не входит ни в одну цепочку, аккаунт не появится ни в «Обзоре»,
|
||||||
|
// ни в «Маршрутизации» — и это выглядит как пропажа. Говорим об этом сразу.
|
||||||
|
function redirectSlotRoleNote() {
|
||||||
|
const pid = window._wiz_redirect_slot_id;
|
||||||
|
if (!pid) return '';
|
||||||
|
const all = (currentSnapshot || {}).all_profiles || [];
|
||||||
|
const prof = all.find((p) => p.profile_id === pid);
|
||||||
|
const roles = prof ? (prof.assigned_roles || []) : [];
|
||||||
|
if (profilesInRouting().has(pid)) {
|
||||||
|
return `<div style="margin-top:6px; font-size:12px; color:var(--text-muted);">Роль: ${escapeHtml(roles.join(', '))}</div>`;
|
||||||
|
}
|
||||||
|
return `<div class="modal-feedback info" style="margin-top:8px; font-size:12px;">
|
||||||
|
Слот <code>${escapeHtml(pid)}</code> не входит ни в одну цепочку, поэтому в
|
||||||
|
«Обзоре» и «Маршрутизации» аккаунт не появится. Добавьте его нужной роли
|
||||||
|
в разделе «Маршрутизация» кнопкой «+ Добавить».
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
async function pollRedirectAuth() {
|
async function pollRedirectAuth() {
|
||||||
const status = document.getElementById('redirect-auth-status');
|
const status = document.getElementById('redirect-auth-status');
|
||||||
if (!status) {
|
if (!status) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue