some
This commit is contained in:
parent
3437cea13c
commit
d34e0a0b42
@ -57,6 +57,7 @@ const startTaskWithChecks = async (id) => {
|
||||
const existingAccounts = store.listAccounts();
|
||||
const filteredResult = filterTaskRolesByAccounts(id, taskAccounts, existingAccounts);
|
||||
const filteredRoles = filteredResult.filtered;
|
||||
let adminPrepPartialWarning = "";
|
||||
const inviteIds = filteredRoles
|
||||
.filter((row) => row.roleInvite && Number(row.inviteLimit || 0) > 0)
|
||||
.map((row) => row.accountId);
|
||||
@ -115,6 +116,12 @@ const startTaskWithChecks = async (id) => {
|
||||
if (adminPrep && !adminPrep.ok) {
|
||||
return { ok: false, error: adminPrep.error || "Не удалось подготовить права админов." };
|
||||
}
|
||||
if (adminPrep && Array.isArray(adminPrep.result)) {
|
||||
const failed = adminPrep.result.filter((item) => !item.ok);
|
||||
if (failed.length) {
|
||||
adminPrepPartialWarning = `Часть прав админа не выдана: ${failed.length} аккаунт(ов).`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let runner = taskRunners.get(id);
|
||||
@ -149,6 +156,9 @@ const startTaskWithChecks = async (id) => {
|
||||
}
|
||||
if (task.invite_via_admins) {
|
||||
warnings.push("Режим инвайта через админов включен.");
|
||||
if (adminPrepPartialWarning) {
|
||||
warnings.push(adminPrepPartialWarning);
|
||||
}
|
||||
}
|
||||
if (filteredResult.removedError) {
|
||||
warnings.push(`Отключены аккаунты с ошибкой: ${filteredResult.removedError}.`);
|
||||
@ -1024,7 +1034,11 @@ ipcMain.handle("tasks:checkInviteAccess", async (_event, id) => {
|
||||
const accountIds = accountRows
|
||||
.filter((row) => existingIds.has(row.account_id))
|
||||
.map((row) => row.account_id);
|
||||
const result = await telegram.checkInvitePermissions(task, accountIds);
|
||||
if (task.invite_via_admins && task.invite_admin_master_id && existingIds.has(Number(task.invite_admin_master_id))) {
|
||||
accountIds.push(Number(task.invite_admin_master_id));
|
||||
}
|
||||
const dedupedAccountIds = Array.from(new Set(accountIds));
|
||||
const result = await telegram.checkInvitePermissions(task, dedupedAccountIds);
|
||||
if (result && result.ok) {
|
||||
store.setTaskInviteAccess(id, result.result || []);
|
||||
}
|
||||
@ -1054,6 +1068,15 @@ const toCsv = (rows, headers) => {
|
||||
return lines.join("\n");
|
||||
};
|
||||
|
||||
const sanitizeFileName = (value) => {
|
||||
return String(value || "")
|
||||
.replace(/[<>:"/\\|?*\x00-\x1F]/g, "_")
|
||||
.replace(/\s+/g, "_")
|
||||
.replace(/_+/g, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.slice(0, 80) || "task";
|
||||
};
|
||||
|
||||
const explainInviteError = (error) => {
|
||||
if (!error) return "";
|
||||
if (error === "USER_ID_INVALID") {
|
||||
@ -1164,6 +1187,71 @@ ipcMain.handle("logs:export", async (_event, taskId) => {
|
||||
return { ok: true, filePath };
|
||||
});
|
||||
|
||||
ipcMain.handle("tasks:exportBundle", async (_event, taskId) => {
|
||||
const id = Number(taskId || 0);
|
||||
if (!id) return { ok: false, error: "Task not found" };
|
||||
const task = store.getTask(id);
|
||||
if (!task) return { ok: false, error: "Task not found" };
|
||||
|
||||
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const taskLabel = sanitizeFileName(task.name || `task-${id}`);
|
||||
const { canceled, filePath } = await dialog.showSaveDialog({
|
||||
title: "Выгрузить логи задачи",
|
||||
defaultPath: `${taskLabel}_${id}_${stamp}.json`
|
||||
});
|
||||
if (canceled || !filePath) return { ok: false, canceled: true };
|
||||
|
||||
const competitors = store.listTaskCompetitors(id);
|
||||
const taskAccounts = store.listTaskAccounts(id);
|
||||
const taskAccountIds = new Set(taskAccounts.map((row) => Number(row.account_id)));
|
||||
const accounts = store.listAccounts().filter((account) => taskAccountIds.has(Number(account.id)));
|
||||
|
||||
const logs = store.listLogs(10000, id);
|
||||
const invites = store.listInvites(50000, id);
|
||||
const queue = store.getPendingInvites(id, 10000, 0);
|
||||
const fallback = store.listFallback(10000, id);
|
||||
const confirmQueue = store.listConfirmQueue(id, 10000);
|
||||
const taskAudit = store.listTaskAudit(id, 10000);
|
||||
const allAccountEvents = store.listAccountEvents(20000);
|
||||
const taskHints = [`задача ${id}`, `задача:${id}`, `task ${id}`, `task:${id}`, `id: ${id}`];
|
||||
const accountEvents = allAccountEvents.filter((item) => {
|
||||
if (taskAccountIds.has(Number(item.accountId))) return true;
|
||||
const message = String(item.message || "").toLowerCase();
|
||||
return taskHints.some((hint) => message.includes(hint));
|
||||
});
|
||||
|
||||
const exportPayload = {
|
||||
exportedAt: new Date().toISOString(),
|
||||
formatVersion: 1,
|
||||
task,
|
||||
competitors,
|
||||
taskAccounts,
|
||||
accounts,
|
||||
logs,
|
||||
invites,
|
||||
queue,
|
||||
fallback,
|
||||
confirmQueue,
|
||||
taskAudit,
|
||||
accountEvents,
|
||||
counts: {
|
||||
competitors: competitors.length,
|
||||
taskAccounts: taskAccounts.length,
|
||||
accounts: accounts.length,
|
||||
logs: logs.length,
|
||||
invites: invites.length,
|
||||
queue: queue.length,
|
||||
fallback: fallback.length,
|
||||
confirmQueue: confirmQueue.length,
|
||||
taskAudit: taskAudit.length,
|
||||
accountEvents: accountEvents.length
|
||||
}
|
||||
};
|
||||
|
||||
fs.writeFileSync(filePath, JSON.stringify(exportPayload, null, 2), "utf8");
|
||||
return { ok: true, filePath, counts: exportPayload.counts };
|
||||
});
|
||||
|
||||
ipcMain.handle("invites:export", async (_event, taskId) => {
|
||||
const { canceled, filePath } = await dialog.showSaveDialog({
|
||||
title: "Выгрузить историю инвайтов",
|
||||
|
||||
@ -29,6 +29,7 @@ contextBridge.exposeInMainWorld("api", {
|
||||
clearLogs: (taskId) => ipcRenderer.invoke("logs:clear", taskId),
|
||||
clearInvites: (taskId) => ipcRenderer.invoke("invites:clear", taskId),
|
||||
exportLogs: (taskId) => ipcRenderer.invoke("logs:export", taskId),
|
||||
exportTaskBundle: (taskId) => ipcRenderer.invoke("tasks:exportBundle", taskId),
|
||||
exportInvites: (taskId) => ipcRenderer.invoke("invites:export", taskId),
|
||||
clearQueue: (taskId) => ipcRenderer.invoke("queue:clear", taskId),
|
||||
clearQueueItems: (payload) => ipcRenderer.invoke("queue:clearItems", payload),
|
||||
|
||||
@ -41,6 +41,42 @@ class TelegramManager {
|
||||
});
|
||||
}
|
||||
|
||||
_toInputUser(entity) {
|
||||
if (!entity) return null;
|
||||
try {
|
||||
if (entity.className === "InputUser") {
|
||||
return entity;
|
||||
}
|
||||
if (entity.className === "InputPeerUser" && entity.userId != null && entity.accessHash != null) {
|
||||
return new Api.InputUser({
|
||||
userId: BigInt(entity.userId),
|
||||
accessHash: BigInt(entity.accessHash)
|
||||
});
|
||||
}
|
||||
if (entity.className === "User" && entity.id != null && entity.accessHash != null) {
|
||||
return new Api.InputUser({
|
||||
userId: BigInt(entity.id),
|
||||
accessHash: BigInt(entity.accessHash)
|
||||
});
|
||||
}
|
||||
if (entity.userId != null && entity.accessHash != null) {
|
||||
return new Api.InputUser({
|
||||
userId: BigInt(entity.userId),
|
||||
accessHash: BigInt(entity.accessHash)
|
||||
});
|
||||
}
|
||||
if (entity.id != null && entity.accessHash != null) {
|
||||
return new Api.InputUser({
|
||||
userId: BigInt(entity.id),
|
||||
accessHash: BigInt(entity.accessHash)
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async _resolveAccountEntityForMaster(masterClient, targetEntity, account) {
|
||||
if (!masterClient || !targetEntity || !account) return null;
|
||||
const username = account.username ? String(account.username).trim() : "";
|
||||
@ -48,8 +84,13 @@ class TelegramManager {
|
||||
|
||||
if (username) {
|
||||
try {
|
||||
const byUsername = await masterClient.getEntity(username.startsWith("@") ? username : `@${username}`);
|
||||
if (byUsername && byUsername.className === "User") return byUsername;
|
||||
const normalized = username.startsWith("@") ? username : `@${username}`;
|
||||
const byUsername = await masterClient.getEntity(normalized);
|
||||
const inputByUsername = this._toInputUser(byUsername);
|
||||
if (inputByUsername) return inputByUsername;
|
||||
const cachedInput = await masterClient.getInputEntity(normalized);
|
||||
const inputCached = this._toInputUser(cachedInput);
|
||||
if (inputCached) return inputCached;
|
||||
} catch (error) {
|
||||
// continue fallback chain
|
||||
}
|
||||
@ -58,7 +99,8 @@ class TelegramManager {
|
||||
if (userId) {
|
||||
try {
|
||||
const byId = await masterClient.getEntity(BigInt(userId));
|
||||
if (byId && byId.className === "User") return byId;
|
||||
const inputById = this._toInputUser(byId);
|
||||
if (inputById) return inputById;
|
||||
} catch (error) {
|
||||
// continue fallback chain
|
||||
}
|
||||
@ -75,7 +117,8 @@ class TelegramManager {
|
||||
const sameUsername = username && item.username && item.username.toLowerCase() === username.toLowerCase();
|
||||
return Boolean(sameId || sameUsername);
|
||||
});
|
||||
if (found) return found;
|
||||
const inputFound = this._toInputUser(found);
|
||||
if (inputFound) return inputFound;
|
||||
} catch (error) {
|
||||
// no-op
|
||||
}
|
||||
@ -617,6 +660,7 @@ class TelegramManager {
|
||||
};
|
||||
const resolveUserForClient = async (targetClient, preferredUser = null) => {
|
||||
if (!targetClient) return null;
|
||||
const sameClient = targetClient === client;
|
||||
const providedUsername = options.username || "";
|
||||
const normalizedUsername = providedUsername
|
||||
? (providedUsername.startsWith("@") ? providedUsername : `@${providedUsername}`)
|
||||
@ -624,7 +668,8 @@ class TelegramManager {
|
||||
if (normalizedUsername) {
|
||||
try {
|
||||
const byUsername = await targetClient.getEntity(normalizedUsername);
|
||||
if (byUsername && byUsername.className === "User") return byUsername;
|
||||
const inputByUsername = this._toInputUser(byUsername);
|
||||
if (inputByUsername) return inputByUsername;
|
||||
} catch (error) {
|
||||
// continue fallback chain
|
||||
}
|
||||
@ -632,33 +677,15 @@ class TelegramManager {
|
||||
if (userId != null && userId !== "") {
|
||||
try {
|
||||
const byId = await targetClient.getEntity(BigInt(String(userId)));
|
||||
if (byId && byId.className === "User") return byId;
|
||||
const inputById = this._toInputUser(byId);
|
||||
if (inputById) return inputById;
|
||||
} catch (error) {
|
||||
// continue fallback chain
|
||||
}
|
||||
}
|
||||
if (preferredUser && preferredUser.className === "User") {
|
||||
return preferredUser;
|
||||
}
|
||||
if (preferredUser && preferredUser.userId != null && preferredUser.accessHash != null) {
|
||||
try {
|
||||
return new Api.InputUser({
|
||||
userId: BigInt(String(preferredUser.userId)),
|
||||
accessHash: BigInt(String(preferredUser.accessHash))
|
||||
});
|
||||
} catch (error) {
|
||||
// ignore malformed input user
|
||||
}
|
||||
}
|
||||
if (preferredUser && preferredUser.id != null && preferredUser.accessHash != null) {
|
||||
try {
|
||||
return new Api.InputUser({
|
||||
userId: BigInt(String(preferredUser.id)),
|
||||
accessHash: BigInt(String(preferredUser.accessHash))
|
||||
});
|
||||
} catch (error) {
|
||||
// ignore malformed user entity
|
||||
}
|
||||
if (sameClient && preferredUser) {
|
||||
const inputPreferred = this._toInputUser(preferredUser);
|
||||
if (inputPreferred) return inputPreferred;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@ -859,7 +886,7 @@ class TelegramManager {
|
||||
}
|
||||
return "Недостаточно прав для инвайта.";
|
||||
};
|
||||
const attemptAdminInvite = async (user, adminClient = client, adminEntry = entry, allowAnonymous = false) => {
|
||||
const attemptAdminInvite = async (user, adminClient = client, adminEntry = entry) => {
|
||||
const targetForAdmin = await getTargetEntityForClient(adminClient, adminEntry);
|
||||
if (!targetForAdmin.ok || !targetForAdmin.entity) {
|
||||
throw new Error(targetForAdmin.error || "Target group not resolved");
|
||||
@ -872,18 +899,9 @@ class TelegramManager {
|
||||
if (!userForAdminClient) {
|
||||
throw new Error("INVITED_USER_NOT_RESOLVED_FOR_ADMIN");
|
||||
}
|
||||
const rights = this._buildInviteAdminRights(allowAnonymous);
|
||||
await adminClient.invoke(new Api.channels.EditAdmin({
|
||||
await adminClient.invoke(new Api.channels.InviteToChannel({
|
||||
channel: adminTargetEntity,
|
||||
userId: userForAdminClient,
|
||||
adminRights: rights,
|
||||
rank: "invite"
|
||||
}));
|
||||
await adminClient.invoke(new Api.channels.EditAdmin({
|
||||
channel: adminTargetEntity,
|
||||
userId: userForAdminClient,
|
||||
adminRights: new Api.ChatAdminRights({}),
|
||||
rank: ""
|
||||
users: [userForAdminClient]
|
||||
}));
|
||||
};
|
||||
|
||||
@ -1084,7 +1102,7 @@ class TelegramManager {
|
||||
const masterId = Number(task.invite_admin_master_id || 0);
|
||||
const masterEntry = masterId ? this.clients.get(masterId) : null;
|
||||
const adminClient = masterEntry ? masterEntry.client : client;
|
||||
await attemptAdminInvite(user, adminClient, masterEntry || entry, Boolean(task.invite_admin_anonymous));
|
||||
await attemptAdminInvite(user, adminClient, masterEntry || entry);
|
||||
const confirm = await confirmMembershipWithFallback(user, entry);
|
||||
if (confirm.confirmed !== true && !confirm.detail) {
|
||||
const label = formatAccountSource("", entry) || "проверка этим аккаунтом";
|
||||
@ -1941,21 +1959,47 @@ class TelegramManager {
|
||||
const rights = this._buildInviteAdminRights(Boolean(task.invite_admin_anonymous));
|
||||
const accounts = this.store.listAccounts();
|
||||
const accountMap = new Map(accounts.map((acc) => [acc.id, acc]));
|
||||
const targetIds = Array.from(new Set((accountIds || []).filter((id) => Number(id) && Number(id) !== Number(masterAccountId))));
|
||||
const results = [];
|
||||
for (const accountId of accountIds) {
|
||||
if (accountId === masterAccountId) continue;
|
||||
for (const accountId of targetIds) {
|
||||
const record = accountMap.get(accountId);
|
||||
if (!record) {
|
||||
results.push({ accountId, ok: false, reason: "Аккаунт не найден" });
|
||||
continue;
|
||||
}
|
||||
if (!record.user_id && !record.username) {
|
||||
const targetEntry = this.clients.get(accountId);
|
||||
if (!targetEntry) {
|
||||
results.push({ accountId, ok: false, reason: "Сессия инвайтера не подключена" });
|
||||
continue;
|
||||
}
|
||||
const targetAccess = await this._resolveGroupEntity(
|
||||
targetEntry.client,
|
||||
task.our_group,
|
||||
Boolean(task.auto_join_our_group),
|
||||
targetEntry.account
|
||||
);
|
||||
if (!targetAccess.ok) {
|
||||
results.push({
|
||||
accountId,
|
||||
ok: false,
|
||||
reason: `Инвайтер не имеет доступа к целевой группе: ${targetAccess.error || "неизвестно"}`
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (!record.user_id && !record.username && !targetEntry.account.username && !targetEntry.account.user_id) {
|
||||
results.push({ accountId, ok: false, reason: "Нет user_id/username" });
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const identifier = record.user_id ? BigInt(record.user_id) : `@${record.username}`;
|
||||
const user = await client.getEntity(identifier);
|
||||
const user = await this._resolveAccountEntityForMaster(client, targetEntity, {
|
||||
...record,
|
||||
username: record.username || targetEntry.account.username || "",
|
||||
user_id: record.user_id || targetEntry.account.user_id || ""
|
||||
});
|
||||
if (!user) {
|
||||
results.push({ accountId, ok: false, reason: "Мастер-админ не смог резолвить аккаунт инвайтера" });
|
||||
continue;
|
||||
}
|
||||
await client.invoke(new Api.channels.EditAdmin({
|
||||
channel: targetEntity,
|
||||
userId: user,
|
||||
@ -1980,6 +2024,25 @@ class TelegramManager {
|
||||
results.push({ accountId, ok: false, reason: errorText });
|
||||
}
|
||||
}
|
||||
const okCount = results.filter((item) => item.ok).length;
|
||||
const failList = results.filter((item) => !item.ok);
|
||||
const failPreview = failList
|
||||
.slice(0, 3)
|
||||
.map((item) => `${item.accountId}: ${item.reason || "ошибка"}`)
|
||||
.join("; ");
|
||||
this.store.addAccountEvent(
|
||||
masterAccountId,
|
||||
masterAccount.phone || "",
|
||||
"admin_grant_summary",
|
||||
`Выдача прав: успешно ${okCount}/${results.length}${failList.length ? `; ошибки: ${failPreview}` : ""}`
|
||||
);
|
||||
if (results.length && okCount === 0) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `Мастер-админ не смог выдать права инвайтерам. ${failPreview || "Проверьте участие аккаунтов в целевой группе и права master-админа."}`,
|
||||
result: results
|
||||
};
|
||||
}
|
||||
return { ok: true, result: results };
|
||||
}
|
||||
|
||||
|
||||
@ -462,6 +462,7 @@ export default function App() {
|
||||
clearInvites,
|
||||
clearAccountEvents,
|
||||
exportLogs,
|
||||
exportTaskBundle,
|
||||
exportInvites,
|
||||
exportProblemInvites,
|
||||
exportFallback,
|
||||
@ -770,6 +771,7 @@ export default function App() {
|
||||
setActiveTab,
|
||||
tasksLength: tasks.length,
|
||||
runTestSafe: () => runTest("safe"),
|
||||
exportTaskBundle,
|
||||
nowLine,
|
||||
nowExpanded,
|
||||
setNowExpanded,
|
||||
@ -796,6 +798,7 @@ export default function App() {
|
||||
taskForm,
|
||||
setTaskForm,
|
||||
activePreset,
|
||||
setActivePreset,
|
||||
applyTaskPreset,
|
||||
formatAccountLabel,
|
||||
accountById,
|
||||
|
||||
@ -24,7 +24,8 @@ export default function QuickActionsBar({
|
||||
pauseReason,
|
||||
setActiveTab,
|
||||
tasksLength,
|
||||
runTestSafe
|
||||
runTestSafe,
|
||||
exportTaskBundle
|
||||
}) {
|
||||
return (
|
||||
<section className="card action-bar">
|
||||
@ -42,6 +43,7 @@ export default function QuickActionsBar({
|
||||
</div>
|
||||
<div className="row-inline action-buttons">
|
||||
<button className="secondary" onClick={() => saveTask("bar")} disabled={!canSaveTask}>Сохранить</button>
|
||||
<button className="secondary" onClick={() => exportTaskBundle("bar")} disabled={!hasSelectedTask}>Экспорт логов</button>
|
||||
<button className="secondary" onClick={() => parseHistory("bar")} disabled={!hasSelectedTask}>Собрать историю</button>
|
||||
<button className="secondary" onClick={() => joinGroupsForTask("bar")} disabled={!hasSelectedTask}>
|
||||
Добавить ботов в Telegram группы
|
||||
|
||||
@ -6,6 +6,7 @@ export default function TaskSettingsTab({
|
||||
taskForm,
|
||||
setTaskForm,
|
||||
activePreset,
|
||||
setActivePreset,
|
||||
applyTaskPreset,
|
||||
formatAccountLabel,
|
||||
accountById,
|
||||
@ -154,6 +155,53 @@ export default function TaskSettingsTab({
|
||||
}
|
||||
};
|
||||
|
||||
const inviteChecks = Array.isArray(inviteAccessStatus) ? inviteAccessStatus : [];
|
||||
const inviteChecksById = React.useMemo(() => {
|
||||
const map = new Map();
|
||||
inviteChecks.forEach((item) => {
|
||||
map.set(Number(item.accountId), item);
|
||||
});
|
||||
return map;
|
||||
}, [inviteChecks]);
|
||||
const masterId = Number(taskForm.inviteAdminMasterId || 0);
|
||||
const masterAccount = masterId ? (accountById.get(masterId) || accounts.find((item) => Number(item.id) === masterId)) : null;
|
||||
const masterCheck = masterId ? inviteChecksById.get(masterId) : null;
|
||||
const checkedCount = inviteChecks.length;
|
||||
const canInviteCount = inviteChecks.filter((item) => item && item.canInvite).length;
|
||||
const diagnostics = [
|
||||
{
|
||||
title: "Режим",
|
||||
value: taskForm.inviteViaAdmins ? "включен" : "выключен",
|
||||
tone: taskForm.inviteViaAdmins ? "ok" : "warn"
|
||||
},
|
||||
{
|
||||
title: "Мастер-админ",
|
||||
value: masterId ? formatAccountLabel(masterAccount || { id: masterId, phone: `ID ${masterId}` }) : "не выбран",
|
||||
tone: masterId ? "ok" : "fail"
|
||||
},
|
||||
{
|
||||
title: "Проверка прав",
|
||||
value: checkedCount ? `выполнена (${checkedCount} аккаунтов)` : "не выполнялась",
|
||||
tone: checkedCount ? "ok" : "warn"
|
||||
},
|
||||
{
|
||||
title: "Права мастера",
|
||||
value: !masterId
|
||||
? "нельзя проверить без выбора мастера"
|
||||
: !checkedCount
|
||||
? "нет данных (нажмите «Проверить права»)"
|
||||
: masterCheck
|
||||
? (masterCheck.canInvite ? "OK: может приглашать" : `ошибка: ${masterCheck.reason || "нет права приглашать"}`)
|
||||
: "мастер не вошел в результаты проверки",
|
||||
tone: !masterId ? "warn" : !checkedCount ? "warn" : (masterCheck && masterCheck.canInvite ? "ok" : "fail")
|
||||
},
|
||||
{
|
||||
title: "Инвайтеры с правом",
|
||||
value: checkedCount ? `${canInviteCount}/${checkedCount}` : "—",
|
||||
tone: !checkedCount ? "warn" : canInviteCount > 0 ? "ok" : "fail"
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="task-columns">
|
||||
<details className="card collapsible task-editor" open>
|
||||
@ -566,6 +614,20 @@ export default function TaskSettingsTab({
|
||||
Использует выдачу прав между аккаунтами, если Telegram ограничивает инвайтинг.
|
||||
</span>
|
||||
</label>
|
||||
<div className="admin-diagnostics" role="status" aria-live="polite">
|
||||
<div className="admin-diagnostics-title">Диагностика мастер-админа</div>
|
||||
<div className="admin-diagnostics-list">
|
||||
{diagnostics.map((item) => (
|
||||
<div key={item.title} className={`admin-diagnostics-item ${item.tone}`}>
|
||||
<span className="name">{item.title}</span>
|
||||
<span className="value">{item.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="hint">
|
||||
Если есть ошибки: проверьте, что мастер-админ в целевой группе, у него есть право выдачи админов, а инвайтеры состоят в целевой группе.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<details className="section">
|
||||
|
||||
@ -5,6 +5,7 @@ export default function useAppTabGroups({
|
||||
taskForm,
|
||||
setTaskForm,
|
||||
activePreset,
|
||||
setActivePreset,
|
||||
applyTaskPreset,
|
||||
formatAccountLabel,
|
||||
accountById,
|
||||
@ -121,6 +122,7 @@ export default function useAppTabGroups({
|
||||
taskForm,
|
||||
setTaskForm,
|
||||
activePreset,
|
||||
setActivePreset,
|
||||
applyTaskPreset,
|
||||
formatAccountLabel,
|
||||
accountById,
|
||||
|
||||
@ -36,9 +36,9 @@ export default function useInviteImport({
|
||||
setFileImportResult(result);
|
||||
setFileImportForm({ onlyIds: false, sourceChat: "" });
|
||||
showNotification("Файл импортирован.", "success");
|
||||
const invitesData = await window.api.listInvites(selectedTaskId);
|
||||
const invitesData = await window.api.listInvites({ limit: 200, taskId: selectedTaskId });
|
||||
setInvites(invitesData);
|
||||
const fallbackData = await window.api.listFallbackInvites(selectedTaskId);
|
||||
const fallbackData = await window.api.listFallback({ limit: 500, taskId: selectedTaskId });
|
||||
setFallbackList(fallbackData);
|
||||
await loadTaskStatuses();
|
||||
} catch (error) {
|
||||
|
||||
@ -23,6 +23,7 @@ export default function useMainUiProps({
|
||||
setActiveTab,
|
||||
tasksLength,
|
||||
runTestSafe,
|
||||
exportTaskBundle,
|
||||
nowLine,
|
||||
nowExpanded,
|
||||
setNowExpanded,
|
||||
@ -66,7 +67,8 @@ export default function useMainUiProps({
|
||||
pauseReason,
|
||||
setActiveTab,
|
||||
tasksLength,
|
||||
runTestSafe
|
||||
runTestSafe,
|
||||
exportTaskBundle
|
||||
};
|
||||
const nowStatus = {
|
||||
nowLine,
|
||||
|
||||
@ -11,6 +11,7 @@ export default function useTabProps(
|
||||
taskForm,
|
||||
setTaskForm,
|
||||
activePreset,
|
||||
setActivePreset,
|
||||
applyTaskPreset,
|
||||
formatAccountLabel,
|
||||
accountById,
|
||||
@ -141,6 +142,7 @@ export default function useTabProps(
|
||||
taskForm,
|
||||
setTaskForm,
|
||||
activePreset,
|
||||
setActivePreset,
|
||||
applyTaskPreset,
|
||||
formatAccountLabel,
|
||||
accountById,
|
||||
|
||||
@ -443,6 +443,23 @@ export default function useTaskActions({
|
||||
}
|
||||
};
|
||||
|
||||
const exportTaskBundle = async (source = "editor") => {
|
||||
if (!window.api || selectedTaskId == null) {
|
||||
showNotification("Сначала выберите задачу.", "error");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await window.api.exportTaskBundle(selectedTaskId);
|
||||
if (result && result.canceled) return;
|
||||
const details = result && result.counts
|
||||
? ` (логи: ${result.counts.logs}, инвайты: ${result.counts.invites}, события: ${result.counts.accountEvents})`
|
||||
: "";
|
||||
setTaskNotice({ text: `Экспорт логов задачи: ${result.filePath}${details}`, tone: "success", source });
|
||||
} catch (error) {
|
||||
showNotification(error.message || String(error), "error");
|
||||
}
|
||||
};
|
||||
|
||||
const exportInvites = async (source = "editor") => {
|
||||
if (!window.api || selectedTaskId == null) {
|
||||
showNotification("Сначала выберите задачу.", "error");
|
||||
@ -602,6 +619,7 @@ export default function useTaskActions({
|
||||
clearInvites,
|
||||
clearAccountEvents,
|
||||
exportLogs,
|
||||
exportTaskBundle,
|
||||
exportInvites,
|
||||
exportProblemInvites,
|
||||
exportFallback,
|
||||
|
||||
@ -435,6 +435,57 @@ body {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.admin-diagnostics {
|
||||
grid-column: span 2;
|
||||
border: 1px solid #dbeafe;
|
||||
background: #f8fbff;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.admin-diagnostics-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #1e3a8a;
|
||||
}
|
||||
|
||||
.admin-diagnostics-list {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.admin-diagnostics-item {
|
||||
display: grid;
|
||||
grid-template-columns: 160px 1fr;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.admin-diagnostics-item .name {
|
||||
color: #475569;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.admin-diagnostics-item .value {
|
||||
color: #0f172a;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.admin-diagnostics-item.ok .value {
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.admin-diagnostics-item.warn .value {
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.admin-diagnostics-item.fail .value {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.admin-invite-grid {
|
||||
grid-template-columns: 1fr;
|
||||
@ -443,6 +494,15 @@ body {
|
||||
.admin-invite-master {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.admin-diagnostics {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.admin-diagnostics-item {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user