From a3a259bd3bc021b4c064a877a6ff7d2c1627b0c4 Mon Sep 17 00:00:00 2001 From: Ivan Neplokhov Date: Sun, 18 Jan 2026 23:29:16 +0400 Subject: [PATCH] some --- src/main/taskRunner.js | 17 +++ src/main/telegram.js | 103 +++++++++++---- src/renderer/App.jsx | 201 ++++++++++++++++++++++++------ src/renderer/styles/app.css | 16 +++ src/renderer/tabs/AccountsTab.jsx | 14 ++- src/renderer/tabs/EventsTab.jsx | 8 +- src/renderer/tabs/LogsTab.jsx | 47 +++++-- src/renderer/tabs/SettingsTab.jsx | 6 +- 8 files changed, 326 insertions(+), 86 deletions(-) diff --git a/src/main/taskRunner.js b/src/main/taskRunner.js index 3b9cd56..e28745a 100644 --- a/src/main/taskRunner.js +++ b/src/main/taskRunner.js @@ -194,8 +194,25 @@ class TaskRunner { result.strategy, result.strategyMeta ); + const detailed = [ + `user=${item.user_id}`, + `error=${result.error || "unknown"}`, + `strategy=${result.strategy || "—"}`, + `meta=${result.strategyMeta || "—"}`, + `source=${item.source_chat || "—"}`, + `account=${result.accountPhone || result.accountId || "—"}` + ].join(" | "); + this.store.addAccountEvent( + watcherAccount ? watcherAccount.id : 0, + watcherAccount ? watcherAccount.phone : "", + "invite_failed", + detailed + ); } } + if (!pending.length) { + errors.push("queue empty"); + } } } catch (error) { errors.push(error.message || String(error)); diff --git a/src/main/telegram.js b/src/main/telegram.js index 18660ff..3a7b8d8 100644 --- a/src/main/telegram.js +++ b/src/main/telegram.js @@ -1298,14 +1298,16 @@ class TelegramManager { : { accessHash: "", strategy: "", detail: "no sender_id", attempts: [] }; if (!resolved || !resolved.accessHash) { if (shouldLogEvent(`${chatId}:skip`, 30000)) { - const strategySummary = this._formatStrategyAttempts(resolved ? resolved.attempts : []); + const strategyLines = this._formatStrategyAttemptLines(resolved ? resolved.attempts : []); const reason = senderInfo && senderInfo.reason ? senderInfo.reason : "нет данных пользователя"; - const extra = strategySummary ? `; стратегии: ${strategySummary}` : ""; + const strategyBlock = strategyLines.length + ? `\nШаги попыток:\n- ${strategyLines.join("\n- ")}` + : ""; this.store.addAccountEvent( monitorAccount.account.id, monitorAccount.account.phone, "new_message_skipped", - `${formatGroupLabel(st)}: ${reason}${extra}, ${this._describeSender(message)}` + `${formatGroupLabel(st)}\nПричина: ${reason}${strategyBlock}\nОтправитель: ${this._describeSender(message)}` ); } return; @@ -1352,13 +1354,15 @@ class TelegramManager { } if (!senderPayload.accessHash && !senderPayload.username) { if (shouldLogEvent(`${chatId}:skip`, 30000)) { - const strategySummary = this._formatStrategyAttempts(resolved ? resolved.attempts : []); - const extra = strategySummary ? `; стратегии: ${strategySummary}` : ""; + const strategyLines = this._formatStrategyAttemptLines(resolved ? resolved.attempts : []); + const strategyBlock = strategyLines.length + ? `\nШаги попыток:\n- ${strategyLines.join("\n- ")}` + : ""; this.store.addAccountEvent( monitorAccount.account.id, monitorAccount.account.phone, "new_message_skipped", - `${formatGroupLabel(st)}: нет access_hash (нет в списке участников)${extra}` + `${formatGroupLabel(st)}\nПричина: нет access_hash (нет в списке участников)${strategyBlock}` ); } return; @@ -1444,14 +1448,16 @@ class TelegramManager { if (!resolved || !resolved.accessHash) { skipped += 1; if (shouldLogEvent(`${key}:skip`, 30000)) { - const strategySummary = this._formatStrategyAttempts(resolved ? resolved.attempts : []); + const strategyLines = this._formatStrategyAttemptLines(resolved ? resolved.attempts : []); const reason = senderInfo && senderInfo.reason ? senderInfo.reason : "нет данных пользователя"; - const extra = strategySummary ? `; стратегии: ${strategySummary}` : ""; + const strategyBlock = strategyLines.length + ? `\nШаги попыток:\n- ${strategyLines.join("\n- ")}` + : ""; this.store.addAccountEvent( monitorAccount.account.id, monitorAccount.account.phone, "new_message_skipped", - `${formatGroupLabel(st)}: ${reason}${extra}, ${this._describeSender(message)}` + `${formatGroupLabel(st)}\nПричина: ${reason}${strategyBlock}\nОтправитель: ${this._describeSender(message)}` ); } continue; @@ -1501,13 +1507,15 @@ class TelegramManager { if (!senderPayload.accessHash && !senderPayload.username) { skipped += 1; if (shouldLogEvent(`${key}:skip`, 30000)) { - const strategySummary = this._formatStrategyAttempts(resolved ? resolved.attempts : []); - const extra = strategySummary ? `; стратегии: ${strategySummary}` : ""; + const strategyLines = this._formatStrategyAttemptLines(resolved ? resolved.attempts : []); + const strategyBlock = strategyLines.length + ? `\nШаги попыток:\n- ${strategyLines.join("\n- ")}` + : ""; this.store.addAccountEvent( monitorAccount.account.id, monitorAccount.account.phone, "new_message_skipped", - `${formatGroupLabel(st)}: нет access_hash (нет в списке участников)${extra}` + `${formatGroupLabel(st)}\nПричина: нет access_hash (нет в списке участников)${strategyBlock}` ); } continue; @@ -1554,7 +1562,7 @@ class TelegramManager { monitorAccount.account.id, monitorAccount.account.phone, "monitor_skip", - `${formatGroupLabel(st)}: сообщения есть, но пользователей нет (пропущено: ${skipped})` + `${formatGroupLabel(st)}\nПричина: сообщения есть, но авторов нельзя определить (анонимные админы/каналы, скрытые участники, нет access_hash)\nПропущено: ${skipped}` ); } } @@ -1660,9 +1668,9 @@ class TelegramManager { const reason = senderInfo && senderInfo.reason ? senderInfo.reason : "нет данных пользователя"; skipReasons[reason] = (skipReasons[reason] || 0) + 1; if (!strategySkipSample) { - const strategySummary = this._formatStrategyAttempts(resolved ? resolved.attempts : []); - if (strategySummary) { - strategySkipSample = `${group}: ${reason}; стратегии: ${strategySummary}`; + const strategyLines = this._formatStrategyAttemptLines(resolved ? resolved.attempts : []); + if (strategyLines.length) { + strategySkipSample = `${group}\nПричина: ${reason}\nШаги попыток:\n- ${strategyLines.join("\n- ")}`; } } continue; @@ -1687,9 +1695,9 @@ class TelegramManager { senderPayload.accessHash = resolved.accessHash; } if (!senderPayload.accessHash && !senderPayload.username && !strategySkipSample) { - const strategySummary = this._formatStrategyAttempts(resolved ? resolved.attempts : []); - if (strategySummary) { - strategySkipSample = `${group}: нет access_hash; стратегии: ${strategySummary}`; + const strategyLines = this._formatStrategyAttemptLines(resolved ? resolved.attempts : []); + if (strategyLines.length) { + strategySkipSample = `${group}\nПричина: нет access_hash (нет в списке участников)\nШаги попыток:\n- ${strategyLines.join("\n- ")}`; } } } @@ -1806,14 +1814,57 @@ class TelegramManager { } } - _formatStrategyAttempts(attempts) { - if (!Array.isArray(attempts) || attempts.length === 0) return ""; - const parts = attempts.map((item) => { - const status = item.ok ? "ok" : "fail"; - const detail = item.detail ? String(item.detail).replace(/\s+/g, " ").slice(0, 80) : ""; - return detail ? `${item.strategy}:${status} (${detail})` : `${item.strategy}:${status}`; + _formatStrategyAttemptLines(attempts) { + if (!Array.isArray(attempts) || attempts.length === 0) return []; + return attempts.map((item, index) => { + const label = this._strategyLabelRu(item.strategy); + const status = item.ok ? "успех" : "не удалось"; + const detail = this._translateStrategyDetail(item.detail); + const detailText = detail ? ` — ${detail}` : ""; + return `${index + 1}) ${label} — ${status}${detailText}`; }); - return parts.join("; "); + } + + _strategyLabelRu(strategy) { + switch (strategy) { + case "access_hash": + return "access_hash из сообщения"; + case "participants": + return "участники группы"; + case "username": + return "поиск по username"; + case "entity": + return "getEntity по userId"; + case "retry": + return "повторная попытка"; + default: + return strategy || "стратегия"; + } + } + + _translateStrategyDetail(detail) { + if (!detail) return ""; + const raw = String(detail).trim(); + if (!raw) return ""; + const normalized = raw.toLowerCase(); + if (normalized === "from message") return "access_hash найден в сообщении"; + if (normalized === "invalid access_hash") return "access_hash невалиден"; + if (normalized === "from participants") return "найден в списке участников"; + if (normalized === "no result") return "нет результата"; + if (normalized === "resolve failed") return "не удалось найти пользователя по username"; + if (normalized === "getentity(userid)") return "получен через getEntity"; + if (normalized === "no access_hash") return "нет access_hash"; + if (normalized === "no sender_id") return "в сообщении нет sender_id"; + if (normalized.includes("could not find the input entity")) { + return "не удалось получить сущность пользователя (entity)"; + } + if (normalized.startsWith("not in participants")) { + const match = raw.match(/\((\d+)\)/); + const checked = match ? ` (проверено: ${match[1]})` : ""; + return `не найден в списке участников${checked}`; + } + if (normalized === "invalid") return "ошибка в данных пользователя"; + return `деталь: ${raw.replace(/\s+/g, " ").slice(0, 120)}`; } stopTaskMonitor(taskId) { diff --git a/src/renderer/App.jsx b/src/renderer/App.jsx index 8f7926b..c0e5857 100644 --- a/src/renderer/App.jsx +++ b/src/renderer/App.jsx @@ -1,4 +1,4 @@ -import React, { Suspense, useEffect, useMemo, useRef, useState } from "react"; +import React, { Suspense, useDeferredValue, useEffect, useMemo, useRef, useState } from "react"; const AccountsTab = React.lazy(() => import("./tabs/AccountsTab.jsx")); const LogsTab = React.lazy(() => import("./tabs/LogsTab.jsx")); @@ -66,8 +66,10 @@ const normalizeTask = (row) => ({ }); const normalizeIntervals = (form) => { - const min = Math.max(1, Number(form.minIntervalMinutes || 1)); - let max = Math.max(1, Number(form.maxIntervalMinutes || 1)); + const minValue = Number(form.minIntervalMinutes); + const maxValue = Number(form.maxIntervalMinutes); + const min = Number.isFinite(minValue) && minValue > 0 ? minValue : 1; + let max = Number.isFinite(maxValue) && maxValue > 0 ? maxValue : 1; if (max < min) max = min; return { ...form, minIntervalMinutes: min, maxIntervalMinutes: max }; }; @@ -147,11 +149,19 @@ export default function App() { const [invitePage, setInvitePage] = useState(1); const [inviteFilter, setInviteFilter] = useState("all"); const [taskSort, setTaskSort] = useState("activity"); - const [sidebarExpanded, setSidebarExpanded] = useState(false); + const [sidebarExpanded, setSidebarExpanded] = useState(true); const [expandedInviteId, setExpandedInviteId] = useState(null); const [now, setNow] = useState(Date.now()); + const [isVisible, setIsVisible] = useState(!document.hidden); const bellRef = useRef(null); const settingsAutosaveReady = useRef(false); + const tasksPollInFlight = useRef(false); + const accountsPollInFlight = useRef(false); + const logsPollInFlight = useRef(false); + const eventsPollInFlight = useRef(false); + const deferredTaskSearch = useDeferredValue(taskSearch); + const deferredLogSearch = useDeferredValue(logSearch); + const deferredInviteSearch = useDeferredValue(inviteSearch); const competitorGroups = useMemo(() => { return competitorText @@ -175,6 +185,13 @@ export default function App() { }); return map; }, [accounts]); + const accountStatsMap = useMemo(() => { + const map = new Map(); + (accountStats || []).forEach((item) => { + map.set(item.id, item); + }); + return map; + }, [accountStats]); const roleSummary = useMemo(() => { const monitor = []; const invite = []; @@ -353,7 +370,7 @@ export default function App() { }, [tasks, taskStatusMap]); const filteredTasks = useMemo(() => { - const query = taskSearch.trim().toLowerCase(); + const query = deferredTaskSearch.trim().toLowerCase(); const filtered = tasks.filter((task) => { const name = (task.name || "").toLowerCase(); const group = (task.our_group || "").toLowerCase(); @@ -393,51 +410,102 @@ export default function App() { return b.id - a.id; }); return sorted; - }, [tasks, taskSearch, taskFilter, taskSort, taskStatusMap]); + }, [tasks, deferredTaskSearch, taskFilter, taskSort, taskStatusMap]); useEffect(() => { if (!window.api) return undefined; - const interval = setInterval(async () => { - const tasksData = await window.api.listTasks(); - setTasks(tasksData); - await loadTaskStatuses(tasksData); - setAccounts(await window.api.listAccounts()); - setAccountAssignments(await window.api.listAccountAssignments()); - const statusData = await window.api.getStatus(); - setAccountStats(statusData.accountStats || []); - if (selectedTaskId != null) { - setTaskStatus(await window.api.taskStatus(selectedTaskId)); + const load = async () => { + if (!isVisible || tasksPollInFlight.current) return; + tasksPollInFlight.current = true; + try { + const tasksData = await window.api.listTasks(); + setTasks(tasksData); + await loadTaskStatuses(tasksData); + if (selectedTaskId != null) { + setTaskStatus(await window.api.taskStatus(selectedTaskId)); + } + } finally { + tasksPollInFlight.current = false; } + }; + load(); + const interval = setInterval(async () => { + await load(); }, 5000); return () => clearInterval(interval); - }, [selectedTaskId]); + }, [selectedTaskId, isVisible]); + + useEffect(() => { + if (!window.api) return undefined; + const load = async () => { + if (!isVisible || accountsPollInFlight.current) return; + accountsPollInFlight.current = true; + try { + setAccounts(await window.api.listAccounts()); + setAccountAssignments(await window.api.listAccountAssignments()); + const statusData = await window.api.getStatus(); + setAccountStats(statusData.accountStats || []); + } finally { + accountsPollInFlight.current = false; + } + }; + load(); + const interval = setInterval(load, 15000); + return () => clearInterval(interval); + }, [isVisible]); useEffect(() => { if (!window.api || activeTab !== "logs" || selectedTaskId == null) return undefined; const load = async () => { - setLogs(await window.api.listLogs({ limit: 100, taskId: selectedTaskId })); - setInvites(await window.api.listInvites({ limit: 200, taskId: selectedTaskId })); + if (!isVisible || logsPollInFlight.current) return; + logsPollInFlight.current = true; + try { + setLogs(await window.api.listLogs({ limit: 100, taskId: selectedTaskId })); + setInvites(await window.api.listInvites({ limit: 200, taskId: selectedTaskId })); + } finally { + logsPollInFlight.current = false; + } }; load(); const interval = setInterval(load, 5000); return () => clearInterval(interval); - }, [activeTab, selectedTaskId]); + }, [activeTab, selectedTaskId, isVisible]); useEffect(() => { if (!window.api || activeTab !== "events") return undefined; const load = async () => { - setAccountEvents(await window.api.listAccountEvents(200)); + if (!isVisible || eventsPollInFlight.current) return; + eventsPollInFlight.current = true; + try { + setAccountEvents(await window.api.listAccountEvents(200)); + } finally { + eventsPollInFlight.current = false; + } }; load(); const interval = setInterval(load, 10000); return () => clearInterval(interval); - }, [activeTab]); + }, [activeTab, isVisible]); useEffect(() => { const timer = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(timer); }, []); + useEffect(() => { + const handleVisibility = () => { + setIsVisible(!document.hidden); + }; + document.addEventListener("visibilitychange", handleVisibility); + window.addEventListener("focus", handleVisibility); + window.addEventListener("blur", handleVisibility); + return () => { + document.removeEventListener("visibilitychange", handleVisibility); + window.removeEventListener("focus", handleVisibility); + window.removeEventListener("blur", handleVisibility); + }; + }, []); + useEffect(() => { if (selectedTaskId == null) return; setTaskStatusMap((prev) => ({ @@ -469,7 +537,7 @@ export default function App() { return `${minutes}:${String(seconds).padStart(2, "0")}`; }; - const explainInviteError = (error) => { + const explainInviteError = useMemo(() => (error) => { if (!error) return ""; if (error === "USER_ID_INVALID") { return "Пользователь удален/скрыт; access_hash невалиден для этой сессии; приглашение в канал/чат без валидной сущности."; @@ -477,6 +545,36 @@ export default function App() { if (error === "CHAT_WRITE_FORBIDDEN") { return "Аккаунт не может приглашать: нет прав или он не участник группы."; } + if (error === "USER_NOT_MUTUAL_CONTACT") { + return "Пользователь не взаимный контакт для добавляющего аккаунта. Обычно это происходит, когда в группе/канале включена опция «добавлять могут только контакты» или у пользователя закрыт приём инвайтов. Решение: использовать аккаунт, который уже в контактах у пользователя, или поменять настройки группы."; + } + if (error === "USER_PRIVACY_RESTRICTED") { + return "Пользователь ограничил приватность и не принимает инвайты."; + } + if (error === "USER_NOT_PARTICIPANT") { + return "Аккаунт не состоит в целевой группе или канал приватный."; + } + if (error === "USER_BANNED_IN_CHANNEL") { + return "Пользователь заблокирован в группе или канале назначения."; + } + if (error === "USER_BOT") { + return "Бота нельзя приглашать как обычного пользователя."; + } + if (error === "USER_KICKED") { + return "Пользователь был удален из группы ранее."; + } + if (error === "CHAT_ADMIN_REQUIRED") { + return "Для добавления участников нужны права администратора."; + } + if (error === "USER_ALREADY_PARTICIPANT") { + return "Пользователь уже состоит в целевой группе."; + } + if (error === "INVITE_HASH_EXPIRED" || error === "INVITE_HASH_INVALID") { + return "Инвайт-ссылка недействительна или истекла."; + } + if (error === "CHANNEL_PRIVATE") { + return "Целевая группа/канал приватные и недоступны по ссылке."; + } if (error === "AUTH_KEY_DUPLICATED") { return "Сессия используется в другом месте, Telegram отозвал ключ."; } @@ -484,7 +582,18 @@ export default function App() { return "Ограничение Telegram по частоте действий."; } return ""; - }; + }, []); + + const explainTdataError = useMemo(() => (error) => { + if (!error) return ""; + if (error.includes("AUTH_KEY_DUPLICATED")) { + return "Эта сессия уже используется в другом месте. Выйдите из аккаунта на других устройствах и пересоберите tdata."; + } + if (error === "DUPLICATE_ACCOUNT") { + return "Аккаунт уже добавлен в приложение."; + } + return ""; + }, []); const showNotification = (text, tone) => { if (tone === "success") return; @@ -519,7 +628,7 @@ export default function App() { }, [notifications, notificationFilter]); const filteredLogs = useMemo(() => { - const query = logSearch.trim().toLowerCase(); + const query = deferredLogSearch.trim().toLowerCase(); if (!query) return logs; return logs.filter((log) => { const text = [ @@ -533,10 +642,10 @@ export default function App() { .toLowerCase(); return text.includes(query); }); - }, [logs, logSearch]); + }, [logs, deferredLogSearch]); const filteredInvites = useMemo(() => { - const query = inviteSearch.trim().toLowerCase(); + const query = deferredInviteSearch.trim().toLowerCase(); return invites.filter((invite) => { if (inviteFilter === "success" && invite.status !== "success") return false; if (inviteFilter === "error" && invite.status === "success") return false; @@ -558,7 +667,7 @@ export default function App() { if (!query) return true; return text.includes(query); }); - }, [invites, inviteSearch, inviteFilter]); + }, [invites, deferredInviteSearch, inviteFilter]); const inviteStrategyStats = useMemo(() => { let success = 0; @@ -1273,7 +1382,8 @@ export default function App() { }); if (result && result.canceled) return; if (!result.ok) { - showNotification(result.error || "Ошибка импорта tdata", "error"); + const hint = explainTdataError(result.error || ""); + showNotification(hint ? `${result.error}. ${hint}` : (result.error || "Ошибка импорта tdata"), "error"); return; } setTdataResult(result); @@ -1515,6 +1625,9 @@ export default function App() {
{item.path}
{item.error}
+ {explainTdataError(item.error) && ( +
{explainTdataError(item.error)}
+ )}
))} @@ -2038,8 +2151,11 @@ export default function App() { setTaskForm({ ...taskForm, minIntervalMinutes: Number(event.target.value) })} + value={taskForm.minIntervalMinutes || ""} + onChange={(event) => { + const value = event.target.value; + setTaskForm({ ...taskForm, minIntervalMinutes: value === "" ? "" : Number(value) }); + }} onBlur={() => setTaskForm(normalizeIntervals(taskForm))} /> @@ -2048,8 +2164,11 @@ export default function App() { setTaskForm({ ...taskForm, maxIntervalMinutes: Number(event.target.value) })} + value={taskForm.maxIntervalMinutes || ""} + onChange={(event) => { + const value = event.target.value; + setTaskForm({ ...taskForm, maxIntervalMinutes: value === "" ? "" : Number(value) }); + }} onBlur={() => setTaskForm(normalizeIntervals(taskForm))} /> @@ -2133,6 +2252,7 @@ export default function App() { onChange={(event) => setTaskForm({ ...taskForm, randomAccounts: event.target.checked })} /> Случайный выбор аккаунтов + Инвайты распределяются случайно между доступными аккаунтами.
@@ -2187,12 +2310,12 @@ export default function App() { {activeTab === "accounts" && ( Загрузка...
}> - { const assignedTasks = assignedAccountMap.get(account.id) || []; const membership = membershipStatus[account.id]; - const stats = accountStats.find((item) => item.id === account.id); + const stats = accountStatsMap.get(account.id); const remaining = stats && stats.remainingToday != null ? stats.remainingToday : null; const used = stats ? stats.usedToday : 0; const limit = stats ? stats.limit : settings.accountDailyLimit; @@ -184,7 +184,7 @@ export default function AccountsTab({ type="button" onClick={() => setAccountRolesAll(account.id, !selected)} > - {selected ? "Снять" : "Оба"} + {selected ? "Снять роли" : "Оба"} )} @@ -227,7 +227,7 @@ export default function AccountsTab({ }) .join(", "); const membership = membershipStatus[account.id]; - const stats = accountStats.find((item) => item.id === account.id); + const stats = accountStatsMap.get(account.id); const remaining = stats && stats.remainingToday != null ? stats.remainingToday : null; const used = stats ? stats.usedToday : 0; const limit = stats ? stats.limit : settings.accountDailyLimit; @@ -342,3 +342,5 @@ export default function AccountsTab({ ); } + +export default memo(AccountsTab); diff --git a/src/renderer/tabs/EventsTab.jsx b/src/renderer/tabs/EventsTab.jsx index da94d83..77df6d0 100644 --- a/src/renderer/tabs/EventsTab.jsx +++ b/src/renderer/tabs/EventsTab.jsx @@ -1,6 +1,6 @@ -import React, { useMemo, useState } from "react"; +import React, { memo, useMemo, useState } from "react"; -export default function EventsTab({ accountEvents, formatTimestamp }) { +function EventsTab({ accountEvents, formatTimestamp }) { const [typeFilter, setTypeFilter] = useState("all"); const [query, setQuery] = useState(""); @@ -24,7 +24,7 @@ export default function EventsTab({ accountEvents, formatTimestamp }) { return (

События аккаунтов

-
+
); } + +export default memo(EventsTab); diff --git a/src/renderer/tabs/LogsTab.jsx b/src/renderer/tabs/LogsTab.jsx index c3734f3..75829a8 100644 --- a/src/renderer/tabs/LogsTab.jsx +++ b/src/renderer/tabs/LogsTab.jsx @@ -1,6 +1,6 @@ -import React from "react"; +import React, { memo } from "react"; -export default function LogsTab({ +function LogsTab({ logsTab, setLogsTab, taskNotice, @@ -52,7 +52,8 @@ export default function LogsTab({ if (!Array.isArray(parsed)) return meta; return parsed .map((item) => `${strategyLabel(item.strategy)}: ${item.ok ? "ok" : "fail"}${item.detail ? ` (${item.detail})` : ""}`) - .join(" | "); + .map((line) => `- ${line}`) + .join("\n"); } catch (error) { return meta; } @@ -68,6 +69,13 @@ export default function LogsTab({ } }; + const getDurationMs = (start, finish) => { + const startMs = new Date(start).getTime(); + const finishMs = new Date(finish).getTime(); + if (!Number.isFinite(startMs) || !Number.isFinite(finishMs)) return null; + return Math.max(0, finishMs - startMs); + }; + return (
@@ -153,11 +161,24 @@ export default function LogsTab({ Пользователи: {successIds.length ? successIds.join(", ") : "—"}
{log.invitedCount === 0 && errors.length === 0 && ( -
Причина: очередь пуста
+
Причина: цикл завершён сразу — очередь пуста
)} {errors.length > 0 && ( -
Ошибки: {errors.join(" | ")}
+
+ Ошибки: {errors.map((err) => { + const code = String(err).split(":").pop().trim(); + const reason = explainInviteError(code) || "Причина не определена"; + return code ? `${err} (${reason})` : err; + }).join(" | ")} +
)} + {(() => { + const durationMs = getDurationMs(log.startedAt, log.finishedAt); + if (durationMs != null && durationMs < 1000) { + return
Цикл завершён сразу: очередь пуста или ошибка на первой попытке.
; + } + return null; + })()}
); @@ -270,14 +291,16 @@ export default function LogsTab({ {invite.error && invite.error !== "" && (
Причина: {invite.error}
)} - {invite.error && explainInviteError(invite.error) && ( -
Вероятная причина: {explainInviteError(invite.error)}
+ {invite.error && ( +
+ Вероятная причина: {explainInviteError(invite.error) || "Причина не определена"} +
)} {invite.strategy && (
Стратегия: {invite.strategy}
)} {invite.strategyMeta && ( -
Стратегии: {formatStrategies(invite.strategyMeta)}
+
{`Стратегии:\n${formatStrategies(invite.strategyMeta)}`}
)} {invite.strategyMeta && !hasStrategySuccess(invite.strategyMeta) && (
Все стратегии не сработали
@@ -299,9 +322,11 @@ export default function LogsTab({
Статус: {invite.status}
Пропуск: {invite.skippedReason || "—"}
Ошибка: {invite.error || "—"}
-
Вероятная причина: {explainInviteError(invite.error) || "—"}
+
Вероятная причина: {explainInviteError(invite.error) || "Причина не определена"}
Стратегия: {invite.strategy || "—"}
-
Стратегии: {invite.strategyMeta ? formatStrategies(invite.strategyMeta) : "—"}
+
+ {invite.strategyMeta ? `Стратегии:\n${formatStrategies(invite.strategyMeta)}` : "Стратегии: —"} +
{invite.strategyMeta && !hasStrategySuccess(invite.strategyMeta) && (
Результат: все стратегии не сработали
)} @@ -317,3 +342,5 @@ export default function LogsTab({
); } + +export default memo(LogsTab); diff --git a/src/renderer/tabs/SettingsTab.jsx b/src/renderer/tabs/SettingsTab.jsx index 665e8cb..8f4f280 100644 --- a/src/renderer/tabs/SettingsTab.jsx +++ b/src/renderer/tabs/SettingsTab.jsx @@ -1,6 +1,6 @@ -import React from "react"; +import React, { memo } from "react"; -export default function SettingsTab({ settings, onSettingsChange, settingsNotice, saveSettings }) { +function SettingsTab({ settings, onSettingsChange, settingsNotice, saveSettings }) { return (

Глобальные настройки аккаунтов

@@ -42,3 +42,5 @@ export default function SettingsTab({ settings, onSettingsChange, settingsNotice
); } + +export default memo(SettingsTab);