some
This commit is contained in:
parent
51af20dd6f
commit
e7df1dc63a
@ -1338,6 +1338,8 @@ ipcMain.handle("tasks:status", (_event, id) => {
|
|||||||
const readiness = { ok: true, reasons: [] };
|
const readiness = { ok: true, reasons: [] };
|
||||||
let restrictedAccounts = [];
|
let restrictedAccounts = [];
|
||||||
let totalInvites = 0;
|
let totalInvites = 0;
|
||||||
|
let totalInvitesSuccess = 0;
|
||||||
|
let totalInvitesAttempts = 0;
|
||||||
let taskInviteLimitTotal = 0;
|
let taskInviteLimitTotal = 0;
|
||||||
let accountDailyLimitTotal = 0;
|
let accountDailyLimitTotal = 0;
|
||||||
let inviteAccountsCount = 0;
|
let inviteAccountsCount = 0;
|
||||||
@ -1385,11 +1387,13 @@ ipcMain.handle("tasks:status", (_event, id) => {
|
|||||||
const account = accountsById.get(row.account_id);
|
const account = accountsById.get(row.account_id);
|
||||||
return sum + Math.max(0, Number(account && account.daily_limit ? account.daily_limit : 0));
|
return sum + Math.max(0, Number(account && account.daily_limit ? account.daily_limit : 0));
|
||||||
}, 0);
|
}, 0);
|
||||||
totalInvites =
|
totalInvitesSuccess = Number(store.countInvitesByStatus(id, "success") || 0);
|
||||||
Number(store.countInvitesByStatus(id, "success") || 0)
|
totalInvitesAttempts =
|
||||||
|
totalInvitesSuccess
|
||||||
+ Number(store.countInvitesByStatus(id, "failed") || 0)
|
+ Number(store.countInvitesByStatus(id, "failed") || 0)
|
||||||
+ Number(store.countInvitesByStatus(id, "skipped") || 0)
|
+ Number(store.countInvitesByStatus(id, "skipped") || 0)
|
||||||
+ Number(store.countInvitesByStatus(id, "unconfirmed") || 0);
|
+ Number(store.countInvitesByStatus(id, "unconfirmed") || 0);
|
||||||
|
totalInvites = totalInvitesAttempts;
|
||||||
const monitorRows = accountRows.filter((row) => row.role_monitor);
|
const monitorRows = accountRows.filter((row) => row.role_monitor);
|
||||||
if (!inviteRows.length) {
|
if (!inviteRows.length) {
|
||||||
const fallbackAvailable = accountRows
|
const fallbackAvailable = accountRows
|
||||||
@ -1510,6 +1514,8 @@ ipcMain.handle("tasks:status", (_event, id) => {
|
|||||||
queueCount,
|
queueCount,
|
||||||
dailyUsed,
|
dailyUsed,
|
||||||
totalInvites,
|
totalInvites,
|
||||||
|
totalInvitesSuccess,
|
||||||
|
totalInvitesAttempts,
|
||||||
unconfirmedCount,
|
unconfirmedCount,
|
||||||
dailyLimit: effectiveLimit,
|
dailyLimit: effectiveLimit,
|
||||||
taskDailyLimitBase: task ? Number(task.daily_limit || 0) : 0,
|
taskDailyLimitBase: task ? Number(task.daily_limit || 0) : 0,
|
||||||
|
|||||||
@ -87,9 +87,11 @@ export default function TasksSidebar({
|
|||||||
const dailyLabel = status
|
const dailyLabel = status
|
||||||
? `Лимит сегодня: ${status.dailyUsed}/${status.dailyLimit}${warmupDelta > 0 ? ` (старт ${warmupStart} +${warmupDelta} прогрев)` : ""}`
|
? `Лимит сегодня: ${status.dailyUsed}/${status.dailyLimit}${warmupDelta > 0 ? ` (старт ${warmupStart} +${warmupDelta} прогрев)` : ""}`
|
||||||
: "Лимит сегодня: —";
|
: "Лимит сегодня: —";
|
||||||
const totalLabel = status ? `Инвайтов всего: ${status.totalInvites || 0}` : "Инвайтов всего: —";
|
const totalSuccessLabel = status ? `Инвайтов всего: ${status.totalInvitesSuccess || 0}` : "Инвайтов всего: —";
|
||||||
|
const totalAttemptsLabel = status ? `Попыток всего: ${status.totalInvitesAttempts || 0}` : "Попыток всего: —";
|
||||||
const taskLimitLabel = status ? `Лимит задачи: ${status.dailyLimit || 0}` : "Лимит задачи: —";
|
const taskLimitLabel = status ? `Лимит задачи: ${status.dailyLimit || 0}` : "Лимит задачи: —";
|
||||||
const accountLimitLabel = status ? `Лимит аккаунтов: ${status.accountDailyLimitTotal || 0}` : "Лимит аккаунтов: —";
|
const accountCycleLimitLabel = status ? `Лимит аккаунтов (цикл): ${status.taskInviteLimitTotal || 0}` : "Лимит аккаунтов (цикл): —";
|
||||||
|
const accountDailyCapLabel = status ? `Потолок аккаунтов (день): ${status.accountDailyLimitTotal || 0}` : "Потолок аккаунтов (день): —";
|
||||||
const cycleLabel = status && status.running ? `Цикл: ${formatCountdown(status.nextRunAt)}` : "Цикл: —";
|
const cycleLabel = status && status.running ? `Цикл: ${formatCountdown(status.nextRunAt)}` : "Цикл: —";
|
||||||
const lastMessageRaw = status && status.monitorInfo && status.monitorInfo.lastMessageAt
|
const lastMessageRaw = status && status.monitorInfo && status.monitorInfo.lastMessageAt
|
||||||
? status.monitorInfo.lastMessageAt
|
? status.monitorInfo.lastMessageAt
|
||||||
@ -115,9 +117,11 @@ export default function TasksSidebar({
|
|||||||
`Статус: ${statusLabel}`,
|
`Статус: ${statusLabel}`,
|
||||||
`Очередь: ${status ? status.queueCount : "—"}`,
|
`Очередь: ${status ? status.queueCount : "—"}`,
|
||||||
`Лимит сегодня: ${status ? `${status.dailyUsed}/${status.dailyLimit}${warmupDelta > 0 ? ` (старт ${warmupStart} +${warmupDelta} прогрев)` : ""}` : "—"}`,
|
`Лимит сегодня: ${status ? `${status.dailyUsed}/${status.dailyLimit}${warmupDelta > 0 ? ` (старт ${warmupStart} +${warmupDelta} прогрев)` : ""}` : "—"}`,
|
||||||
`Инвайтов всего: ${status ? status.totalInvites || 0 : "—"}`,
|
`Инвайтов всего (успешных): ${status ? status.totalInvitesSuccess || 0 : "—"}`,
|
||||||
|
`Попыток всего: ${status ? status.totalInvitesAttempts || 0 : "—"}`,
|
||||||
`Лимит задачи: ${status ? status.dailyLimit || 0 : "—"}`,
|
`Лимит задачи: ${status ? status.dailyLimit || 0 : "—"}`,
|
||||||
`Лимит аккаунтов: ${status ? status.accountDailyLimitTotal || 0 : "—"}`,
|
`Лимит аккаунтов (цикл): ${status ? status.taskInviteLimitTotal || 0 : "—"}`,
|
||||||
|
`Потолок аккаунтов (день): ${status ? status.accountDailyLimitTotal || 0 : "—"}`,
|
||||||
`Мониторинг: ${monitoring ? "активен" : "нет"}`,
|
`Мониторинг: ${monitoring ? "активен" : "нет"}`,
|
||||||
`Мониторит: ${monitorLabel}`,
|
`Мониторит: ${monitorLabel}`,
|
||||||
`Последнее: ${lastMessage}`,
|
`Последнее: ${lastMessage}`,
|
||||||
@ -146,11 +150,15 @@ export default function TasksSidebar({
|
|||||||
<div className="task-meta-row">
|
<div className="task-meta-row">
|
||||||
<span className="task-meta">{queueLabel}</span>
|
<span className="task-meta">{queueLabel}</span>
|
||||||
<span className="task-meta">{dailyLabel}</span>
|
<span className="task-meta">{dailyLabel}</span>
|
||||||
<span className="task-meta">{totalLabel}</span>
|
<span className="task-meta">{totalSuccessLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="task-meta-row">
|
<div className="task-meta-row">
|
||||||
<span className="task-meta">{taskLimitLabel}</span>
|
<span className="task-meta">{taskLimitLabel}</span>
|
||||||
<span className="task-meta">{accountLimitLabel}</span>
|
<span className="task-meta">{accountCycleLimitLabel}</span>
|
||||||
|
<span className="task-meta">{accountDailyCapLabel}</span>
|
||||||
|
</div>
|
||||||
|
<div className="task-meta-row">
|
||||||
|
<span className="task-meta">{totalAttemptsLabel}</span>
|
||||||
<span className="task-meta">{cycleLabel}</span>
|
<span className="task-meta">{cycleLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -340,6 +340,12 @@ export default function useTabProps(
|
|||||||
|
|
||||||
const eventsTabProps = {
|
const eventsTabProps = {
|
||||||
accountEvents,
|
accountEvents,
|
||||||
|
selectedTaskId,
|
||||||
|
selectedTask,
|
||||||
|
competitorLinks: String(competitorText || "")
|
||||||
|
.split("\n")
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean),
|
||||||
formatTimestamp,
|
formatTimestamp,
|
||||||
onClearEvents,
|
onClearEvents,
|
||||||
accountById,
|
accountById,
|
||||||
|
|||||||
@ -72,6 +72,17 @@ export default function useTaskStatusView({
|
|||||||
const inviteAccessChecked = inviteAccessStatus && inviteAccessStatus.length > 0;
|
const inviteAccessChecked = inviteAccessStatus && inviteAccessStatus.length > 0;
|
||||||
const inviteAccessOk = inviteAccessChecked && inviteAccessStatus.every((item) => item.canInvite);
|
const inviteAccessOk = inviteAccessChecked && inviteAccessStatus.every((item) => item.canInvite);
|
||||||
const inviteAccessWarn = inviteAccessChecked && !inviteAccessOk;
|
const inviteAccessWarn = inviteAccessChecked && !inviteAccessOk;
|
||||||
|
const inviteAccessById = new Map((inviteAccessStatus || []).map((item) => [Number(item.accountId), item]));
|
||||||
|
const resolveInviteMembership = (accountId) => {
|
||||||
|
const direct = membershipStatus && membershipStatus[accountId] ? membershipStatus[accountId] : null;
|
||||||
|
if (direct) return direct;
|
||||||
|
const access = inviteAccessById.get(Number(accountId));
|
||||||
|
if (!access) return null;
|
||||||
|
return {
|
||||||
|
ourGroupMember: access.member === true,
|
||||||
|
ourGroupPending: false
|
||||||
|
};
|
||||||
|
};
|
||||||
const masterAdminId = Number(selectedTask?.invite_admin_master_id || 0);
|
const masterAdminId = Number(selectedTask?.invite_admin_master_id || 0);
|
||||||
const masterAccessRow = inviteAccessChecked
|
const masterAccessRow = inviteAccessChecked
|
||||||
? (inviteAccessStatus || []).find((item) => Number(item.accountId) === masterAdminId)
|
? (inviteAccessStatus || []).find((item) => Number(item.accountId) === masterAdminId)
|
||||||
@ -121,7 +132,7 @@ export default function useTaskStatusView({
|
|||||||
const account = accountById.get(accountId);
|
const account = accountById.get(accountId);
|
||||||
const label = account ? formatAccountLabel(account) : String(accountId);
|
const label = account ? formatAccountLabel(account) : String(accountId);
|
||||||
const row = inviteById.get(Number(accountId));
|
const row = inviteById.get(Number(accountId));
|
||||||
const member = membershipStatus && membershipStatus[accountId] ? membershipStatus[accountId] : null;
|
const member = resolveInviteMembership(accountId);
|
||||||
const membershipText = !member
|
const membershipText = !member
|
||||||
? "статус участия не проверен"
|
? "статус участия не проверен"
|
||||||
: member.ourGroupMember
|
: member.ourGroupMember
|
||||||
@ -210,7 +221,7 @@ export default function useTaskStatusView({
|
|||||||
);
|
);
|
||||||
const inviteMembershipList = inviteAccountIds.map((id) => ({
|
const inviteMembershipList = inviteAccountIds.map((id) => ({
|
||||||
id,
|
id,
|
||||||
status: membershipStatus ? membershipStatus[id] : null
|
status: resolveInviteMembership(id)
|
||||||
}));
|
}));
|
||||||
const inviteMembershipUnknown = inviteMembershipList.filter((item) => !item.status).length;
|
const inviteMembershipUnknown = inviteMembershipList.filter((item) => !item.status).length;
|
||||||
const inviteMembershipPending = inviteMembershipList.filter((item) => item.status && item.status.ourGroupPending).length;
|
const inviteMembershipPending = inviteMembershipList.filter((item) => item.status && item.status.ourGroupPending).length;
|
||||||
|
|||||||
@ -47,18 +47,6 @@ function AccountsTab({
|
|||||||
const [healthFilter, setHealthFilter] = useState("all");
|
const [healthFilter, setHealthFilter] = useState("all");
|
||||||
const [proxyBusy, setProxyBusy] = useState(false);
|
const [proxyBusy, setProxyBusy] = useState(false);
|
||||||
const [proxyNotice, setProxyNotice] = useState(null);
|
const [proxyNotice, setProxyNotice] = useState(null);
|
||||||
const [proxyImportText, setProxyImportText] = useState("");
|
|
||||||
const [bulkProxyId, setBulkProxyId] = useState(0);
|
|
||||||
const [proxyForm, setProxyForm] = useState({
|
|
||||||
id: 0,
|
|
||||||
name: "",
|
|
||||||
protocol: "socks5",
|
|
||||||
host: "",
|
|
||||||
port: "1080",
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
enabled: true
|
|
||||||
});
|
|
||||||
const inviteAccessById = React.useMemo(() => {
|
const inviteAccessById = React.useMemo(() => {
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
(inviteAccessStatus || []).forEach((item) => {
|
(inviteAccessStatus || []).forEach((item) => {
|
||||||
@ -67,6 +55,24 @@ function AccountsTab({
|
|||||||
});
|
});
|
||||||
return map;
|
return map;
|
||||||
}, [inviteAccessStatus]);
|
}, [inviteAccessStatus]);
|
||||||
|
const resolveMembership = React.useCallback((accountId) => {
|
||||||
|
const direct = membershipStatus && membershipStatus[accountId] ? membershipStatus[accountId] : null;
|
||||||
|
if (direct) return { value: direct, fromInviteAccess: false };
|
||||||
|
const access = inviteAccessById.get(Number(accountId));
|
||||||
|
if (!access) return { value: null, fromInviteAccess: false };
|
||||||
|
return {
|
||||||
|
value: {
|
||||||
|
competitorCount: 0,
|
||||||
|
competitorTotal: 0,
|
||||||
|
competitorGroups: [],
|
||||||
|
ourGroupMember: access.member === true,
|
||||||
|
ourGroupPending: false,
|
||||||
|
ourGroupPendingAt: "",
|
||||||
|
ourGroup: null
|
||||||
|
},
|
||||||
|
fromInviteAccess: true
|
||||||
|
};
|
||||||
|
}, [membershipStatus, inviteAccessById]);
|
||||||
const adminConfirmConfigRisk = React.useMemo(
|
const adminConfirmConfigRisk = React.useMemo(
|
||||||
() => (typeof computeAdminConfirmConfigRisk === "function" ? computeAdminConfirmConfigRisk(taskAccountRoles) : null),
|
() => (typeof computeAdminConfirmConfigRisk === "function" ? computeAdminConfirmConfigRisk(taskAccountRoles) : null),
|
||||||
[computeAdminConfirmConfigRisk, taskAccountRoles]
|
[computeAdminConfirmConfigRisk, taskAccountRoles]
|
||||||
@ -107,18 +113,6 @@ function AccountsTab({
|
|||||||
const visibleFreeOrSelected = (accountBuckets.freeOrSelected || []).filter(healthFilterMatch);
|
const visibleFreeOrSelected = (accountBuckets.freeOrSelected || []).filter(healthFilterMatch);
|
||||||
const visibleBusy = (accountBuckets.busy || []).filter(healthFilterMatch);
|
const visibleBusy = (accountBuckets.busy || []).filter(healthFilterMatch);
|
||||||
const authDupCount = (accounts || []).filter(isAuthKeyDuplicatedAccount).length;
|
const authDupCount = (accounts || []).filter(isAuthKeyDuplicatedAccount).length;
|
||||||
const resetProxyForm = () => {
|
|
||||||
setProxyForm({
|
|
||||||
id: 0,
|
|
||||||
name: "",
|
|
||||||
protocol: "socks5",
|
|
||||||
host: "",
|
|
||||||
port: "1080",
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
enabled: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const setProxyMessage = (text, tone = "info") => {
|
const setProxyMessage = (text, tone = "info") => {
|
||||||
setProxyNotice({ text, tone, at: Date.now() });
|
setProxyNotice({ text, tone, at: Date.now() });
|
||||||
};
|
};
|
||||||
@ -130,61 +124,6 @@ function AccountsTab({
|
|||||||
setProxyBusy(false);
|
setProxyBusy(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onSaveProxy = async () => {
|
|
||||||
const host = String(proxyForm.host || "").trim();
|
|
||||||
const port = Number(proxyForm.port || 0);
|
|
||||||
if (!host || !port) {
|
|
||||||
setProxyMessage("Укажи host и port прокси.", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await runProxyAction(async () => {
|
|
||||||
const result = await saveProxy({
|
|
||||||
id: Number(proxyForm.id || 0),
|
|
||||||
name: proxyForm.name,
|
|
||||||
protocol: proxyForm.protocol,
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
username: proxyForm.username,
|
|
||||||
password: proxyForm.password,
|
|
||||||
enabled: Boolean(proxyForm.enabled)
|
|
||||||
});
|
|
||||||
if (result && result.ok) {
|
|
||||||
const testInfo = result.test
|
|
||||||
? (result.test.ok
|
|
||||||
? ` Проверка: OK${result.test.latencyMs ? ` (${result.test.latencyMs} ms)` : ""}.`
|
|
||||||
: ` Проверка: ${result.test.error || "ошибка"}.`)
|
|
||||||
: "";
|
|
||||||
setProxyMessage(`Прокси сохранен.${testInfo}`, "success");
|
|
||||||
resetProxyForm();
|
|
||||||
} else {
|
|
||||||
setProxyMessage((result && result.error) || "Не удалось сохранить прокси.", "error");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const onTestProxy = async (proxy) => {
|
|
||||||
await runProxyAction(async () => {
|
|
||||||
const result = await testProxy(proxy && proxy.id ? { id: proxy.id } : proxyForm);
|
|
||||||
if (result && result.ok) {
|
|
||||||
setProxyMessage(`Прокси доступен${result.latencyMs ? ` (${result.latencyMs} ms)` : ""}.`, "success");
|
|
||||||
} else {
|
|
||||||
setProxyMessage(`Проверка не прошла: ${(result && result.error) || "ошибка"}.`, "error");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const onDeleteProxy = async (proxy) => {
|
|
||||||
if (!proxy || !proxy.id) return;
|
|
||||||
await runProxyAction(async () => {
|
|
||||||
const result = await removeProxy(proxy.id);
|
|
||||||
if (result && result.ok) {
|
|
||||||
setProxyMessage("Прокси удален.", "success");
|
|
||||||
if (Number(proxyForm.id || 0) === Number(proxy.id)) {
|
|
||||||
resetProxyForm();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setProxyMessage((result && result.error) || "Не удалось удалить прокси.", "error");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const onAccountProxyChange = async (accountId, nextProxyId) => {
|
const onAccountProxyChange = async (accountId, nextProxyId) => {
|
||||||
await runProxyAction(async () => {
|
await runProxyAction(async () => {
|
||||||
try {
|
try {
|
||||||
@ -195,358 +134,17 @@ function AccountsTab({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const parseProxyLine = (line, defaultProtocol = "socks5") => {
|
|
||||||
const raw = String(line || "").trim();
|
|
||||||
if (!raw) return null;
|
|
||||||
try {
|
|
||||||
if (raw.includes("://")) {
|
|
||||||
const url = new URL(raw);
|
|
||||||
const protocol = String(url.protocol || "").replace(":", "").toLowerCase();
|
|
||||||
const host = (url.hostname || "").trim();
|
|
||||||
const port = Number(url.port || 0);
|
|
||||||
if (host && port) {
|
|
||||||
return {
|
|
||||||
protocol: protocol === "socks4" ? "socks4" : "socks5",
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
username: decodeURIComponent(url.username || ""),
|
|
||||||
password: decodeURIComponent(url.password || "")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_error) {
|
|
||||||
// fallback below
|
|
||||||
}
|
|
||||||
const parts = raw.split(":").map((part) => part.trim());
|
|
||||||
if (parts.length < 2) return null;
|
|
||||||
const host = parts[0] || "";
|
|
||||||
const port = Number(parts[1] || 0);
|
|
||||||
if (!host || !port) return null;
|
|
||||||
if (parts.length >= 4) {
|
|
||||||
return {
|
|
||||||
protocol: defaultProtocol === "socks4" ? "socks4" : "socks5",
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
username: parts[2] || "",
|
|
||||||
password: parts.slice(3).join(":") || ""
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
protocol: defaultProtocol === "socks4" ? "socks4" : "socks5",
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
username: "",
|
|
||||||
password: ""
|
|
||||||
};
|
|
||||||
};
|
|
||||||
const onImportProxies = async () => {
|
|
||||||
const lines = String(proxyImportText || "")
|
|
||||||
.split(/\r?\n/)
|
|
||||||
.map((line) => line.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
if (!lines.length) {
|
|
||||||
setProxyMessage("Вставь список прокси для импорта.", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let imported = 0;
|
|
||||||
let failed = 0;
|
|
||||||
await runProxyAction(async () => {
|
|
||||||
for (let i = 0; i < lines.length; i += 1) {
|
|
||||||
const parsed = parseProxyLine(lines[i], proxyForm.protocol || "socks5");
|
|
||||||
if (!parsed) {
|
|
||||||
failed += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const payload = {
|
|
||||||
name: `import_${Date.now()}_${i + 1}`,
|
|
||||||
protocol: parsed.protocol,
|
|
||||||
host: parsed.host,
|
|
||||||
port: parsed.port,
|
|
||||||
username: parsed.username,
|
|
||||||
password: parsed.password,
|
|
||||||
enabled: true
|
|
||||||
};
|
|
||||||
const result = await saveProxy(payload);
|
|
||||||
if (result && result.ok) imported += 1;
|
|
||||||
else failed += 1;
|
|
||||||
}
|
|
||||||
await reloadProxies();
|
|
||||||
});
|
|
||||||
setProxyMessage(`Импорт завершен: добавлено ${imported}, ошибок ${failed}.`, failed ? "warn" : "success");
|
|
||||||
if (imported > 0) {
|
|
||||||
setProxyImportText("");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onImportProxiesFromFile = async () => {
|
|
||||||
const input = document.createElement("input");
|
|
||||||
input.type = "file";
|
|
||||||
input.accept = ".txt,.csv,text/plain,text/csv";
|
|
||||||
input.onchange = async () => {
|
|
||||||
const file = input.files && input.files[0] ? input.files[0] : null;
|
|
||||||
if (!file) return;
|
|
||||||
try {
|
|
||||||
const text = await file.text();
|
|
||||||
setProxyImportText(text || "");
|
|
||||||
setProxyMessage(`Файл загружен: ${file.name}. Нажми «Импортировать».`, "info");
|
|
||||||
} catch (error) {
|
|
||||||
setProxyMessage(`Не удалось прочитать файл: ${error.message || String(error)}`, "error");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
input.click();
|
|
||||||
};
|
|
||||||
const onTestAllProxies = async () => {
|
|
||||||
const rows = Array.isArray(proxies) ? proxies : [];
|
|
||||||
if (!rows.length) {
|
|
||||||
setProxyMessage("Нет прокси для проверки.", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let okCount = 0;
|
|
||||||
let failCount = 0;
|
|
||||||
await runProxyAction(async () => {
|
|
||||||
for (const proxy of rows) {
|
|
||||||
const result = await testProxy({ id: proxy.id });
|
|
||||||
if (result && result.ok) okCount += 1;
|
|
||||||
else failCount += 1;
|
|
||||||
}
|
|
||||||
await reloadProxies();
|
|
||||||
});
|
|
||||||
setProxyMessage(`Проверка завершена: OK ${okCount}, ошибки ${failCount}.`, failCount ? "warn" : "success");
|
|
||||||
};
|
|
||||||
const onAutoDistributeProxies = async () => {
|
|
||||||
const enabledProxies = (proxies || []).filter((proxy) => proxy && proxy.enabled);
|
|
||||||
if (!enabledProxies.length) {
|
|
||||||
setProxyMessage("Нет активных прокси для распределения.", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const accountIds = (selectedAccountIds || []).map((id) => Number(id || 0)).filter((id) => id > 0);
|
|
||||||
if (!accountIds.length) {
|
|
||||||
setProxyMessage("Для распределения выбери аккаунты в задаче.", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const assignments = accountIds.map((accountId, index) => ({
|
|
||||||
accountId,
|
|
||||||
proxyId: Number(enabledProxies[index % enabledProxies.length].id || 0)
|
|
||||||
}));
|
|
||||||
await runProxyAction(async () => {
|
|
||||||
try {
|
|
||||||
const result = await setAccountsProxyMap(assignments);
|
|
||||||
setProxyMessage(
|
|
||||||
`Распределение выполнено: аккаунтов ${accountIds.length}, обновлено ${result.changed}, reconnect ok ${result.reconnected}, ошибок ${result.failed}.`,
|
|
||||||
result.failed ? "warn" : "success"
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
setProxyMessage(error.message || String(error), "error");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const onAssignBulkProxy = async () => {
|
|
||||||
const ids = (selectedAccountIds || []).map((id) => Number(id || 0)).filter((id) => id > 0);
|
|
||||||
if (!ids.length) {
|
|
||||||
setProxyMessage("Для массовой привязки выбери аккаунты в задаче.", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await runProxyAction(async () => {
|
|
||||||
try {
|
|
||||||
const result = await setAccountsProxyBulk(ids, Number(bulkProxyId || 0));
|
|
||||||
setProxyMessage(
|
|
||||||
`Массовая привязка выполнена: ${result.changed}/${ids.length}, reconnect ok ${result.reconnected}, ошибок ${result.failed}.`,
|
|
||||||
result.failed ? "warn" : "success"
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
setProxyMessage(error.message || String(error), "error");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="card">
|
<section className="card">
|
||||||
<div className="row-header">
|
<div className="row-header">
|
||||||
<h3>Аккаунты</h3>
|
<h3>Аккаунты</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-inner" style={{ marginBottom: 12 }}>
|
|
||||||
<div className="row-header">
|
|
||||||
<h4>Мои прокси</h4>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="secondary"
|
|
||||||
onClick={() => runProxyAction(() => reloadProxies())}
|
|
||||||
disabled={proxyBusy}
|
|
||||||
>
|
|
||||||
Обновить
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{proxyNotice && (
|
{proxyNotice && (
|
||||||
<div className={`notice ${proxyNotice.tone === "error" || proxyNotice.tone === "warn" ? "warn" : "ok"}`}>
|
<div className={`notice ${proxyNotice.tone === "error" || proxyNotice.tone === "warn" ? "warn" : "ok"}`}>
|
||||||
{proxyNotice.text}
|
{proxyNotice.text}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="role-presets">
|
|
||||||
<label className="inline-input">
|
|
||||||
Название
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={proxyForm.name}
|
|
||||||
onChange={(event) => setProxyForm((prev) => ({ ...prev, name: event.target.value }))}
|
|
||||||
placeholder="Прокси 1"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="inline-input">
|
|
||||||
Тип
|
|
||||||
<select
|
|
||||||
value={proxyForm.protocol}
|
|
||||||
onChange={(event) => setProxyForm((prev) => ({ ...prev, protocol: event.target.value }))}
|
|
||||||
>
|
|
||||||
<option value="socks5">SOCKS5</option>
|
|
||||||
<option value="socks4">SOCKS4</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label className="inline-input">
|
|
||||||
Host
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={proxyForm.host}
|
|
||||||
onChange={(event) => setProxyForm((prev) => ({ ...prev, host: event.target.value }))}
|
|
||||||
placeholder="127.0.0.1"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="inline-input">
|
|
||||||
Port
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
value={proxyForm.port}
|
|
||||||
onChange={(event) => setProxyForm((prev) => ({ ...prev, port: event.target.value }))}
|
|
||||||
placeholder="1080"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="inline-input">
|
|
||||||
Логин
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={proxyForm.username}
|
|
||||||
onChange={(event) => setProxyForm((prev) => ({ ...prev, username: event.target.value }))}
|
|
||||||
placeholder="username"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="inline-input">
|
|
||||||
Пароль
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={proxyForm.password}
|
|
||||||
onChange={(event) => setProxyForm((prev) => ({ ...prev, password: event.target.value }))}
|
|
||||||
placeholder="password"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="inline-input">
|
|
||||||
Активен
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={Boolean(proxyForm.enabled)}
|
|
||||||
onChange={(event) => setProxyForm((prev) => ({ ...prev, enabled: event.target.checked }))}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<button type="button" className="secondary" onClick={onSaveProxy} disabled={proxyBusy}>
|
|
||||||
{proxyForm.id ? "Обновить прокси" : "Добавить прокси"}
|
|
||||||
</button>
|
|
||||||
<button type="button" className="secondary" onClick={() => onTestProxy(null)} disabled={proxyBusy}>
|
|
||||||
Проверить
|
|
||||||
</button>
|
|
||||||
{proxyForm.id > 0 && (
|
|
||||||
<button type="button" className="secondary" onClick={resetProxyForm} disabled={proxyBusy}>
|
|
||||||
Сбросить форму
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="role-presets" style={{ marginTop: 8 }}>
|
|
||||||
<label className="inline-input" style={{ flex: 1 }}>
|
|
||||||
Импорт списка прокси (по 1 в строке)
|
|
||||||
<textarea
|
|
||||||
rows={4}
|
|
||||||
value={proxyImportText}
|
|
||||||
onChange={(event) => setProxyImportText(event.target.value)}
|
|
||||||
placeholder={"socks5://user:pass@1.2.3.4:1080\n1.2.3.4:1080:user:pass\n1.2.3.4:1080"}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<button type="button" className="secondary" onClick={onImportProxiesFromFile} disabled={proxyBusy}>
|
|
||||||
Загрузить файл
|
|
||||||
</button>
|
|
||||||
<button type="button" className="secondary" onClick={onImportProxies} disabled={proxyBusy}>
|
|
||||||
Импортировать
|
|
||||||
</button>
|
|
||||||
<button type="button" className="secondary" onClick={onTestAllProxies} disabled={proxyBusy}>
|
|
||||||
Проверить все
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="role-presets" style={{ marginTop: 8 }}>
|
|
||||||
<label className="inline-input">
|
|
||||||
Массовый прокси для выбранных аккаунтов
|
|
||||||
<select value={Number(bulkProxyId || 0)} onChange={(event) => setBulkProxyId(Number(event.target.value || 0))}>
|
|
||||||
<option value={0}>Без прокси</option>
|
|
||||||
{(proxies || []).map((proxy) => (
|
|
||||||
<option key={proxy.id} value={proxy.id}>
|
|
||||||
{proxy.name || `${proxy.host}:${proxy.port}`} ({String(proxy.protocol || "socks5").toUpperCase()})
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<button type="button" className="secondary" onClick={onAssignBulkProxy} disabled={proxyBusy}>
|
|
||||||
Применить к выбранным ({(selectedAccountIds || []).length})
|
|
||||||
</button>
|
|
||||||
<button type="button" className="secondary" onClick={onAutoDistributeProxies} disabled={proxyBusy}>
|
|
||||||
Автораспределить
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="account-list">
|
|
||||||
{(proxies || []).length === 0 && <div className="empty">Прокси не добавлены.</div>}
|
|
||||||
{(proxies || []).map((proxy) => (
|
|
||||||
<div key={proxy.id} className="account-row free">
|
|
||||||
<div className="account-main">
|
|
||||||
<div className="account-header">
|
|
||||||
<div>
|
|
||||||
<div className="account-phone">
|
|
||||||
{proxy.name || `Proxy #${proxy.id}`} · {String(proxy.protocol || "socks5").toUpperCase()}
|
|
||||||
</div>
|
|
||||||
<div className={`status ${proxy.status === "ok" ? "ok" : proxy.status === "error" ? "error" : "limited"}`}>
|
|
||||||
{proxy.status || "unknown"}
|
|
||||||
</div>
|
|
||||||
<div className="account-meta">
|
|
||||||
{proxy.host}:{proxy.port}
|
|
||||||
{proxy.username ? ` · ${proxy.username}` : ""}
|
|
||||||
</div>
|
|
||||||
<div className="account-meta">Аккаунтов: {proxy.accountsCount || 0}</div>
|
|
||||||
{proxy.lastError && <div className="account-meta">Ошибка: {proxy.lastError}</div>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="account-actions">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="secondary tiny"
|
|
||||||
onClick={() => setProxyForm({
|
|
||||||
id: Number(proxy.id || 0),
|
|
||||||
name: proxy.name || "",
|
|
||||||
protocol: proxy.protocol || "socks5",
|
|
||||||
host: proxy.host || "",
|
|
||||||
port: String(proxy.port || 1080),
|
|
||||||
username: proxy.username || "",
|
|
||||||
password: proxy.password || "",
|
|
||||||
enabled: Boolean(proxy.enabled)
|
|
||||||
})}
|
|
||||||
disabled={proxyBusy}
|
|
||||||
>
|
|
||||||
Редактировать
|
|
||||||
</button>
|
|
||||||
<button type="button" className="secondary tiny" onClick={() => onTestProxy(proxy)} disabled={proxyBusy}>
|
|
||||||
Проверить
|
|
||||||
</button>
|
|
||||||
<button type="button" className="danger tiny" onClick={() => onDeleteProxy(proxy)} disabled={proxyBusy}>
|
|
||||||
Удалить
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="task-filters">
|
<div className="task-filters">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -685,7 +283,9 @@ function AccountsTab({
|
|||||||
{accounts.length === 0 && <div className="empty">Аккаунты не добавлены.</div>}
|
{accounts.length === 0 && <div className="empty">Аккаунты не добавлены.</div>}
|
||||||
{visibleFreeOrSelected.map((account) => {
|
{visibleFreeOrSelected.map((account) => {
|
||||||
const assignedTasks = assignedAccountMap.get(account.id) || [];
|
const assignedTasks = assignedAccountMap.get(account.id) || [];
|
||||||
const membership = membershipStatus[account.id];
|
const membershipResolved = resolveMembership(account.id);
|
||||||
|
const membership = membershipResolved.value;
|
||||||
|
const membershipFromInviteAccess = membershipResolved.fromInviteAccess;
|
||||||
const stats = accountStatsMap.get(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;
|
||||||
@ -713,7 +313,7 @@ function AccountsTab({
|
|||||||
: membership && membership.ourGroupPending
|
: membership && membership.ourGroupPending
|
||||||
? [`Заявка отправлена${membership.ourGroupPendingAt ? ` (${new Date(membership.ourGroupPendingAt).toLocaleString()})` : ""}`]
|
? [`Заявка отправлена${membership.ourGroupPendingAt ? ` (${new Date(membership.ourGroupPendingAt).toLocaleString()})` : ""}`]
|
||||||
: [];
|
: [];
|
||||||
const competitorInfo = membership
|
const competitorInfo = membership && !membershipFromInviteAccess
|
||||||
? `В конкурентах: ${membership.competitorCount}/${membership.competitorTotal}`
|
? `В конкурентах: ${membership.competitorCount}/${membership.competitorTotal}`
|
||||||
: "В конкурентах: —";
|
: "В конкурентах: —";
|
||||||
const ourInfo = membership
|
const ourInfo = membership
|
||||||
@ -950,7 +550,9 @@ function AccountsTab({
|
|||||||
return roles ? `${name} (${roles})` : name;
|
return roles ? `${name} (${roles})` : name;
|
||||||
})
|
})
|
||||||
.join(", ");
|
.join(", ");
|
||||||
const membership = membershipStatus[account.id];
|
const membershipResolved = resolveMembership(account.id);
|
||||||
|
const membership = membershipResolved.value;
|
||||||
|
const membershipFromInviteAccess = membershipResolved.fromInviteAccess;
|
||||||
const stats = accountStatsMap.get(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;
|
||||||
@ -978,7 +580,7 @@ function AccountsTab({
|
|||||||
: membership && membership.ourGroupPending
|
: membership && membership.ourGroupPending
|
||||||
? [`Заявка отправлена${membership.ourGroupPendingAt ? ` (${new Date(membership.ourGroupPendingAt).toLocaleString()})` : ""}`]
|
? [`Заявка отправлена${membership.ourGroupPendingAt ? ` (${new Date(membership.ourGroupPendingAt).toLocaleString()})` : ""}`]
|
||||||
: [];
|
: [];
|
||||||
const competitorInfo = membership
|
const competitorInfo = membership && !membershipFromInviteAccess
|
||||||
? `В конкурентах: ${membership.competitorCount}/${membership.competitorTotal}`
|
? `В конкурентах: ${membership.competitorCount}/${membership.competitorTotal}`
|
||||||
: "В конкурентах: —";
|
: "В конкурентах: —";
|
||||||
const ourInfo = membership
|
const ourInfo = membership
|
||||||
|
|||||||
@ -1,12 +1,28 @@
|
|||||||
import React, { memo, useMemo, useState } from "react";
|
import React, { memo, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
const TEMP_ADMIN_FILTER = "__temp_admin__";
|
const TEMP_ADMIN_FILTER = "__temp_admin__";
|
||||||
|
|
||||||
function EventsTab({ accountEvents, formatTimestamp, onClearEvents, accountById, formatAccountLabel }) {
|
function EventsTab({
|
||||||
|
accountEvents,
|
||||||
|
selectedTaskId,
|
||||||
|
selectedTask,
|
||||||
|
competitorLinks,
|
||||||
|
formatTimestamp,
|
||||||
|
onClearEvents,
|
||||||
|
accountById,
|
||||||
|
formatAccountLabel
|
||||||
|
}) {
|
||||||
const [typeFilter, setTypeFilter] = useState("all");
|
const [typeFilter, setTypeFilter] = useState("all");
|
||||||
|
const [scopeFilter, setScopeFilter] = useState(selectedTaskId ? "task" : "all");
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [expandedEventId, setExpandedEventId] = useState(null);
|
const [expandedEventId, setExpandedEventId] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedTaskId && scopeFilter === "task") {
|
||||||
|
setScopeFilter("all");
|
||||||
|
}
|
||||||
|
}, [selectedTaskId, scopeFilter]);
|
||||||
|
|
||||||
const buildEventSummary = (event) => {
|
const buildEventSummary = (event) => {
|
||||||
const firstLine = event.message ? String(event.message).split("\n")[0] : "";
|
const firstLine = event.message ? String(event.message).split("\n")[0] : "";
|
||||||
const extractUserLabel = (line) => {
|
const extractUserLabel = (line) => {
|
||||||
@ -133,7 +149,20 @@ function EventsTab({ accountEvents, formatTimestamp, onClearEvents, accountById,
|
|||||||
|
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
const q = query.trim().toLowerCase();
|
const q = query.trim().toLowerCase();
|
||||||
|
const taskIdToken = selectedTaskId ? `задача ${selectedTaskId}` : "";
|
||||||
|
const ourGroup = String(selectedTask && selectedTask.our_group ? selectedTask.our_group : "").trim();
|
||||||
|
const links = Array.isArray(competitorLinks) ? competitorLinks.filter(Boolean) : [];
|
||||||
|
const belongsToTask = (event) => {
|
||||||
|
if (!selectedTaskId) return true;
|
||||||
|
const message = String(event && event.message ? event.message : "");
|
||||||
|
if (!message) return false;
|
||||||
|
if (taskIdToken && message.includes(taskIdToken)) return true;
|
||||||
|
if (ourGroup && message.includes(ourGroup)) return true;
|
||||||
|
if (links.some((link) => message.includes(link))) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
return accountEvents.filter((event) => {
|
return accountEvents.filter((event) => {
|
||||||
|
if (scopeFilter === "task" && !belongsToTask(event)) return false;
|
||||||
if (typeFilter === TEMP_ADMIN_FILTER) {
|
if (typeFilter === TEMP_ADMIN_FILTER) {
|
||||||
if (!String(event.eventType || "").startsWith("temp_admin_")) return false;
|
if (!String(event.eventType || "").startsWith("temp_admin_")) return false;
|
||||||
} else if (typeFilter !== "all" && event.eventType !== typeFilter) {
|
} else if (typeFilter !== "all" && event.eventType !== typeFilter) {
|
||||||
@ -145,7 +174,7 @@ function EventsTab({ accountEvents, formatTimestamp, onClearEvents, accountById,
|
|||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
return text.includes(q);
|
return text.includes(q);
|
||||||
});
|
});
|
||||||
}, [accountEvents, typeFilter, query]);
|
}, [accountEvents, typeFilter, scopeFilter, query, selectedTaskId, selectedTask, competitorLinks]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="card logs">
|
<section className="card logs">
|
||||||
@ -165,6 +194,24 @@ function EventsTab({ accountEvents, formatTimestamp, onClearEvents, accountById,
|
|||||||
onChange={(event) => setQuery(event.target.value)}
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
placeholder="Поиск по событиям"
|
placeholder="Поиск по событиям"
|
||||||
/>
|
/>
|
||||||
|
<div className="row-inline">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`chip ${scopeFilter === "task" ? "active" : ""}`}
|
||||||
|
onClick={() => setScopeFilter("task")}
|
||||||
|
disabled={!selectedTaskId}
|
||||||
|
title={selectedTaskId ? "Показывать только события текущей задачи" : "Сначала выбери задачу"}
|
||||||
|
>
|
||||||
|
Текущая задача
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`chip ${scopeFilter === "all" ? "active" : ""}`}
|
||||||
|
onClick={() => setScopeFilter("all")}
|
||||||
|
>
|
||||||
|
Все задачи
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div className="task-filters">
|
<div className="task-filters">
|
||||||
{eventTypes.map((type) => (
|
{eventTypes.map((type) => (
|
||||||
<button
|
<button
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user