some
This commit is contained in:
parent
a5d55012a8
commit
d299eb36be
@ -13,6 +13,97 @@ let telegram;
|
|||||||
let scheduler;
|
let scheduler;
|
||||||
const taskRunners = new Map();
|
const taskRunners = new Map();
|
||||||
|
|
||||||
|
const filterTaskRolesByAccounts = (taskId, roles, accounts) => {
|
||||||
|
const accountMap = new Map(accounts.map((acc) => [acc.id, acc]));
|
||||||
|
const filtered = [];
|
||||||
|
let removedMissing = 0;
|
||||||
|
let removedError = 0;
|
||||||
|
roles.forEach((row) => {
|
||||||
|
const account = accountMap.get(row.account_id);
|
||||||
|
if (!account) {
|
||||||
|
removedMissing += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (account.status && account.status !== "ok") {
|
||||||
|
removedError += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
filtered.push({
|
||||||
|
accountId: row.account_id,
|
||||||
|
roleMonitor: Boolean(row.role_monitor),
|
||||||
|
roleInvite: Boolean(row.role_invite)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (removedMissing || removedError) {
|
||||||
|
store.setTaskAccountRoles(taskId, filtered);
|
||||||
|
}
|
||||||
|
return { filtered, removedMissing, removedError };
|
||||||
|
};
|
||||||
|
|
||||||
|
const startTaskWithChecks = async (id) => {
|
||||||
|
const task = store.getTask(id);
|
||||||
|
if (!task) return { ok: false, error: "Task not found" };
|
||||||
|
const competitors = store.listTaskCompetitors(id).map((row) => row.link);
|
||||||
|
const taskAccounts = store.listTaskAccounts(id);
|
||||||
|
const existingAccounts = store.listAccounts();
|
||||||
|
const filteredResult = filterTaskRolesByAccounts(id, taskAccounts, existingAccounts);
|
||||||
|
const filteredRoles = filteredResult.filtered;
|
||||||
|
const inviteIds = filteredRoles.filter((row) => row.roleInvite).map((row) => row.accountId);
|
||||||
|
const monitorIds = filteredRoles.filter((row) => row.roleMonitor).map((row) => row.accountId);
|
||||||
|
if (!inviteIds.length) {
|
||||||
|
return { ok: false, error: "Нет аккаунтов с ролью инвайта." };
|
||||||
|
}
|
||||||
|
if (!monitorIds.length) {
|
||||||
|
return { ok: false, error: "Нет аккаунтов с ролью мониторинга." };
|
||||||
|
}
|
||||||
|
const accessCheck = await telegram.checkGroupAccess(competitors, task.our_group);
|
||||||
|
if (accessCheck && accessCheck.ok) {
|
||||||
|
const ourAccess = accessCheck.result.find((item) => item.type === "our");
|
||||||
|
if (ourAccess && !ourAccess.ok) {
|
||||||
|
return { ok: false, error: `Нет доступа к нашей группе: ${ourAccess.details || ourAccess.value}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const inviteAccess = await telegram.checkInvitePermissions(task, inviteIds);
|
||||||
|
if (inviteAccess && inviteAccess.ok) {
|
||||||
|
store.setTaskInviteAccess(id, inviteAccess.result || []);
|
||||||
|
} else if (inviteAccess && inviteAccess.error) {
|
||||||
|
return { ok: false, error: inviteAccess.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
let runner = taskRunners.get(id);
|
||||||
|
if (!runner) {
|
||||||
|
runner = new TaskRunner(store, telegram, task);
|
||||||
|
taskRunners.set(id, runner);
|
||||||
|
} else {
|
||||||
|
runner.task = task;
|
||||||
|
}
|
||||||
|
await runner.start();
|
||||||
|
const warnings = [];
|
||||||
|
if (accessCheck && accessCheck.ok) {
|
||||||
|
const competitorIssues = accessCheck.result.filter((item) => item.type === "competitor" && !item.ok);
|
||||||
|
if (competitorIssues.length) {
|
||||||
|
warnings.push(`Нет доступа к ${competitorIssues.length} группе(ам) конкурентов.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inviteAccess && inviteAccess.ok) {
|
||||||
|
const missingSessions = (inviteAccess.result || []).filter((row) => !row.ok || row.reason === "Сессия не подключена");
|
||||||
|
if (missingSessions.length) {
|
||||||
|
warnings.push(`Сессии не подключены: ${missingSessions.length} аккаунт(ов).`);
|
||||||
|
}
|
||||||
|
const noRights = (inviteAccess.result || []).filter((row) => !row.canInvite);
|
||||||
|
if (noRights.length) {
|
||||||
|
warnings.push(`Нет прав инвайта у ${noRights.length} аккаунт(ов).`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (filteredResult.removedError) {
|
||||||
|
warnings.push(`Отключены аккаунты с ошибкой: ${filteredResult.removedError}.`);
|
||||||
|
}
|
||||||
|
if (filteredResult.removedMissing) {
|
||||||
|
warnings.push(`Удалены отсутствующие аккаунты: ${filteredResult.removedMissing}.`);
|
||||||
|
}
|
||||||
|
return { ok: true, warnings };
|
||||||
|
};
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
const iconPath = path.join(__dirname, "..", "..", "resources", "icon.png");
|
const iconPath = path.join(__dirname, "..", "..", "resources", "icon.png");
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
@ -236,17 +327,7 @@ ipcMain.handle("tasks:delete", (_event, id) => {
|
|||||||
return { ok: true };
|
return { ok: true };
|
||||||
});
|
});
|
||||||
ipcMain.handle("tasks:start", async (_event, id) => {
|
ipcMain.handle("tasks:start", async (_event, id) => {
|
||||||
const task = store.getTask(id);
|
return startTaskWithChecks(id);
|
||||||
if (!task) return { ok: false, error: "Task not found" };
|
|
||||||
let runner = taskRunners.get(id);
|
|
||||||
if (!runner) {
|
|
||||||
runner = new TaskRunner(store, telegram, task);
|
|
||||||
taskRunners.set(id, runner);
|
|
||||||
} else {
|
|
||||||
runner.task = task;
|
|
||||||
}
|
|
||||||
await runner.start();
|
|
||||||
return { ok: true };
|
|
||||||
});
|
});
|
||||||
ipcMain.handle("tasks:stop", (_event, id) => {
|
ipcMain.handle("tasks:stop", (_event, id) => {
|
||||||
const runner = taskRunners.get(id);
|
const runner = taskRunners.get(id);
|
||||||
@ -310,15 +391,12 @@ ipcMain.handle("tasks:startAll", async () => {
|
|||||||
skipped += 1;
|
skipped += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let runner = taskRunners.get(task.id);
|
|
||||||
if (!runner) {
|
|
||||||
runner = new TaskRunner(store, telegram, task);
|
|
||||||
taskRunners.set(task.id, runner);
|
|
||||||
} else {
|
|
||||||
runner.task = task;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await runner.start();
|
const result = await startTaskWithChecks(task.id);
|
||||||
|
if (!result.ok) {
|
||||||
|
errors.push({ id: task.id, error: result.error || "start failed" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
started += 1;
|
started += 1;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errors.push({ id: task.id, error: error.message || String(error) });
|
errors.push({ id: task.id, error: error.message || String(error) });
|
||||||
@ -342,24 +420,48 @@ ipcMain.handle("tasks:status", (_event, id) => {
|
|||||||
const task = store.getTask(id);
|
const task = store.getTask(id);
|
||||||
const monitorInfo = telegram.getTaskMonitorInfo(id);
|
const monitorInfo = telegram.getTaskMonitorInfo(id);
|
||||||
const warnings = [];
|
const warnings = [];
|
||||||
|
const readiness = { ok: true, reasons: [] };
|
||||||
if (task) {
|
if (task) {
|
||||||
const accountRows = store.listTaskAccounts(id);
|
const accountRows = store.listTaskAccounts(id);
|
||||||
|
const accounts = store.listAccounts();
|
||||||
|
const accountsById = new Map(accounts.map((acc) => [acc.id, acc]));
|
||||||
|
if (runner && runner.isRunning()) {
|
||||||
|
const sanitized = filterTaskRolesByAccounts(id, accountRows, accounts);
|
||||||
|
if (sanitized.removedError || sanitized.removedMissing) {
|
||||||
|
warnings.push(`Авто-синхронизация ролей: удалено ${sanitized.removedMissing + sanitized.removedError} аккаунт(ов).`);
|
||||||
|
}
|
||||||
|
if (!sanitized.filtered.length) {
|
||||||
|
warnings.push("Задача остановлена: нет доступных аккаунтов.");
|
||||||
|
runner.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const inviteRows = accountRows.filter((row) => row.role_invite);
|
||||||
|
const monitorRows = accountRows.filter((row) => row.role_monitor);
|
||||||
|
if (!inviteRows.length) {
|
||||||
|
readiness.ok = false;
|
||||||
|
readiness.reasons.push("Нет аккаунтов с ролью инвайта.");
|
||||||
|
}
|
||||||
|
if (!monitorRows.length) {
|
||||||
|
readiness.ok = false;
|
||||||
|
readiness.reasons.push("Нет аккаунтов с ролью мониторинга.");
|
||||||
|
}
|
||||||
if (task.require_same_bot_in_both) {
|
if (task.require_same_bot_in_both) {
|
||||||
const hasSame = accountRows.some((row) => row.role_monitor && row.role_invite);
|
const hasSame = accountRows.some((row) => row.role_monitor && row.role_invite);
|
||||||
if (!hasSame) {
|
if (!hasSame) {
|
||||||
warnings.push("Режим “один бот в обоих группах” включен, но нет аккаунта с обеими ролями.");
|
warnings.push("Режим “один бот в обоих группах” включен, но нет аккаунта с обеими ролями.");
|
||||||
|
readiness.ok = false;
|
||||||
|
readiness.reasons.push("Нет аккаунта с двумя ролями в режиме “один бот”.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const allAssignments = store.listAllTaskAccounts();
|
const allAssignments = store.listAllTaskAccounts();
|
||||||
const accountMap = new Map();
|
const accountTaskMap = new Map();
|
||||||
allAssignments.forEach((row) => {
|
allAssignments.forEach((row) => {
|
||||||
if (!accountMap.has(row.account_id)) accountMap.set(row.account_id, new Set());
|
if (!accountTaskMap.has(row.account_id)) accountTaskMap.set(row.account_id, new Set());
|
||||||
accountMap.get(row.account_id).add(row.task_id);
|
accountTaskMap.get(row.account_id).add(row.task_id);
|
||||||
});
|
});
|
||||||
const accountsById = new Map(store.listAccounts().map((acc) => [acc.id, acc]));
|
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
accountRows.forEach((row) => {
|
accountRows.forEach((row) => {
|
||||||
const tasksForAccount = accountMap.get(row.account_id);
|
const tasksForAccount = accountTaskMap.get(row.account_id);
|
||||||
if (tasksForAccount && tasksForAccount.size > 1 && !seen.has(row.account_id)) {
|
if (tasksForAccount && tasksForAccount.size > 1 && !seen.has(row.account_id)) {
|
||||||
seen.add(row.account_id);
|
seen.add(row.account_id);
|
||||||
const account = accountsById.get(row.account_id);
|
const account = accountsById.get(row.account_id);
|
||||||
@ -367,6 +469,57 @@ ipcMain.handle("tasks:status", (_event, id) => {
|
|||||||
warnings.push(`Аккаунт ${label} используется в ${tasksForAccount.size} задачах. Лимиты действий/групп общие.`);
|
warnings.push(`Аккаунт ${label} используется в ${tasksForAccount.size} задачах. Лимиты действий/групп общие.`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (inviteRows.length) {
|
||||||
|
const inviteAccounts = inviteRows.map((row) => accountsById.get(row.account_id)).filter(Boolean);
|
||||||
|
const badSessions = inviteAccounts.filter((acc) => acc.status && acc.status !== "ok");
|
||||||
|
if (badSessions.length) {
|
||||||
|
readiness.ok = false;
|
||||||
|
readiness.reasons.push(`Есть аккаунты с ошибкой сессии: ${badSessions.length}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runner && runner.isRunning() && queueCount === 0) {
|
||||||
|
if (!monitorInfo || !monitorInfo.monitoring) {
|
||||||
|
warnings.push("Очередь пуста: мониторинг не активен.");
|
||||||
|
} else if (!monitorInfo.groups || monitorInfo.groups.length === 0) {
|
||||||
|
warnings.push("Очередь пуста: нет групп в мониторинге.");
|
||||||
|
} else if (!monitorInfo.lastMessageAt) {
|
||||||
|
warnings.push("Очередь пуста: новых сообщений пока нет.");
|
||||||
|
} else {
|
||||||
|
warnings.push(`Очередь пуста: последнее сообщение ${monitorInfo.lastMessageAt}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.our_group && (task.our_group.includes("joinchat/") || task.our_group.includes("t.me/+"))) {
|
||||||
|
warnings.push("Целевая группа указана по инвайт-ссылке — доступ может быть ограничен.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.task_invite_access) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(task.task_invite_access);
|
||||||
|
if (Array.isArray(parsed) && parsed.length) {
|
||||||
|
const total = parsed.length;
|
||||||
|
const canInvite = parsed.filter((row) => row.canInvite).length;
|
||||||
|
const disconnected = parsed.filter((row) => row.reason === "Сессия не подключена").length;
|
||||||
|
const isChannel = parsed.some((row) => row.targetType === "channel");
|
||||||
|
const checkedAt = task.task_invite_access_at || "";
|
||||||
|
warnings.push(`Права инвайта: ${canInvite}/${total} аккаунтов могут добавлять.${checkedAt ? ` Проверка: ${checkedAt}.` : ""}`);
|
||||||
|
if (disconnected) {
|
||||||
|
warnings.push(`Сессия не подключена: ${disconnected} аккаунт(ов).`);
|
||||||
|
}
|
||||||
|
if (isChannel) {
|
||||||
|
warnings.push("Цель — канал: добавлять участников могут только админы.");
|
||||||
|
}
|
||||||
|
if (canInvite === 0) {
|
||||||
|
readiness.ok = false;
|
||||||
|
readiness.reasons.push("Нет аккаунтов с правами инвайта.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// ignore parsing errors
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
running: runner ? runner.isRunning() : false,
|
running: runner ? runner.isRunning() : false,
|
||||||
@ -379,7 +532,8 @@ ipcMain.handle("tasks:status", (_event, id) => {
|
|||||||
nextInviteAccountId: runner ? runner.getNextInviteAccountId() : 0,
|
nextInviteAccountId: runner ? runner.getNextInviteAccountId() : 0,
|
||||||
lastInviteAccountId: runner ? runner.getLastInviteAccountId() : 0,
|
lastInviteAccountId: runner ? runner.getLastInviteAccountId() : 0,
|
||||||
pendingStats: store.getPendingStats(id),
|
pendingStats: store.getPendingStats(id),
|
||||||
warnings
|
warnings,
|
||||||
|
readiness
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -425,7 +579,11 @@ ipcMain.handle("tasks:checkInviteAccess", async (_event, id) => {
|
|||||||
const accountIds = accountRows
|
const accountIds = accountRows
|
||||||
.filter((row) => existingIds.has(row.account_id))
|
.filter((row) => existingIds.has(row.account_id))
|
||||||
.map((row) => row.account_id);
|
.map((row) => row.account_id);
|
||||||
return telegram.checkInvitePermissions(task, accountIds);
|
const result = await telegram.checkInvitePermissions(task, accountIds);
|
||||||
|
if (result && result.ok) {
|
||||||
|
store.setTaskInviteAccess(id, result.result || []);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("tasks:groupVisibility", async (_event, id) => {
|
ipcMain.handle("tasks:groupVisibility", async (_event, id) => {
|
||||||
@ -538,6 +696,7 @@ ipcMain.handle("status:get", () => {
|
|||||||
const dailyRemaining = Math.max(0, Number(settings.dailyLimit || 0) - dailyUsed);
|
const dailyRemaining = Math.max(0, Number(settings.dailyLimit || 0) - dailyUsed);
|
||||||
const queueCount = store.getPendingCount();
|
const queueCount = store.getPendingCount();
|
||||||
const accounts = store.listAccounts();
|
const accounts = store.listAccounts();
|
||||||
|
const connectedSessions = telegram.getConnectedCount();
|
||||||
const accountStats = accounts.map((account) => {
|
const accountStats = accounts.map((account) => {
|
||||||
const used = store.countInvitesTodayByAccount(account.id);
|
const used = store.countInvitesTodayByAccount(account.id);
|
||||||
const limit = Number(account.daily_limit || settings.accountDailyLimit || 0);
|
const limit = Number(account.daily_limit || settings.accountDailyLimit || 0);
|
||||||
@ -556,6 +715,8 @@ ipcMain.handle("status:get", () => {
|
|||||||
dailyRemaining,
|
dailyRemaining,
|
||||||
dailyUsed,
|
dailyUsed,
|
||||||
dailyLimit: Number(settings.dailyLimit || 0),
|
dailyLimit: Number(settings.dailyLimit || 0),
|
||||||
|
connectedSessions,
|
||||||
|
totalAccounts: accounts.length,
|
||||||
accountStats,
|
accountStats,
|
||||||
monitorInfo
|
monitorInfo
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,6 +13,7 @@ const DEFAULT_SETTINGS = {
|
|||||||
accountMaxGroups: 10,
|
accountMaxGroups: 10,
|
||||||
accountDailyLimit: 50,
|
accountDailyLimit: 50,
|
||||||
floodCooldownMinutes: 1440,
|
floodCooldownMinutes: 1440,
|
||||||
|
queueTtlHours: 24,
|
||||||
autoJoinCompetitors: false,
|
autoJoinCompetitors: false,
|
||||||
autoJoinOurGroup: false
|
autoJoinOurGroup: false
|
||||||
};
|
};
|
||||||
@ -125,6 +126,8 @@ function initStore(userDataPath) {
|
|||||||
stop_blocked_percent INTEGER NOT NULL DEFAULT 25,
|
stop_blocked_percent INTEGER NOT NULL DEFAULT 25,
|
||||||
notes TEXT NOT NULL DEFAULT '',
|
notes TEXT NOT NULL DEFAULT '',
|
||||||
enabled INTEGER NOT NULL DEFAULT 1,
|
enabled INTEGER NOT NULL DEFAULT 1,
|
||||||
|
task_invite_access TEXT NOT NULL DEFAULT '',
|
||||||
|
task_invite_access_at TEXT NOT NULL DEFAULT '',
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT NOT NULL
|
updated_at TEXT NOT NULL
|
||||||
);
|
);
|
||||||
@ -183,6 +186,8 @@ function initStore(userDataPath) {
|
|||||||
ensureColumn("tasks", "auto_join_our_group", "INTEGER NOT NULL DEFAULT 1");
|
ensureColumn("tasks", "auto_join_our_group", "INTEGER NOT NULL DEFAULT 1");
|
||||||
ensureColumn("tasks", "separate_bot_roles", "INTEGER NOT NULL DEFAULT 0");
|
ensureColumn("tasks", "separate_bot_roles", "INTEGER NOT NULL DEFAULT 0");
|
||||||
ensureColumn("tasks", "require_same_bot_in_both", "INTEGER NOT NULL DEFAULT 0");
|
ensureColumn("tasks", "require_same_bot_in_both", "INTEGER NOT NULL DEFAULT 0");
|
||||||
|
ensureColumn("tasks", "task_invite_access", "TEXT NOT NULL DEFAULT ''");
|
||||||
|
ensureColumn("tasks", "task_invite_access_at", "TEXT NOT NULL DEFAULT ''");
|
||||||
ensureColumn("task_accounts", "role_monitor", "INTEGER NOT NULL DEFAULT 1");
|
ensureColumn("task_accounts", "role_monitor", "INTEGER NOT NULL DEFAULT 1");
|
||||||
ensureColumn("task_accounts", "role_invite", "INTEGER NOT NULL DEFAULT 1");
|
ensureColumn("task_accounts", "role_invite", "INTEGER NOT NULL DEFAULT 1");
|
||||||
|
|
||||||
@ -399,6 +404,18 @@ function initStore(userDataPath) {
|
|||||||
db.prepare("DELETE FROM invite_queue WHERE task_id = ?").run(taskId || 0);
|
db.prepare("DELETE FROM invite_queue WHERE task_id = ?").run(taskId || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearQueueOlderThan(taskId, hours) {
|
||||||
|
const limit = Number(hours || 0);
|
||||||
|
if (!Number.isFinite(limit) || limit <= 0) return 0;
|
||||||
|
const cutoff = dayjs().subtract(limit, "hour").toISOString();
|
||||||
|
if (taskId == null) {
|
||||||
|
const result = db.prepare("DELETE FROM invite_queue WHERE created_at < ?").run(cutoff);
|
||||||
|
return result.changes || 0;
|
||||||
|
}
|
||||||
|
const result = db.prepare("DELETE FROM invite_queue WHERE task_id = ? AND created_at < ?").run(taskId || 0, cutoff);
|
||||||
|
return result.changes || 0;
|
||||||
|
}
|
||||||
|
|
||||||
function listTasks() {
|
function listTasks() {
|
||||||
return db.prepare("SELECT * FROM tasks ORDER BY id DESC").all();
|
return db.prepare("SELECT * FROM tasks ORDER BY id DESC").all();
|
||||||
}
|
}
|
||||||
@ -474,6 +491,13 @@ function initStore(userDataPath) {
|
|||||||
return result.lastInsertRowid;
|
return result.lastInsertRowid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setTaskInviteAccess(taskId, payload) {
|
||||||
|
const now = dayjs().toISOString();
|
||||||
|
const value = payload ? JSON.stringify(payload) : "";
|
||||||
|
db.prepare("UPDATE tasks SET task_invite_access = ?, task_invite_access_at = ?, updated_at = ? WHERE id = ?")
|
||||||
|
.run(value, value ? now : "", now, taskId);
|
||||||
|
}
|
||||||
|
|
||||||
function deleteTask(id) {
|
function deleteTask(id) {
|
||||||
db.prepare("DELETE FROM tasks WHERE id = ?").run(id);
|
db.prepare("DELETE FROM tasks WHERE id = ?").run(id);
|
||||||
db.prepare("DELETE FROM task_competitors WHERE task_id = ?").run(id);
|
db.prepare("DELETE FROM task_competitors WHERE task_id = ?").run(id);
|
||||||
@ -721,6 +745,7 @@ function initStore(userDataPath) {
|
|||||||
listTasks,
|
listTasks,
|
||||||
getTask,
|
getTask,
|
||||||
saveTask,
|
saveTask,
|
||||||
|
setTaskInviteAccess,
|
||||||
deleteTask,
|
deleteTask,
|
||||||
listTaskCompetitors,
|
listTaskCompetitors,
|
||||||
setTaskCompetitors,
|
setTaskCompetitors,
|
||||||
@ -746,6 +771,7 @@ function initStore(userDataPath) {
|
|||||||
getPendingCount,
|
getPendingCount,
|
||||||
getPendingStats,
|
getPendingStats,
|
||||||
clearQueue,
|
clearQueue,
|
||||||
|
clearQueueOlderThan,
|
||||||
markInviteStatus,
|
markInviteStatus,
|
||||||
incrementInviteAttempt,
|
incrementInviteAttempt,
|
||||||
recordInvite,
|
recordInvite,
|
||||||
|
|||||||
@ -75,6 +75,11 @@ class TaskRunner {
|
|||||||
this.nextInviteAccountId = 0;
|
this.nextInviteAccountId = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const settings = this.store.getSettings();
|
||||||
|
const ttlHours = Number(settings.queueTtlHours || 0);
|
||||||
|
if (ttlHours > 0) {
|
||||||
|
this.store.clearQueueOlderThan(this.task.id, ttlHours);
|
||||||
|
}
|
||||||
const accounts = this.store.listTaskAccounts(this.task.id).map((row) => row.account_id);
|
const accounts = this.store.listTaskAccounts(this.task.id).map((row) => row.account_id);
|
||||||
let inviteAccounts = accounts;
|
let inviteAccounts = accounts;
|
||||||
const roles = this.telegram.getTaskRoleAssignments(this.task.id);
|
const roles = this.telegram.getTaskRoleAssignments(this.task.id);
|
||||||
|
|||||||
@ -2085,6 +2085,10 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getConnectedCount() {
|
||||||
|
return this.clients.size;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { TelegramManager };
|
module.exports = { TelegramManager };
|
||||||
|
|||||||
@ -14,7 +14,8 @@ const emptySettings = {
|
|||||||
historyLimit: 200,
|
historyLimit: 200,
|
||||||
accountMaxGroups: 10,
|
accountMaxGroups: 10,
|
||||||
accountDailyLimit: 50,
|
accountDailyLimit: 50,
|
||||||
floodCooldownMinutes: 1440
|
floodCooldownMinutes: 1440,
|
||||||
|
queueTtlHours: 24
|
||||||
};
|
};
|
||||||
|
|
||||||
const emptyTaskForm = {
|
const emptyTaskForm = {
|
||||||
@ -89,6 +90,7 @@ export default function App() {
|
|||||||
const [accounts, setAccounts] = useState([]);
|
const [accounts, setAccounts] = useState([]);
|
||||||
const [accountStats, setAccountStats] = useState([]);
|
const [accountStats, setAccountStats] = useState([]);
|
||||||
const [accountAssignments, setAccountAssignments] = useState([]);
|
const [accountAssignments, setAccountAssignments] = useState([]);
|
||||||
|
const [globalStatus, setGlobalStatus] = useState({ connectedSessions: 0, totalAccounts: 0 });
|
||||||
const [logs, setLogs] = useState([]);
|
const [logs, setLogs] = useState([]);
|
||||||
const [invites, setInvites] = useState([]);
|
const [invites, setInvites] = useState([]);
|
||||||
const [tasks, setTasks] = useState([]);
|
const [tasks, setTasks] = useState([]);
|
||||||
@ -129,6 +131,7 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
const [tdataResult, setTdataResult] = useState(null);
|
const [tdataResult, setTdataResult] = useState(null);
|
||||||
const [tdataLoading, setTdataLoading] = useState(false);
|
const [tdataLoading, setTdataLoading] = useState(false);
|
||||||
|
const [taskActionLoading, setTaskActionLoading] = useState(false);
|
||||||
const [loginId, setLoginId] = useState("");
|
const [loginId, setLoginId] = useState("");
|
||||||
const [loginStatus, setLoginStatus] = useState("");
|
const [loginStatus, setLoginStatus] = useState("");
|
||||||
const [taskNotice, setTaskNotice] = useState(null);
|
const [taskNotice, setTaskNotice] = useState(null);
|
||||||
@ -203,6 +206,17 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
return { monitor, invite };
|
return { monitor, invite };
|
||||||
}, [taskAccountRoles]);
|
}, [taskAccountRoles]);
|
||||||
|
const roleIntersectionCount = useMemo(() => {
|
||||||
|
let count = 0;
|
||||||
|
Object.values(taskAccountRoles).forEach((roles) => {
|
||||||
|
if (roles.monitor && roles.invite) count += 1;
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
}, [taskAccountRoles]);
|
||||||
|
const assignedAccountCount = useMemo(() => {
|
||||||
|
const ids = new Set([...roleSummary.monitor, ...roleSummary.invite]);
|
||||||
|
return ids.size;
|
||||||
|
}, [roleSummary]);
|
||||||
const assignedAccountMap = useMemo(() => {
|
const assignedAccountMap = useMemo(() => {
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
accountAssignments.forEach((row) => {
|
accountAssignments.forEach((row) => {
|
||||||
@ -334,6 +348,10 @@ export default function App() {
|
|||||||
setAccounts(accountsData);
|
setAccounts(accountsData);
|
||||||
setAccountEvents(eventsData);
|
setAccountEvents(eventsData);
|
||||||
setAccountStats(statusData.accountStats || []);
|
setAccountStats(statusData.accountStats || []);
|
||||||
|
setGlobalStatus({
|
||||||
|
connectedSessions: statusData.connectedSessions || 0,
|
||||||
|
totalAccounts: statusData.totalAccounts || 0
|
||||||
|
});
|
||||||
const tasksData = await loadTasks();
|
const tasksData = await loadTasks();
|
||||||
await loadAccountAssignments();
|
await loadAccountAssignments();
|
||||||
await loadTaskStatuses(tasksData);
|
await loadTaskStatuses(tasksData);
|
||||||
@ -349,6 +367,10 @@ export default function App() {
|
|||||||
setInviteAccessStatus([]);
|
setInviteAccessStatus([]);
|
||||||
setMembershipStatus({});
|
setMembershipStatus({});
|
||||||
setTaskNotice(null);
|
setTaskNotice(null);
|
||||||
|
if (selectedTaskId != null) {
|
||||||
|
checkAccess("auto", true);
|
||||||
|
checkInviteAccess("auto", true);
|
||||||
|
}
|
||||||
}, [selectedTaskId]);
|
}, [selectedTaskId]);
|
||||||
|
|
||||||
const taskSummary = useMemo(() => {
|
const taskSummary = useMemo(() => {
|
||||||
@ -447,6 +469,10 @@ export default function App() {
|
|||||||
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 || []);
|
||||||
|
setGlobalStatus({
|
||||||
|
connectedSessions: statusData.connectedSessions || 0,
|
||||||
|
totalAccounts: statusData.totalAccounts || 0
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
accountsPollInFlight.current = false;
|
accountsPollInFlight.current = false;
|
||||||
}
|
}
|
||||||
@ -516,6 +542,39 @@ export default function App() {
|
|||||||
}));
|
}));
|
||||||
}, [selectedTaskId, taskStatus]);
|
}, [selectedTaskId, taskStatus]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasSelectedTask) return;
|
||||||
|
if (taskForm.requireSameBotInBoth) {
|
||||||
|
const nextValue = Math.max(1, roleIntersectionCount || 0);
|
||||||
|
if (taskForm.maxCompetitorBots !== nextValue || taskForm.maxOurBots !== nextValue) {
|
||||||
|
setTaskForm((prev) => sanitizeTaskForm({
|
||||||
|
...prev,
|
||||||
|
maxCompetitorBots: nextValue,
|
||||||
|
maxOurBots: nextValue
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (taskForm.separateBotRoles) {
|
||||||
|
const nextCompetitors = Math.max(1, roleSummary.monitor.length || 0);
|
||||||
|
const nextOur = Math.max(1, roleSummary.invite.length || 0);
|
||||||
|
if (taskForm.maxCompetitorBots !== nextCompetitors || taskForm.maxOurBots !== nextOur) {
|
||||||
|
setTaskForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
maxCompetitorBots: nextCompetitors,
|
||||||
|
maxOurBots: nextOur
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
hasSelectedTask,
|
||||||
|
roleIntersectionCount,
|
||||||
|
roleSummary.monitor.length,
|
||||||
|
roleSummary.invite.length,
|
||||||
|
taskForm.requireSameBotInBoth,
|
||||||
|
taskForm.separateBotRoles
|
||||||
|
]);
|
||||||
|
|
||||||
const formatAccountStatus = (status) => {
|
const formatAccountStatus = (status) => {
|
||||||
if (status === "limited") return "В спаме";
|
if (status === "limited") return "В спаме";
|
||||||
if (status === "error") return "Ошибка";
|
if (status === "error") return "Ошибка";
|
||||||
@ -830,6 +889,23 @@ export default function App() {
|
|||||||
showNotification("Сохраняем задачу...", "info");
|
showNotification("Сохраняем задачу...", "info");
|
||||||
const nextForm = sanitizeTaskForm(taskForm);
|
const nextForm = sanitizeTaskForm(taskForm);
|
||||||
setTaskForm(nextForm);
|
setTaskForm(nextForm);
|
||||||
|
const validateLink = (value) => {
|
||||||
|
const trimmed = String(value || "").trim();
|
||||||
|
if (!trimmed) return false;
|
||||||
|
if (trimmed.startsWith("@")) return true;
|
||||||
|
if (trimmed.startsWith("https://t.me/")) return true;
|
||||||
|
if (trimmed.startsWith("http://t.me/")) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
const invalidCompetitors = competitorGroups.filter((link) => !validateLink(link));
|
||||||
|
if (!validateLink(nextForm.ourGroup)) {
|
||||||
|
showNotification("Наша группа должна быть ссылкой t.me или @username.", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (invalidCompetitors.length) {
|
||||||
|
showNotification(`Некорректные ссылки конкурентов: ${invalidCompetitors.join(", ")}`, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
let accountRolesMap = { ...taskAccountRoles };
|
let accountRolesMap = { ...taskAccountRoles };
|
||||||
let accountIds = Object.keys(accountRolesMap).map((id) => Number(id));
|
let accountIds = Object.keys(accountRolesMap).map((id) => Number(id));
|
||||||
if (nextForm.requireSameBotInBoth) {
|
if (nextForm.requireSameBotInBoth) {
|
||||||
@ -931,11 +1007,16 @@ export default function App() {
|
|||||||
showNotification("Сначала выберите задачу.", "error");
|
showNotification("Сначала выберите задачу.", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (taskActionLoading) return;
|
||||||
|
setTaskActionLoading(true);
|
||||||
showNotification("Запуск...", "info");
|
showNotification("Запуск...", "info");
|
||||||
try {
|
try {
|
||||||
const result = await window.api.startTaskById(selectedTaskId);
|
const result = await window.api.startTaskById(selectedTaskId);
|
||||||
if (result && result.ok) {
|
if (result && result.ok) {
|
||||||
setTaskNotice({ text: "Запущено.", tone: "success", source });
|
setTaskNotice({ text: "Запущено.", tone: "success", source });
|
||||||
|
if (result.warnings && result.warnings.length) {
|
||||||
|
showNotification(`Предупреждения: ${result.warnings.join(" | ")}`, "info");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
showNotification(result.error || "Не удалось запустить", "error");
|
showNotification(result.error || "Не удалось запустить", "error");
|
||||||
}
|
}
|
||||||
@ -943,6 +1024,8 @@ export default function App() {
|
|||||||
const message = error.message || String(error);
|
const message = error.message || String(error);
|
||||||
setTaskNotice({ text: message, tone: "error", source });
|
setTaskNotice({ text: message, tone: "error", source });
|
||||||
showNotification(message, "error");
|
showNotification(message, "error");
|
||||||
|
} finally {
|
||||||
|
setTaskActionLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -974,9 +1057,11 @@ export default function App() {
|
|||||||
showNotification("Сначала выберите задачу.", "error");
|
showNotification("Сначала выберите задачу.", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (taskActionLoading) return;
|
||||||
if (!window.confirm(`Остановить задачу: ${selectedTaskName}?`)) {
|
if (!window.confirm(`Остановить задачу: ${selectedTaskName}?`)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setTaskActionLoading(true);
|
||||||
showNotification("Остановка...", "info");
|
showNotification("Остановка...", "info");
|
||||||
try {
|
try {
|
||||||
await window.api.stopTaskById(selectedTaskId);
|
await window.api.stopTaskById(selectedTaskId);
|
||||||
@ -985,6 +1070,8 @@ export default function App() {
|
|||||||
const message = error.message || String(error);
|
const message = error.message || String(error);
|
||||||
setTaskNotice({ text: message, tone: "error", source });
|
setTaskNotice({ text: message, tone: "error", source });
|
||||||
showNotification(message, "error");
|
showNotification(message, "error");
|
||||||
|
} finally {
|
||||||
|
setTaskActionLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1060,42 +1147,42 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkAccess = async (source = "editor") => {
|
const checkAccess = async (source = "editor", silent = false) => {
|
||||||
if (!window.api || selectedTaskId == null) {
|
if (!window.api || selectedTaskId == null) {
|
||||||
showNotification("Сначала выберите задачу.", "error");
|
if (!silent) showNotification("Сначала выберите задачу.", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showNotification("Проверяем доступ к группам...", "info");
|
if (!silent) showNotification("Проверяем доступ к группам...", "info");
|
||||||
try {
|
try {
|
||||||
const result = await window.api.checkAccessByTask(selectedTaskId);
|
const result = await window.api.checkAccessByTask(selectedTaskId);
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
showNotification(result.error || "Не удалось проверить доступ", "error");
|
if (!silent) showNotification(result.error || "Не удалось проверить доступ", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setAccessStatus(result.result || []);
|
setAccessStatus(result.result || []);
|
||||||
setTaskNotice({ text: "Проверка доступа завершена.", tone: "success", source });
|
if (!silent) setTaskNotice({ text: "Проверка доступа завершена.", tone: "success", source });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showNotification(error.message || String(error), "error");
|
if (!silent) showNotification(error.message || String(error), "error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkInviteAccess = async (source = "editor") => {
|
const checkInviteAccess = async (source = "editor", silent = false) => {
|
||||||
if (!window.api || selectedTaskId == null) {
|
if (!window.api || selectedTaskId == null) {
|
||||||
showNotification("Сначала выберите задачу.", "error");
|
if (!silent) showNotification("Сначала выберите задачу.", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setInviteAccessStatus([]);
|
setInviteAccessStatus([]);
|
||||||
showNotification("Проверяем права инвайта...", "info");
|
if (!silent) showNotification("Проверяем права инвайта...", "info");
|
||||||
try {
|
try {
|
||||||
const result = await window.api.checkInviteAccessByTask(selectedTaskId);
|
const result = await window.api.checkInviteAccessByTask(selectedTaskId);
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
showNotification(result.error || "Не удалось проверить права", "error");
|
if (!silent) showNotification(result.error || "Не удалось проверить права", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setInviteAccessStatus(result.result || []);
|
setInviteAccessStatus(result.result || []);
|
||||||
setTaskNotice({ text: "Проверка прав инвайта завершена.", tone: "success", source });
|
if (!silent) setTaskNotice({ text: "Проверка прав инвайта завершена.", tone: "success", source });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showNotification(error.message || String(error), "error");
|
if (!silent) showNotification(error.message || String(error), "error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1728,6 +1815,12 @@ export default function App() {
|
|||||||
<div className="live-label">Запущено</div>
|
<div className="live-label">Запущено</div>
|
||||||
<div className="summary-value">{taskSummary.running}</div>
|
<div className="summary-value">{taskSummary.running}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="summary-card">
|
||||||
|
<div className="live-label">Сессии</div>
|
||||||
|
<div className="summary-value">
|
||||||
|
{globalStatus.connectedSessions}/{globalStatus.totalAccounts}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="summary-card">
|
<div className="summary-card">
|
||||||
<div className="live-label">Очередь</div>
|
<div className="live-label">Очередь</div>
|
||||||
<div className="summary-value">{taskSummary.queue}</div>
|
<div className="summary-value">{taskSummary.queue}</div>
|
||||||
@ -1974,13 +2067,37 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="status-actions">
|
<div className="status-actions">
|
||||||
{taskStatus.running ? (
|
{taskStatus.running ? (
|
||||||
<button className="danger" onClick={() => stopTask("status")} disabled={!hasSelectedTask}>Остановить</button>
|
<button className="danger" onClick={() => stopTask("status")} disabled={!hasSelectedTask || taskActionLoading}>
|
||||||
|
{taskActionLoading ? "Остановка..." : "Остановить"}
|
||||||
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button className="primary" onClick={() => startTask("status")} disabled={!hasSelectedTask}>Запустить</button>
|
<button className="primary" onClick={() => startTask("status")} disabled={!hasSelectedTask || taskActionLoading}>
|
||||||
|
{taskActionLoading ? "Запуск..." : "Запустить"}
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{hasSelectedTask && (
|
{hasSelectedTask && (
|
||||||
<div className="status-text">
|
<div className="status-text">
|
||||||
|
{taskStatus.readiness && (
|
||||||
|
<div className={`notice inline ${taskStatus.readiness.ok ? "success" : "warn"}`}>
|
||||||
|
Готовность: {taskStatus.readiness.ok ? "Да" : "Нет"}
|
||||||
|
{!taskStatus.readiness.ok && taskStatus.readiness.reasons && (
|
||||||
|
<div className="pre-line">{taskStatus.readiness.reasons.join("\n")}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{roleSummary.invite.length > 0 && (
|
||||||
|
<div className="status-text compact">
|
||||||
|
Лимиты аккаунтов: {roleSummary.invite.map((id) => {
|
||||||
|
const account = accountById.get(id);
|
||||||
|
const stats = accountStatsMap.get(id);
|
||||||
|
if (!account || !stats) return null;
|
||||||
|
const label = account.phone || account.user_id || id;
|
||||||
|
const remaining = stats.remainingToday != null ? stats.remainingToday : "—";
|
||||||
|
return `${label} (${remaining})`;
|
||||||
|
}).filter(Boolean).join(", ")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{taskStatus.warnings && taskStatus.warnings.length > 0 && (
|
{taskStatus.warnings && taskStatus.warnings.length > 0 && (
|
||||||
<div className="notice inline warn">
|
<div className="notice inline warn">
|
||||||
{taskStatus.warnings.map((warning, index) => (
|
{taskStatus.warnings.map((warning, index) => (
|
||||||
@ -2369,7 +2486,7 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="side-stat">
|
<div className="side-stat">
|
||||||
<span>Аккаунты</span>
|
<span>Аккаунты</span>
|
||||||
<strong>{selectedAccountIds.length}</strong>
|
<strong>{assignedAccountCount}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="side-stat">
|
<div className="side-stat">
|
||||||
<span>Очередь</span>
|
<span>Очередь</span>
|
||||||
@ -2391,15 +2508,24 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
{sidebarExpanded && (
|
{sidebarExpanded && (
|
||||||
<div className="sidebar-actions expanded">
|
<div className="sidebar-actions expanded">
|
||||||
<button className="secondary" onClick={() => saveTask("sidebar")} disabled={!canSaveTask}>Сохранить задачу</button>
|
<button className="secondary" onClick={() => saveTask("sidebar")} disabled={!canSaveTask}>Сохранить задачу</button>
|
||||||
<button className="secondary" onClick={() => parseHistory("sidebar")} disabled={!hasSelectedTask}>Собрать историю</button>
|
<button className="secondary" onClick={() => parseHistory("sidebar")} disabled={!hasSelectedTask}>Собрать историю</button>
|
||||||
<button className="secondary" onClick={() => checkAccess("sidebar")} disabled={!hasSelectedTask}>Проверить доступ</button>
|
<button className="secondary" onClick={() => checkAccess("sidebar")} disabled={!hasSelectedTask}>Проверить доступ</button>
|
||||||
<button className="secondary" onClick={() => checkInviteAccess("sidebar")} disabled={!hasSelectedTask}>Проверить права инвайта</button>
|
<button className="secondary" onClick={() => checkInviteAccess("sidebar")} disabled={!hasSelectedTask}>Проверить права инвайта</button>
|
||||||
<button className="secondary" onClick={() => refreshMembership("sidebar")} disabled={!hasSelectedTask}>Проверить участие</button>
|
<button className="secondary" onClick={() => refreshMembership("sidebar")} disabled={!hasSelectedTask}>Проверить участие</button>
|
||||||
<button className="secondary" onClick={() => clearQueue("sidebar")} disabled={!hasSelectedTask}>Очистить очередь</button>
|
<button className="secondary" onClick={() => clearQueue("sidebar")} disabled={!hasSelectedTask}>Очистить очередь</button>
|
||||||
{accessStatus.length > 0 && (
|
{taskStatus.running ? (
|
||||||
<div className="access-block">
|
<button className="danger" onClick={() => stopTask("sidebar")} disabled={!hasSelectedTask || taskActionLoading}>
|
||||||
<div className="access-title">Доступ к группам</div>
|
{taskActionLoading ? "Остановка..." : "Остановить"}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button className="primary" onClick={() => startTask("sidebar")} disabled={!hasSelectedTask || taskActionLoading}>
|
||||||
|
{taskActionLoading ? "Запуск..." : "Запустить"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{accessStatus.length > 0 && (
|
||||||
|
<div className="access-block">
|
||||||
|
<div className="access-title">Доступ к группам</div>
|
||||||
<div className="access-list">
|
<div className="access-list">
|
||||||
{accessStatus.map((item, index) => (
|
{accessStatus.map((item, index) => (
|
||||||
<div key={`${item.value}-${index}`} className={`access-row ${item.ok ? "ok" : "fail"}`}>
|
<div key={`${item.value}-${index}`} className={`access-row ${item.ok ? "ok" : "fail"}`}>
|
||||||
@ -2418,6 +2544,9 @@ export default function App() {
|
|||||||
{inviteAccessStatus.length > 0 && (
|
{inviteAccessStatus.length > 0 && (
|
||||||
<div className="access-block">
|
<div className="access-block">
|
||||||
<div className="access-title">Права инвайта</div>
|
<div className="access-title">Права инвайта</div>
|
||||||
|
<div className="access-subtitle">
|
||||||
|
Проверяются аккаунты с ролью инвайта: {roleSummary.invite.length}
|
||||||
|
</div>
|
||||||
<div className="access-list">
|
<div className="access-list">
|
||||||
{inviteAccessStatus.map((item, index) => (
|
{inviteAccessStatus.map((item, index) => (
|
||||||
<div key={`${item.accountId}-${index}`} className={`access-row ${item.canInvite ? "ok" : "fail"}`}>
|
<div key={`${item.accountId}-${index}`} className={`access-row ${item.canInvite ? "ok" : "fail"}`}>
|
||||||
|
|||||||
@ -188,6 +188,12 @@ body {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.access-subtitle {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #64748b;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.access-status {
|
.access-status {
|
||||||
color: #334155;
|
color: #334155;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,6 +32,15 @@ function SettingsTab({ settings, onSettingsChange, settingsNotice, saveSettings
|
|||||||
onChange={(event) => onSettingsChange("floodCooldownMinutes", Number(event.target.value))}
|
onChange={(event) => onSettingsChange("floodCooldownMinutes", Number(event.target.value))}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
<span className="label-line">Хранить очередь (часы)</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={settings.queueTtlHours}
|
||||||
|
onChange={(event) => onSettingsChange("queueTtlHours", Number(event.target.value))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="row-inline">
|
<div className="row-inline">
|
||||||
<button className="secondary" onClick={saveSettings}>Сохранить настройки</button>
|
<button className="secondary" onClick={saveSettings}>Сохранить настройки</button>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user