some
This commit is contained in:
parent
4bba4f3149
commit
a3a259bd3b
@ -194,8 +194,25 @@ class TaskRunner {
|
|||||||
result.strategy,
|
result.strategy,
|
||||||
result.strategyMeta
|
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) {
|
} catch (error) {
|
||||||
errors.push(error.message || String(error));
|
errors.push(error.message || String(error));
|
||||||
|
|||||||
@ -1298,14 +1298,16 @@ class TelegramManager {
|
|||||||
: { accessHash: "", strategy: "", detail: "no sender_id", attempts: [] };
|
: { accessHash: "", strategy: "", detail: "no sender_id", attempts: [] };
|
||||||
if (!resolved || !resolved.accessHash) {
|
if (!resolved || !resolved.accessHash) {
|
||||||
if (shouldLogEvent(`${chatId}:skip`, 30000)) {
|
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 reason = senderInfo && senderInfo.reason ? senderInfo.reason : "нет данных пользователя";
|
||||||
const extra = strategySummary ? `; стратегии: ${strategySummary}` : "";
|
const strategyBlock = strategyLines.length
|
||||||
|
? `\nШаги попыток:\n- ${strategyLines.join("\n- ")}`
|
||||||
|
: "";
|
||||||
this.store.addAccountEvent(
|
this.store.addAccountEvent(
|
||||||
monitorAccount.account.id,
|
monitorAccount.account.id,
|
||||||
monitorAccount.account.phone,
|
monitorAccount.account.phone,
|
||||||
"new_message_skipped",
|
"new_message_skipped",
|
||||||
`${formatGroupLabel(st)}: ${reason}${extra}, ${this._describeSender(message)}`
|
`${formatGroupLabel(st)}\nПричина: ${reason}${strategyBlock}\nОтправитель: ${this._describeSender(message)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -1352,13 +1354,15 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
if (!senderPayload.accessHash && !senderPayload.username) {
|
if (!senderPayload.accessHash && !senderPayload.username) {
|
||||||
if (shouldLogEvent(`${chatId}:skip`, 30000)) {
|
if (shouldLogEvent(`${chatId}:skip`, 30000)) {
|
||||||
const strategySummary = this._formatStrategyAttempts(resolved ? resolved.attempts : []);
|
const strategyLines = this._formatStrategyAttemptLines(resolved ? resolved.attempts : []);
|
||||||
const extra = strategySummary ? `; стратегии: ${strategySummary}` : "";
|
const strategyBlock = strategyLines.length
|
||||||
|
? `\nШаги попыток:\n- ${strategyLines.join("\n- ")}`
|
||||||
|
: "";
|
||||||
this.store.addAccountEvent(
|
this.store.addAccountEvent(
|
||||||
monitorAccount.account.id,
|
monitorAccount.account.id,
|
||||||
monitorAccount.account.phone,
|
monitorAccount.account.phone,
|
||||||
"new_message_skipped",
|
"new_message_skipped",
|
||||||
`${formatGroupLabel(st)}: нет access_hash (нет в списке участников)${extra}`
|
`${formatGroupLabel(st)}\nПричина: нет access_hash (нет в списке участников)${strategyBlock}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -1444,14 +1448,16 @@ class TelegramManager {
|
|||||||
if (!resolved || !resolved.accessHash) {
|
if (!resolved || !resolved.accessHash) {
|
||||||
skipped += 1;
|
skipped += 1;
|
||||||
if (shouldLogEvent(`${key}:skip`, 30000)) {
|
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 reason = senderInfo && senderInfo.reason ? senderInfo.reason : "нет данных пользователя";
|
||||||
const extra = strategySummary ? `; стратегии: ${strategySummary}` : "";
|
const strategyBlock = strategyLines.length
|
||||||
|
? `\nШаги попыток:\n- ${strategyLines.join("\n- ")}`
|
||||||
|
: "";
|
||||||
this.store.addAccountEvent(
|
this.store.addAccountEvent(
|
||||||
monitorAccount.account.id,
|
monitorAccount.account.id,
|
||||||
monitorAccount.account.phone,
|
monitorAccount.account.phone,
|
||||||
"new_message_skipped",
|
"new_message_skipped",
|
||||||
`${formatGroupLabel(st)}: ${reason}${extra}, ${this._describeSender(message)}`
|
`${formatGroupLabel(st)}\nПричина: ${reason}${strategyBlock}\nОтправитель: ${this._describeSender(message)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@ -1501,13 +1507,15 @@ class TelegramManager {
|
|||||||
if (!senderPayload.accessHash && !senderPayload.username) {
|
if (!senderPayload.accessHash && !senderPayload.username) {
|
||||||
skipped += 1;
|
skipped += 1;
|
||||||
if (shouldLogEvent(`${key}:skip`, 30000)) {
|
if (shouldLogEvent(`${key}:skip`, 30000)) {
|
||||||
const strategySummary = this._formatStrategyAttempts(resolved ? resolved.attempts : []);
|
const strategyLines = this._formatStrategyAttemptLines(resolved ? resolved.attempts : []);
|
||||||
const extra = strategySummary ? `; стратегии: ${strategySummary}` : "";
|
const strategyBlock = strategyLines.length
|
||||||
|
? `\nШаги попыток:\n- ${strategyLines.join("\n- ")}`
|
||||||
|
: "";
|
||||||
this.store.addAccountEvent(
|
this.store.addAccountEvent(
|
||||||
monitorAccount.account.id,
|
monitorAccount.account.id,
|
||||||
monitorAccount.account.phone,
|
monitorAccount.account.phone,
|
||||||
"new_message_skipped",
|
"new_message_skipped",
|
||||||
`${formatGroupLabel(st)}: нет access_hash (нет в списке участников)${extra}`
|
`${formatGroupLabel(st)}\nПричина: нет access_hash (нет в списке участников)${strategyBlock}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@ -1554,7 +1562,7 @@ class TelegramManager {
|
|||||||
monitorAccount.account.id,
|
monitorAccount.account.id,
|
||||||
monitorAccount.account.phone,
|
monitorAccount.account.phone,
|
||||||
"monitor_skip",
|
"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 : "нет данных пользователя";
|
const reason = senderInfo && senderInfo.reason ? senderInfo.reason : "нет данных пользователя";
|
||||||
skipReasons[reason] = (skipReasons[reason] || 0) + 1;
|
skipReasons[reason] = (skipReasons[reason] || 0) + 1;
|
||||||
if (!strategySkipSample) {
|
if (!strategySkipSample) {
|
||||||
const strategySummary = this._formatStrategyAttempts(resolved ? resolved.attempts : []);
|
const strategyLines = this._formatStrategyAttemptLines(resolved ? resolved.attempts : []);
|
||||||
if (strategySummary) {
|
if (strategyLines.length) {
|
||||||
strategySkipSample = `${group}: ${reason}; стратегии: ${strategySummary}`;
|
strategySkipSample = `${group}\nПричина: ${reason}\nШаги попыток:\n- ${strategyLines.join("\n- ")}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@ -1687,9 +1695,9 @@ class TelegramManager {
|
|||||||
senderPayload.accessHash = resolved.accessHash;
|
senderPayload.accessHash = resolved.accessHash;
|
||||||
}
|
}
|
||||||
if (!senderPayload.accessHash && !senderPayload.username && !strategySkipSample) {
|
if (!senderPayload.accessHash && !senderPayload.username && !strategySkipSample) {
|
||||||
const strategySummary = this._formatStrategyAttempts(resolved ? resolved.attempts : []);
|
const strategyLines = this._formatStrategyAttemptLines(resolved ? resolved.attempts : []);
|
||||||
if (strategySummary) {
|
if (strategyLines.length) {
|
||||||
strategySkipSample = `${group}: нет access_hash; стратегии: ${strategySummary}`;
|
strategySkipSample = `${group}\nПричина: нет access_hash (нет в списке участников)\nШаги попыток:\n- ${strategyLines.join("\n- ")}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1806,14 +1814,57 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_formatStrategyAttempts(attempts) {
|
_formatStrategyAttemptLines(attempts) {
|
||||||
if (!Array.isArray(attempts) || attempts.length === 0) return "";
|
if (!Array.isArray(attempts) || attempts.length === 0) return [];
|
||||||
const parts = attempts.map((item) => {
|
return attempts.map((item, index) => {
|
||||||
const status = item.ok ? "ok" : "fail";
|
const label = this._strategyLabelRu(item.strategy);
|
||||||
const detail = item.detail ? String(item.detail).replace(/\s+/g, " ").slice(0, 80) : "";
|
const status = item.ok ? "успех" : "не удалось";
|
||||||
return detail ? `${item.strategy}:${status} (${detail})` : `${item.strategy}:${status}`;
|
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) {
|
stopTaskMonitor(taskId) {
|
||||||
|
|||||||
@ -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 AccountsTab = React.lazy(() => import("./tabs/AccountsTab.jsx"));
|
||||||
const LogsTab = React.lazy(() => import("./tabs/LogsTab.jsx"));
|
const LogsTab = React.lazy(() => import("./tabs/LogsTab.jsx"));
|
||||||
@ -66,8 +66,10 @@ const normalizeTask = (row) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const normalizeIntervals = (form) => {
|
const normalizeIntervals = (form) => {
|
||||||
const min = Math.max(1, Number(form.minIntervalMinutes || 1));
|
const minValue = Number(form.minIntervalMinutes);
|
||||||
let max = Math.max(1, Number(form.maxIntervalMinutes || 1));
|
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;
|
if (max < min) max = min;
|
||||||
return { ...form, minIntervalMinutes: min, maxIntervalMinutes: max };
|
return { ...form, minIntervalMinutes: min, maxIntervalMinutes: max };
|
||||||
};
|
};
|
||||||
@ -147,11 +149,19 @@ export default function App() {
|
|||||||
const [invitePage, setInvitePage] = useState(1);
|
const [invitePage, setInvitePage] = useState(1);
|
||||||
const [inviteFilter, setInviteFilter] = useState("all");
|
const [inviteFilter, setInviteFilter] = useState("all");
|
||||||
const [taskSort, setTaskSort] = useState("activity");
|
const [taskSort, setTaskSort] = useState("activity");
|
||||||
const [sidebarExpanded, setSidebarExpanded] = useState(false);
|
const [sidebarExpanded, setSidebarExpanded] = useState(true);
|
||||||
const [expandedInviteId, setExpandedInviteId] = useState(null);
|
const [expandedInviteId, setExpandedInviteId] = useState(null);
|
||||||
const [now, setNow] = useState(Date.now());
|
const [now, setNow] = useState(Date.now());
|
||||||
|
const [isVisible, setIsVisible] = useState(!document.hidden);
|
||||||
const bellRef = useRef(null);
|
const bellRef = useRef(null);
|
||||||
const settingsAutosaveReady = useRef(false);
|
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(() => {
|
const competitorGroups = useMemo(() => {
|
||||||
return competitorText
|
return competitorText
|
||||||
@ -175,6 +185,13 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
return map;
|
return map;
|
||||||
}, [accounts]);
|
}, [accounts]);
|
||||||
|
const accountStatsMap = useMemo(() => {
|
||||||
|
const map = new Map();
|
||||||
|
(accountStats || []).forEach((item) => {
|
||||||
|
map.set(item.id, item);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [accountStats]);
|
||||||
const roleSummary = useMemo(() => {
|
const roleSummary = useMemo(() => {
|
||||||
const monitor = [];
|
const monitor = [];
|
||||||
const invite = [];
|
const invite = [];
|
||||||
@ -353,7 +370,7 @@ export default function App() {
|
|||||||
}, [tasks, taskStatusMap]);
|
}, [tasks, taskStatusMap]);
|
||||||
|
|
||||||
const filteredTasks = useMemo(() => {
|
const filteredTasks = useMemo(() => {
|
||||||
const query = taskSearch.trim().toLowerCase();
|
const query = deferredTaskSearch.trim().toLowerCase();
|
||||||
const filtered = tasks.filter((task) => {
|
const filtered = tasks.filter((task) => {
|
||||||
const name = (task.name || "").toLowerCase();
|
const name = (task.name || "").toLowerCase();
|
||||||
const group = (task.our_group || "").toLowerCase();
|
const group = (task.our_group || "").toLowerCase();
|
||||||
@ -393,51 +410,102 @@ export default function App() {
|
|||||||
return b.id - a.id;
|
return b.id - a.id;
|
||||||
});
|
});
|
||||||
return sorted;
|
return sorted;
|
||||||
}, [tasks, taskSearch, taskFilter, taskSort, taskStatusMap]);
|
}, [tasks, deferredTaskSearch, taskFilter, taskSort, taskStatusMap]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!window.api) return undefined;
|
if (!window.api) return undefined;
|
||||||
const interval = setInterval(async () => {
|
const load = async () => {
|
||||||
|
if (!isVisible || tasksPollInFlight.current) return;
|
||||||
|
tasksPollInFlight.current = true;
|
||||||
|
try {
|
||||||
const tasksData = await window.api.listTasks();
|
const tasksData = await window.api.listTasks();
|
||||||
setTasks(tasksData);
|
setTasks(tasksData);
|
||||||
await loadTaskStatuses(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, isVisible]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!window.api) return undefined;
|
||||||
|
const load = async () => {
|
||||||
|
if (!isVisible || accountsPollInFlight.current) return;
|
||||||
|
accountsPollInFlight.current = true;
|
||||||
|
try {
|
||||||
setAccounts(await window.api.listAccounts());
|
setAccounts(await window.api.listAccounts());
|
||||||
setAccountAssignments(await window.api.listAccountAssignments());
|
setAccountAssignments(await window.api.listAccountAssignments());
|
||||||
const statusData = await window.api.getStatus();
|
const statusData = await window.api.getStatus();
|
||||||
setAccountStats(statusData.accountStats || []);
|
setAccountStats(statusData.accountStats || []);
|
||||||
if (selectedTaskId != null) {
|
} finally {
|
||||||
setTaskStatus(await window.api.taskStatus(selectedTaskId));
|
accountsPollInFlight.current = false;
|
||||||
}
|
}
|
||||||
}, 5000);
|
};
|
||||||
|
load();
|
||||||
|
const interval = setInterval(load, 15000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [selectedTaskId]);
|
}, [isVisible]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!window.api || activeTab !== "logs" || selectedTaskId == null) return undefined;
|
if (!window.api || activeTab !== "logs" || selectedTaskId == null) return undefined;
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
|
if (!isVisible || logsPollInFlight.current) return;
|
||||||
|
logsPollInFlight.current = true;
|
||||||
|
try {
|
||||||
setLogs(await window.api.listLogs({ limit: 100, taskId: selectedTaskId }));
|
setLogs(await window.api.listLogs({ limit: 100, taskId: selectedTaskId }));
|
||||||
setInvites(await window.api.listInvites({ limit: 200, taskId: selectedTaskId }));
|
setInvites(await window.api.listInvites({ limit: 200, taskId: selectedTaskId }));
|
||||||
|
} finally {
|
||||||
|
logsPollInFlight.current = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
load();
|
load();
|
||||||
const interval = setInterval(load, 5000);
|
const interval = setInterval(load, 5000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [activeTab, selectedTaskId]);
|
}, [activeTab, selectedTaskId, isVisible]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!window.api || activeTab !== "events") return undefined;
|
if (!window.api || activeTab !== "events") return undefined;
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
|
if (!isVisible || eventsPollInFlight.current) return;
|
||||||
|
eventsPollInFlight.current = true;
|
||||||
|
try {
|
||||||
setAccountEvents(await window.api.listAccountEvents(200));
|
setAccountEvents(await window.api.listAccountEvents(200));
|
||||||
|
} finally {
|
||||||
|
eventsPollInFlight.current = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
load();
|
load();
|
||||||
const interval = setInterval(load, 10000);
|
const interval = setInterval(load, 10000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [activeTab]);
|
}, [activeTab, isVisible]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setInterval(() => setNow(Date.now()), 1000);
|
const timer = setInterval(() => setNow(Date.now()), 1000);
|
||||||
return () => clearInterval(timer);
|
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(() => {
|
useEffect(() => {
|
||||||
if (selectedTaskId == null) return;
|
if (selectedTaskId == null) return;
|
||||||
setTaskStatusMap((prev) => ({
|
setTaskStatusMap((prev) => ({
|
||||||
@ -469,7 +537,7 @@ export default function App() {
|
|||||||
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const explainInviteError = (error) => {
|
const explainInviteError = useMemo(() => (error) => {
|
||||||
if (!error) return "";
|
if (!error) return "";
|
||||||
if (error === "USER_ID_INVALID") {
|
if (error === "USER_ID_INVALID") {
|
||||||
return "Пользователь удален/скрыт; access_hash невалиден для этой сессии; приглашение в канал/чат без валидной сущности.";
|
return "Пользователь удален/скрыт; access_hash невалиден для этой сессии; приглашение в канал/чат без валидной сущности.";
|
||||||
@ -477,6 +545,36 @@ export default function App() {
|
|||||||
if (error === "CHAT_WRITE_FORBIDDEN") {
|
if (error === "CHAT_WRITE_FORBIDDEN") {
|
||||||
return "Аккаунт не может приглашать: нет прав или он не участник группы.";
|
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") {
|
if (error === "AUTH_KEY_DUPLICATED") {
|
||||||
return "Сессия используется в другом месте, Telegram отозвал ключ.";
|
return "Сессия используется в другом месте, Telegram отозвал ключ.";
|
||||||
}
|
}
|
||||||
@ -484,7 +582,18 @@ export default function App() {
|
|||||||
return "Ограничение Telegram по частоте действий.";
|
return "Ограничение Telegram по частоте действий.";
|
||||||
}
|
}
|
||||||
return "";
|
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) => {
|
const showNotification = (text, tone) => {
|
||||||
if (tone === "success") return;
|
if (tone === "success") return;
|
||||||
@ -519,7 +628,7 @@ export default function App() {
|
|||||||
}, [notifications, notificationFilter]);
|
}, [notifications, notificationFilter]);
|
||||||
|
|
||||||
const filteredLogs = useMemo(() => {
|
const filteredLogs = useMemo(() => {
|
||||||
const query = logSearch.trim().toLowerCase();
|
const query = deferredLogSearch.trim().toLowerCase();
|
||||||
if (!query) return logs;
|
if (!query) return logs;
|
||||||
return logs.filter((log) => {
|
return logs.filter((log) => {
|
||||||
const text = [
|
const text = [
|
||||||
@ -533,10 +642,10 @@ export default function App() {
|
|||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
return text.includes(query);
|
return text.includes(query);
|
||||||
});
|
});
|
||||||
}, [logs, logSearch]);
|
}, [logs, deferredLogSearch]);
|
||||||
|
|
||||||
const filteredInvites = useMemo(() => {
|
const filteredInvites = useMemo(() => {
|
||||||
const query = inviteSearch.trim().toLowerCase();
|
const query = deferredInviteSearch.trim().toLowerCase();
|
||||||
return invites.filter((invite) => {
|
return invites.filter((invite) => {
|
||||||
if (inviteFilter === "success" && invite.status !== "success") return false;
|
if (inviteFilter === "success" && invite.status !== "success") return false;
|
||||||
if (inviteFilter === "error" && 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;
|
if (!query) return true;
|
||||||
return text.includes(query);
|
return text.includes(query);
|
||||||
});
|
});
|
||||||
}, [invites, inviteSearch, inviteFilter]);
|
}, [invites, deferredInviteSearch, inviteFilter]);
|
||||||
|
|
||||||
const inviteStrategyStats = useMemo(() => {
|
const inviteStrategyStats = useMemo(() => {
|
||||||
let success = 0;
|
let success = 0;
|
||||||
@ -1273,7 +1382,8 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
if (result && result.canceled) return;
|
if (result && result.canceled) return;
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
showNotification(result.error || "Ошибка импорта tdata", "error");
|
const hint = explainTdataError(result.error || "");
|
||||||
|
showNotification(hint ? `${result.error}. ${hint}` : (result.error || "Ошибка импорта tdata"), "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setTdataResult(result);
|
setTdataResult(result);
|
||||||
@ -1515,6 +1625,9 @@ export default function App() {
|
|||||||
<div key={`${item.path}-${index}`} className="tdata-error-row">
|
<div key={`${item.path}-${index}`} className="tdata-error-row">
|
||||||
<div className="tdata-error-path">{item.path}</div>
|
<div className="tdata-error-path">{item.path}</div>
|
||||||
<div className="tdata-error-text">{item.error}</div>
|
<div className="tdata-error-text">{item.error}</div>
|
||||||
|
{explainTdataError(item.error) && (
|
||||||
|
<div className="tdata-error-hint">{explainTdataError(item.error)}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -2038,8 +2151,11 @@ export default function App() {
|
|||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
value={taskForm.minIntervalMinutes}
|
value={taskForm.minIntervalMinutes || ""}
|
||||||
onChange={(event) => setTaskForm({ ...taskForm, minIntervalMinutes: Number(event.target.value) })}
|
onChange={(event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
setTaskForm({ ...taskForm, minIntervalMinutes: value === "" ? "" : Number(value) });
|
||||||
|
}}
|
||||||
onBlur={() => setTaskForm(normalizeIntervals(taskForm))}
|
onBlur={() => setTaskForm(normalizeIntervals(taskForm))}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@ -2048,8 +2164,11 @@ export default function App() {
|
|||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
value={taskForm.maxIntervalMinutes}
|
value={taskForm.maxIntervalMinutes || ""}
|
||||||
onChange={(event) => setTaskForm({ ...taskForm, maxIntervalMinutes: Number(event.target.value) })}
|
onChange={(event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
setTaskForm({ ...taskForm, maxIntervalMinutes: value === "" ? "" : Number(value) });
|
||||||
|
}}
|
||||||
onBlur={() => setTaskForm(normalizeIntervals(taskForm))}
|
onBlur={() => setTaskForm(normalizeIntervals(taskForm))}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@ -2133,6 +2252,7 @@ export default function App() {
|
|||||||
onChange={(event) => setTaskForm({ ...taskForm, randomAccounts: event.target.checked })}
|
onChange={(event) => setTaskForm({ ...taskForm, randomAccounts: event.target.checked })}
|
||||||
/>
|
/>
|
||||||
Случайный выбор аккаунтов
|
Случайный выбор аккаунтов
|
||||||
|
<span className="hint">Инвайты распределяются случайно между доступными аккаунтами.</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="checkbox">
|
<label className="checkbox">
|
||||||
<input
|
<input
|
||||||
@ -2141,6 +2261,7 @@ export default function App() {
|
|||||||
onChange={(event) => setTaskForm({ ...taskForm, multiAccountsPerRun: event.target.checked })}
|
onChange={(event) => setTaskForm({ ...taskForm, multiAccountsPerRun: event.target.checked })}
|
||||||
/>
|
/>
|
||||||
Несколько аккаунтов за цикл
|
Несколько аккаунтов за цикл
|
||||||
|
<span className="hint">Если выключено — в каждом цикле используется один аккаунт.</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="checkbox">
|
<label className="checkbox">
|
||||||
<input
|
<input
|
||||||
@ -2149,6 +2270,7 @@ export default function App() {
|
|||||||
onChange={(event) => setTaskForm({ ...taskForm, retryOnFail: event.target.checked })}
|
onChange={(event) => setTaskForm({ ...taskForm, retryOnFail: event.target.checked })}
|
||||||
/>
|
/>
|
||||||
Повторять при ошибке
|
Повторять при ошибке
|
||||||
|
<span className="hint">Повторяем до 2 раз при неудачном инвайте.</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="checkbox">
|
<label className="checkbox">
|
||||||
<input
|
<input
|
||||||
@ -2157,6 +2279,7 @@ export default function App() {
|
|||||||
onChange={(event) => setTaskForm({ ...taskForm, stopOnBlocked: event.target.checked })}
|
onChange={(event) => setTaskForm({ ...taskForm, stopOnBlocked: event.target.checked })}
|
||||||
/>
|
/>
|
||||||
Останавливать при блокировках
|
Останавливать при блокировках
|
||||||
|
<span className="hint">Останавливает задачу, если % ограниченных аккаунтов выше порога.</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
@ -2189,7 +2312,7 @@ export default function App() {
|
|||||||
<Suspense fallback={<div className="card">Загрузка...</div>}>
|
<Suspense fallback={<div className="card">Загрузка...</div>}>
|
||||||
<AccountsTab
|
<AccountsTab
|
||||||
accounts={accounts}
|
accounts={accounts}
|
||||||
accountStats={accountStats}
|
accountStatsMap={accountStatsMap}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
membershipStatus={membershipStatus}
|
membershipStatus={membershipStatus}
|
||||||
assignedAccountMap={assignedAccountMap}
|
assignedAccountMap={assignedAccountMap}
|
||||||
|
|||||||
@ -544,6 +544,11 @@ textarea {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.row-inline.column {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
.select-inline {
|
.select-inline {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -1027,6 +1032,11 @@ label .hint {
|
|||||||
color: #b91c1c;
|
color: #b91c1c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tdata-error-hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #7c2d12;
|
||||||
|
}
|
||||||
|
|
||||||
.status-text {
|
.status-text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #1d4ed8;
|
color: #1d4ed8;
|
||||||
@ -1109,6 +1119,7 @@ label .hint {
|
|||||||
.log-errors {
|
.log-errors {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #475569;
|
color: #475569;
|
||||||
|
white-space: pre-line;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invite-details {
|
.invite-details {
|
||||||
@ -1120,12 +1131,17 @@ label .hint {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
white-space: pre-line;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pre-line {
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
button:disabled {
|
button:disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import React, { useState } from "react";
|
import React, { memo, useState } from "react";
|
||||||
|
|
||||||
export default function AccountsTab({
|
function AccountsTab({
|
||||||
accounts,
|
accounts,
|
||||||
accountStats,
|
accountStatsMap,
|
||||||
settings,
|
settings,
|
||||||
membershipStatus,
|
membershipStatus,
|
||||||
assignedAccountMap,
|
assignedAccountMap,
|
||||||
@ -76,7 +76,7 @@ export default function AccountsTab({
|
|||||||
{accountBuckets.freeOrSelected.map((account) => {
|
{accountBuckets.freeOrSelected.map((account) => {
|
||||||
const assignedTasks = assignedAccountMap.get(account.id) || [];
|
const assignedTasks = assignedAccountMap.get(account.id) || [];
|
||||||
const membership = membershipStatus[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 remaining = stats && stats.remainingToday != null ? stats.remainingToday : null;
|
||||||
const used = stats ? stats.usedToday : 0;
|
const used = stats ? stats.usedToday : 0;
|
||||||
const limit = stats ? stats.limit : settings.accountDailyLimit;
|
const limit = stats ? stats.limit : settings.accountDailyLimit;
|
||||||
@ -184,7 +184,7 @@ export default function AccountsTab({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setAccountRolesAll(account.id, !selected)}
|
onClick={() => setAccountRolesAll(account.id, !selected)}
|
||||||
>
|
>
|
||||||
{selected ? "Снять" : "Оба"}
|
{selected ? "Снять роли" : "Оба"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -227,7 +227,7 @@ export default function AccountsTab({
|
|||||||
})
|
})
|
||||||
.join(", ");
|
.join(", ");
|
||||||
const membership = membershipStatus[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 remaining = stats && stats.remainingToday != null ? stats.remainingToday : null;
|
||||||
const used = stats ? stats.usedToday : 0;
|
const used = stats ? stats.usedToday : 0;
|
||||||
const limit = stats ? stats.limit : settings.accountDailyLimit;
|
const limit = stats ? stats.limit : settings.accountDailyLimit;
|
||||||
@ -342,3 +342,5 @@ export default function AccountsTab({
|
|||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(AccountsTab);
|
||||||
|
|||||||
@ -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 [typeFilter, setTypeFilter] = useState("all");
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ export default function EventsTab({ accountEvents, formatTimestamp }) {
|
|||||||
return (
|
return (
|
||||||
<section className="card logs">
|
<section className="card logs">
|
||||||
<h2>События аккаунтов</h2>
|
<h2>События аккаунтов</h2>
|
||||||
<div className="row-inline">
|
<div className="row-inline column">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={query}
|
value={query}
|
||||||
@ -60,3 +60,5 @@ export default function EventsTab({ accountEvents, formatTimestamp }) {
|
|||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(EventsTab);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React, { memo } from "react";
|
||||||
|
|
||||||
export default function LogsTab({
|
function LogsTab({
|
||||||
logsTab,
|
logsTab,
|
||||||
setLogsTab,
|
setLogsTab,
|
||||||
taskNotice,
|
taskNotice,
|
||||||
@ -52,7 +52,8 @@ export default function LogsTab({
|
|||||||
if (!Array.isArray(parsed)) return meta;
|
if (!Array.isArray(parsed)) return meta;
|
||||||
return parsed
|
return parsed
|
||||||
.map((item) => `${strategyLabel(item.strategy)}: ${item.ok ? "ok" : "fail"}${item.detail ? ` (${item.detail})` : ""}`)
|
.map((item) => `${strategyLabel(item.strategy)}: ${item.ok ? "ok" : "fail"}${item.detail ? ` (${item.detail})` : ""}`)
|
||||||
.join(" | ");
|
.map((line) => `- ${line}`)
|
||||||
|
.join("\n");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return meta;
|
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 (
|
return (
|
||||||
<section className="card logs">
|
<section className="card logs">
|
||||||
<div className="row-header">
|
<div className="row-header">
|
||||||
@ -153,11 +161,24 @@ export default function LogsTab({
|
|||||||
Пользователи: {successIds.length ? successIds.join(", ") : "—"}
|
Пользователи: {successIds.length ? successIds.join(", ") : "—"}
|
||||||
</div>
|
</div>
|
||||||
{log.invitedCount === 0 && errors.length === 0 && (
|
{log.invitedCount === 0 && errors.length === 0 && (
|
||||||
<div className="log-errors">Причина: очередь пуста</div>
|
<div className="log-errors">Причина: цикл завершён сразу — очередь пуста</div>
|
||||||
)}
|
)}
|
||||||
{errors.length > 0 && (
|
{errors.length > 0 && (
|
||||||
<div className="log-errors">Ошибки: {errors.join(" | ")}</div>
|
<div className="log-errors">
|
||||||
|
Ошибки: {errors.map((err) => {
|
||||||
|
const code = String(err).split(":").pop().trim();
|
||||||
|
const reason = explainInviteError(code) || "Причина не определена";
|
||||||
|
return code ? `${err} (${reason})` : err;
|
||||||
|
}).join(" | ")}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{(() => {
|
||||||
|
const durationMs = getDurationMs(log.startedAt, log.finishedAt);
|
||||||
|
if (durationMs != null && durationMs < 1000) {
|
||||||
|
return <div className="log-errors">Цикл завершён сразу: очередь пуста или ошибка на первой попытке.</div>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -270,14 +291,16 @@ export default function LogsTab({
|
|||||||
{invite.error && invite.error !== "" && (
|
{invite.error && invite.error !== "" && (
|
||||||
<div className="log-errors">Причина: {invite.error}</div>
|
<div className="log-errors">Причина: {invite.error}</div>
|
||||||
)}
|
)}
|
||||||
{invite.error && explainInviteError(invite.error) && (
|
{invite.error && (
|
||||||
<div className="log-users">Вероятная причина: {explainInviteError(invite.error)}</div>
|
<div className="log-users">
|
||||||
|
Вероятная причина: {explainInviteError(invite.error) || "Причина не определена"}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{invite.strategy && (
|
{invite.strategy && (
|
||||||
<div className="log-users">Стратегия: {invite.strategy}</div>
|
<div className="log-users">Стратегия: {invite.strategy}</div>
|
||||||
)}
|
)}
|
||||||
{invite.strategyMeta && (
|
{invite.strategyMeta && (
|
||||||
<div className="log-users">Стратегии: {formatStrategies(invite.strategyMeta)}</div>
|
<div className="log-users">{`Стратегии:\n${formatStrategies(invite.strategyMeta)}`}</div>
|
||||||
)}
|
)}
|
||||||
{invite.strategyMeta && !hasStrategySuccess(invite.strategyMeta) && (
|
{invite.strategyMeta && !hasStrategySuccess(invite.strategyMeta) && (
|
||||||
<div className="log-errors">Все стратегии не сработали</div>
|
<div className="log-errors">Все стратегии не сработали</div>
|
||||||
@ -299,9 +322,11 @@ export default function LogsTab({
|
|||||||
<div>Статус: {invite.status}</div>
|
<div>Статус: {invite.status}</div>
|
||||||
<div>Пропуск: {invite.skippedReason || "—"}</div>
|
<div>Пропуск: {invite.skippedReason || "—"}</div>
|
||||||
<div>Ошибка: {invite.error || "—"}</div>
|
<div>Ошибка: {invite.error || "—"}</div>
|
||||||
<div>Вероятная причина: {explainInviteError(invite.error) || "—"}</div>
|
<div>Вероятная причина: {explainInviteError(invite.error) || "Причина не определена"}</div>
|
||||||
<div>Стратегия: {invite.strategy || "—"}</div>
|
<div>Стратегия: {invite.strategy || "—"}</div>
|
||||||
<div>Стратегии: {invite.strategyMeta ? formatStrategies(invite.strategyMeta) : "—"}</div>
|
<div className="pre-line">
|
||||||
|
{invite.strategyMeta ? `Стратегии:\n${formatStrategies(invite.strategyMeta)}` : "Стратегии: —"}
|
||||||
|
</div>
|
||||||
{invite.strategyMeta && !hasStrategySuccess(invite.strategyMeta) && (
|
{invite.strategyMeta && !hasStrategySuccess(invite.strategyMeta) && (
|
||||||
<div>Результат: все стратегии не сработали</div>
|
<div>Результат: все стратегии не сработали</div>
|
||||||
)}
|
)}
|
||||||
@ -317,3 +342,5 @@ export default function LogsTab({
|
|||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(LogsTab);
|
||||||
|
|||||||
@ -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 (
|
return (
|
||||||
<section className="card">
|
<section className="card">
|
||||||
<h2>Глобальные настройки аккаунтов</h2>
|
<h2>Глобальные настройки аккаунтов</h2>
|
||||||
@ -42,3 +42,5 @@ export default function SettingsTab({ settings, onSettingsChange, settingsNotice
|
|||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(SettingsTab);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user