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: [] };
|
||||
let restrictedAccounts = [];
|
||||
let totalInvites = 0;
|
||||
let totalInvitesSuccess = 0;
|
||||
let totalInvitesAttempts = 0;
|
||||
let taskInviteLimitTotal = 0;
|
||||
let accountDailyLimitTotal = 0;
|
||||
let inviteAccountsCount = 0;
|
||||
@ -1385,11 +1387,13 @@ ipcMain.handle("tasks:status", (_event, id) => {
|
||||
const account = accountsById.get(row.account_id);
|
||||
return sum + Math.max(0, Number(account && account.daily_limit ? account.daily_limit : 0));
|
||||
}, 0);
|
||||
totalInvites =
|
||||
Number(store.countInvitesByStatus(id, "success") || 0)
|
||||
totalInvitesSuccess = Number(store.countInvitesByStatus(id, "success") || 0);
|
||||
totalInvitesAttempts =
|
||||
totalInvitesSuccess
|
||||
+ Number(store.countInvitesByStatus(id, "failed") || 0)
|
||||
+ Number(store.countInvitesByStatus(id, "skipped") || 0)
|
||||
+ Number(store.countInvitesByStatus(id, "unconfirmed") || 0);
|
||||
totalInvites = totalInvitesAttempts;
|
||||
const monitorRows = accountRows.filter((row) => row.role_monitor);
|
||||
if (!inviteRows.length) {
|
||||
const fallbackAvailable = accountRows
|
||||
@ -1510,6 +1514,8 @@ ipcMain.handle("tasks:status", (_event, id) => {
|
||||
queueCount,
|
||||
dailyUsed,
|
||||
totalInvites,
|
||||
totalInvitesSuccess,
|
||||
totalInvitesAttempts,
|
||||
unconfirmedCount,
|
||||
dailyLimit: effectiveLimit,
|
||||
taskDailyLimitBase: task ? Number(task.daily_limit || 0) : 0,
|
||||
|
||||
@ -87,9 +87,11 @@ export default function TasksSidebar({
|
||||
const dailyLabel = status
|
||||
? `Лимит сегодня: ${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 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 lastMessageRaw = status && status.monitorInfo && status.monitorInfo.lastMessageAt
|
||||
? status.monitorInfo.lastMessageAt
|
||||
@ -115,9 +117,11 @@ export default function TasksSidebar({
|
||||
`Статус: ${statusLabel}`,
|
||||
`Очередь: ${status ? status.queueCount : "—"}`,
|
||||
`Лимит сегодня: ${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.accountDailyLimitTotal || 0 : "—"}`,
|
||||
`Лимит аккаунтов (цикл): ${status ? status.taskInviteLimitTotal || 0 : "—"}`,
|
||||
`Потолок аккаунтов (день): ${status ? status.accountDailyLimitTotal || 0 : "—"}`,
|
||||
`Мониторинг: ${monitoring ? "активен" : "нет"}`,
|
||||
`Мониторит: ${monitorLabel}`,
|
||||
`Последнее: ${lastMessage}`,
|
||||
@ -146,11 +150,15 @@ export default function TasksSidebar({
|
||||
<div className="task-meta-row">
|
||||
<span className="task-meta">{queueLabel}</span>
|
||||
<span className="task-meta">{dailyLabel}</span>
|
||||
<span className="task-meta">{totalLabel}</span>
|
||||
<span className="task-meta">{totalSuccessLabel}</span>
|
||||
</div>
|
||||
<div className="task-meta-row">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -340,6 +340,12 @@ export default function useTabProps(
|
||||
|
||||
const eventsTabProps = {
|
||||
accountEvents,
|
||||
selectedTaskId,
|
||||
selectedTask,
|
||||
competitorLinks: String(competitorText || "")
|
||||
.split("\n")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean),
|
||||
formatTimestamp,
|
||||
onClearEvents,
|
||||
accountById,
|
||||
|
||||
@ -72,6 +72,17 @@ export default function useTaskStatusView({
|
||||
const inviteAccessChecked = inviteAccessStatus && inviteAccessStatus.length > 0;
|
||||
const inviteAccessOk = inviteAccessChecked && inviteAccessStatus.every((item) => item.canInvite);
|
||||
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 masterAccessRow = inviteAccessChecked
|
||||
? (inviteAccessStatus || []).find((item) => Number(item.accountId) === masterAdminId)
|
||||
@ -121,7 +132,7 @@ export default function useTaskStatusView({
|
||||
const account = accountById.get(accountId);
|
||||
const label = account ? formatAccountLabel(account) : String(accountId);
|
||||
const row = inviteById.get(Number(accountId));
|
||||
const member = membershipStatus && membershipStatus[accountId] ? membershipStatus[accountId] : null;
|
||||
const member = resolveInviteMembership(accountId);
|
||||
const membershipText = !member
|
||||
? "статус участия не проверен"
|
||||
: member.ourGroupMember
|
||||
@ -210,7 +221,7 @@ export default function useTaskStatusView({
|
||||
);
|
||||
const inviteMembershipList = inviteAccountIds.map((id) => ({
|
||||
id,
|
||||
status: membershipStatus ? membershipStatus[id] : null
|
||||
status: resolveInviteMembership(id)
|
||||
}));
|
||||
const inviteMembershipUnknown = inviteMembershipList.filter((item) => !item.status).length;
|
||||
const inviteMembershipPending = inviteMembershipList.filter((item) => item.status && item.status.ourGroupPending).length;
|
||||
|
||||
@ -47,18 +47,6 @@ function AccountsTab({
|
||||
const [healthFilter, setHealthFilter] = useState("all");
|
||||
const [proxyBusy, setProxyBusy] = useState(false);
|
||||
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 map = new Map();
|
||||
(inviteAccessStatus || []).forEach((item) => {
|
||||
@ -67,6 +55,24 @@ function AccountsTab({
|
||||
});
|
||||
return map;
|
||||
}, [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(
|
||||
() => (typeof computeAdminConfirmConfigRisk === "function" ? computeAdminConfirmConfigRisk(taskAccountRoles) : null),
|
||||
[computeAdminConfirmConfigRisk, taskAccountRoles]
|
||||
@ -107,18 +113,6 @@ function AccountsTab({
|
||||
const visibleFreeOrSelected = (accountBuckets.freeOrSelected || []).filter(healthFilterMatch);
|
||||
const visibleBusy = (accountBuckets.busy || []).filter(healthFilterMatch);
|
||||
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") => {
|
||||
setProxyNotice({ text, tone, at: Date.now() });
|
||||
};
|
||||
@ -130,61 +124,6 @@ function AccountsTab({
|
||||
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) => {
|
||||
await runProxyAction(async () => {
|
||||
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 (
|
||||
<section className="card">
|
||||
<div className="row-header">
|
||||
<h3>Аккаунты</h3>
|
||||
</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 && (
|
||||
<div className={`notice ${proxyNotice.tone === "error" || proxyNotice.tone === "warn" ? "warn" : "ok"}`}>
|
||||
{proxyNotice.text}
|
||||
</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">
|
||||
<button
|
||||
type="button"
|
||||
@ -685,7 +283,9 @@ function AccountsTab({
|
||||
{accounts.length === 0 && <div className="empty">Аккаунты не добавлены.</div>}
|
||||
{visibleFreeOrSelected.map((account) => {
|
||||
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 remaining = stats && stats.remainingToday != null ? stats.remainingToday : null;
|
||||
const used = stats ? stats.usedToday : 0;
|
||||
@ -713,7 +313,7 @@ function AccountsTab({
|
||||
: membership && membership.ourGroupPending
|
||||
? [`Заявка отправлена${membership.ourGroupPendingAt ? ` (${new Date(membership.ourGroupPendingAt).toLocaleString()})` : ""}`]
|
||||
: [];
|
||||
const competitorInfo = membership
|
||||
const competitorInfo = membership && !membershipFromInviteAccess
|
||||
? `В конкурентах: ${membership.competitorCount}/${membership.competitorTotal}`
|
||||
: "В конкурентах: —";
|
||||
const ourInfo = membership
|
||||
@ -950,7 +550,9 @@ function AccountsTab({
|
||||
return roles ? `${name} (${roles})` : name;
|
||||
})
|
||||
.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 remaining = stats && stats.remainingToday != null ? stats.remainingToday : null;
|
||||
const used = stats ? stats.usedToday : 0;
|
||||
@ -978,7 +580,7 @@ function AccountsTab({
|
||||
: membership && membership.ourGroupPending
|
||||
? [`Заявка отправлена${membership.ourGroupPendingAt ? ` (${new Date(membership.ourGroupPendingAt).toLocaleString()})` : ""}`]
|
||||
: [];
|
||||
const competitorInfo = membership
|
||||
const competitorInfo = membership && !membershipFromInviteAccess
|
||||
? `В конкурентах: ${membership.competitorCount}/${membership.competitorTotal}`
|
||||
: "В конкурентах: —";
|
||||
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__";
|
||||
|
||||
function EventsTab({ accountEvents, formatTimestamp, onClearEvents, accountById, formatAccountLabel }) {
|
||||
function EventsTab({
|
||||
accountEvents,
|
||||
selectedTaskId,
|
||||
selectedTask,
|
||||
competitorLinks,
|
||||
formatTimestamp,
|
||||
onClearEvents,
|
||||
accountById,
|
||||
formatAccountLabel
|
||||
}) {
|
||||
const [typeFilter, setTypeFilter] = useState("all");
|
||||
const [scopeFilter, setScopeFilter] = useState(selectedTaskId ? "task" : "all");
|
||||
const [query, setQuery] = useState("");
|
||||
const [expandedEventId, setExpandedEventId] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedTaskId && scopeFilter === "task") {
|
||||
setScopeFilter("all");
|
||||
}
|
||||
}, [selectedTaskId, scopeFilter]);
|
||||
|
||||
const buildEventSummary = (event) => {
|
||||
const firstLine = event.message ? String(event.message).split("\n")[0] : "";
|
||||
const extractUserLabel = (line) => {
|
||||
@ -133,7 +149,20 @@ function EventsTab({ accountEvents, formatTimestamp, onClearEvents, accountById,
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
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) => {
|
||||
if (scopeFilter === "task" && !belongsToTask(event)) return false;
|
||||
if (typeFilter === TEMP_ADMIN_FILTER) {
|
||||
if (!String(event.eventType || "").startsWith("temp_admin_")) return false;
|
||||
} else if (typeFilter !== "all" && event.eventType !== typeFilter) {
|
||||
@ -145,7 +174,7 @@ function EventsTab({ accountEvents, formatTimestamp, onClearEvents, accountById,
|
||||
.toLowerCase();
|
||||
return text.includes(q);
|
||||
});
|
||||
}, [accountEvents, typeFilter, query]);
|
||||
}, [accountEvents, typeFilter, scopeFilter, query, selectedTaskId, selectedTask, competitorLinks]);
|
||||
|
||||
return (
|
||||
<section className="card logs">
|
||||
@ -165,6 +194,24 @@ function EventsTab({ accountEvents, formatTimestamp, onClearEvents, accountById,
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
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">
|
||||
{eventTypes.map((type) => (
|
||||
<button
|
||||
|
||||
Loading…
Reference in New Issue
Block a user