diff --git a/src/antigravity_provider/router/web/static/app.js b/src/antigravity_provider/router/web/static/app.js index f93166e..d6adc46 100644 --- a/src/antigravity_provider/router/web/static/app.js +++ b/src/antigravity_provider/router/web/static/app.js @@ -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) { const profiles = ((currentSnapshot || {}).profiles_by_provider || {})[providerId] || []; if (!profiles.length) { return ''; } + // Роль слота показываем прямо в списке. Без этого выбор вслепую: слоты + // ag-spare-* и ag-cold-* не входят ни в одну цепочку, поэтому подключённый + // в них аккаунт честно не появляется ни в «Обзоре», ни в «Маршрутизации» — + // и выглядит это как пропажа. + const routed = profilesInRouting(); const free = []; const used = []; + const idle = []; profiles.forEach((p) => { 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 label = isFree - ? `${p.profile_id} — свободен` - : `${p.profile_id} — занят${who ? ': ' + who : ''}`; - (isFree ? free : used).push( - `` - ); + const state = isFree ? 'свободен' : `занят${who ? ': ' + who : ''}`; + const label = inRouting + ? `${p.profile_id} — ${state} · ${roles || 'в маршрутизации'}` + : `${p.profile_id} — ${state} · не участвует в маршрутизации`; + const opt = ``; + 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; @@ -2292,6 +2319,7 @@ async function startRedirectAuth(providerId) { const d = res.data || {}; window._wiz_redirect_session = d.session_id; window._wiz_redirect_provider = providerId; + window._wiz_redirect_slot_id = d.profile_id; const pastesUrl = d.paste_kind !== 'code'; const label = pastesUrl @@ -2377,7 +2405,7 @@ async function submitRedirectCallback() { if (res && res.ok) { stopRedirectAuthPolling(); - status.innerHTML = 'Аккаунт подключён'; + status.innerHTML = 'Аккаунт подключён' + redirectSlotRoleNote(); showToast('Аккаунт подключён', 'success'); fetchSnapshot(); return; @@ -2387,6 +2415,24 @@ async function submitRedirectCallback() { status.innerHTML = `${escapeHtml((res && res.message) || 'Не удалось завершить вход')}`; } +// Если слот не входит ни в одну цепочку, аккаунт не появится ни в «Обзоре», +// ни в «Маршрутизации» — и это выглядит как пропажа. Говорим об этом сразу. +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 `
Роль: ${escapeHtml(roles.join(', '))}
`; + } + return ``; +} + async function pollRedirectAuth() { const status = document.getElementById('redirect-auth-status'); if (!status) {