diff --git a/src/core/Deal.Api/Telegram/TgStatusService.cs b/src/core/Deal.Api/Telegram/TgStatusService.cs index 9a6a4e7..1ba58ba 100644 --- a/src/core/Deal.Api/Telegram/TgStatusService.cs +++ b/src/core/Deal.Api/Telegram/TgStatusService.cs @@ -22,6 +22,7 @@ public sealed class TgStatusService( TelegramKeysService keys) { private const string IdlePhase = "idle"; + private const string ReadyPhase = "ready"; // Опции JSON KV-значений статуса: camelCase (как пишет ингресс) + терпимость регистра. private static readonly JsonSerializerOptions KvJsonOptions = new() @@ -37,13 +38,19 @@ public sealed class TgStatusService( public async Task GetAsync(CancellationToken ct) { TelegramAccountStatusDto live = await ReadLiveAsync(ct).ConfigureAwait(false); - string account = await ReadAccountAsync(ct).ConfigureAwait(false); + // «Подключён» для UI = авторизован (phase ready). Транспортный connected сервиса + // означает лишь живость соединения и не гарантирует вход — в UI он даёт «зависание». + bool authorized = string.Equals(live.Phase, ReadyPhase, StringComparison.Ordinal); + // Живой account (имя из Telegram) приоритетнее KV: при QR-входе KV ещё не заполнен. + string account = string.IsNullOrEmpty(live.Account) + ? await ReadAccountAsync(ct).ConfigureAwait(false) + : live.Account; int monitored = (await dialogs.ListMonitoredIdsAsync(ct).ConfigureAwait(false)).Count; TgKeysSnapshot snapshot = await keys.GetAsync(ct).ConfigureAwait(false); return new TgStatusDto( Phase: live.Phase, - Connected: live.Connected, + Connected: authorized, Listener: live.Listener, Account: account, Monitored: monitored, diff --git a/src/frontend/src/components/settings/TelegramTab.vue b/src/frontend/src/components/settings/TelegramTab.vue index fd83edd..a0b253b 100644 --- a/src/frontend/src/components/settings/TelegramTab.vue +++ b/src/frontend/src/components/settings/TelegramTab.vue @@ -2,10 +2,9 @@ // Вкладка «Telegram» настроек: подключение аккаунта (QR / номер / код / // облачный пароль) и авто-мониторинг новых чатов. Ключи приложения // (api_id/api_hash) задаёт оператор глобально — у тенанта их нет. -// Опрос статуса при QR-входе живёт в SettingsView (watch на state.tgState), -// чтобы поведение при переключении вкладок осталось прежним. -import { ref } from 'vue' -import { state, connectTgStep, tgStart, disconnectTg } from '../../store.js' +// Пока идёт QR-вход, вкладка сама опрашивает статус и обновляет картинку. +import { onBeforeUnmount, onMounted, ref, watch } from 'vue' +import { state, connectTgStep, tgStart, disconnectTg, refreshTgStatus } from '../../store.js' import Icon from '../Icon.vue' import UiToggle from '../ui/ToggleSwitch.vue' @@ -14,6 +13,58 @@ const code = ref('') const tgPass = ref('') const qrTick = ref(0) +// QR-ссылка приходит после старта: перезагружаем картинку, когда она появилась/сменилась. +watch( + () => state.tgQrUrl, + () => { + if (state.tgState === 'qr') qrTick.value++ + }, +) + +// Таймер QR: пока идёт вход — периодически обновляем статус и саму картинку +// (токен Telegram меняется); при подключении показываем учётку и останавливаемся. +const QR_POLL_MS = 5000 +let qrTimer = null + +function stopQrTimer() { + if (qrTimer) { + clearInterval(qrTimer) + qrTimer = null + } +} + +function startQrTimer() { + stopQrTimer() + qrTimer = setInterval(async () => { + await refreshTgStatus() + // Продолжаем опрос, пока не авторизованы: переходной статус (phase ready до + // подтверждения транспорта) не должен останавливать обновление. + if (state.tgConnected) { + stopQrTimer() + return + } + if (state.tgState === 'idle') { + stopQrTimer() + return + } + qrTick.value++ + }, QR_POLL_MS) +} + +// Таймер стартует при входе в режим QR и сам останавливается при подключении/сбросе. +watch( + () => state.tgState, + (s) => { + if (s === 'qr') startQrTimer() + }, +) + +onMounted(async () => { + await refreshTgStatus() + if (state.tgState === 'qr') startQrTimer() +}) +onBeforeUnmount(stopQrTimer) + async function submitCode() { await connectTgStep(code.value) if (state.tgState === 'done') code.value = '' diff --git a/src/frontend/src/store/telegram.js b/src/frontend/src/store/telegram.js index 56a443a..e9b5153 100644 --- a/src/frontend/src/store/telegram.js +++ b/src/frontend/src/store/telegram.js @@ -5,11 +5,12 @@ import { state, toast, errMsg, fmtMsgTime } from './core.js' export function mapTgStatus(st) { if (!st) return - state.tgConnected = !!st.connected + const phase = st.phase || 'idle' + // «Подключён» = авторизован (phase ready); транспортный connected ненадёжен для UI. + state.tgConnected = phase === 'ready' || !!st.connected state.tgAccount = st.account || '' state.tgKeysSet = !!st.keysSet if (st.error) state.tgError = st.error - const phase = st.phase || 'idle' if (phase === 'ready') { state.tgState = 'done' } else if (['phone', 'code', 'password', 'qr'].includes(phase)) { @@ -147,10 +148,12 @@ export async function refreshTgStatus() { export async function tgStart() { state.tgError = '' if (state.tgQrMode) { - state.tgState = 'qr' try { + // Сначала стартуем QR на бэке, затем показываем картинку: иначе успевает запросить + // /api/tg/qr-image до готовности QR и остаётся пустым. const r = await api.post('/api/tg/start-qr') state.tgQrUrl = r.qrUrl || '' + state.tgState = 'qr' toast(t('settings.ssylka-dlya-vhoda-sgenerirovana-otkrojte'), { icon: 'send' }) } catch (e) { state.tgError = errMsg(e) diff --git a/src/frontend/src/views/SettingsView.vue b/src/frontend/src/views/SettingsView.vue index f0eb0e2..e16ef05 100644 --- a/src/frontend/src/views/SettingsView.vue +++ b/src/frontend/src/views/SettingsView.vue @@ -1,7 +1,6 @@