some
This commit is contained in:
parent
4e15becd60
commit
303755f221
@ -112,11 +112,13 @@ const startTaskWithChecks = async (id) => {
|
|||||||
if (missingSessions.length) {
|
if (missingSessions.length) {
|
||||||
warnings.push(`Сессии не подключены: ${missingSessions.length} аккаунт(ов).`);
|
warnings.push(`Сессии не подключены: ${missingSessions.length} аккаунт(ов).`);
|
||||||
}
|
}
|
||||||
|
if (task.invite_via_admins) {
|
||||||
const noRights = (inviteAccess.result || []).filter((row) => !row.canInvite);
|
const noRights = (inviteAccess.result || []).filter((row) => !row.canInvite);
|
||||||
if (noRights.length) {
|
if (noRights.length) {
|
||||||
warnings.push(`Нет прав инвайта у ${noRights.length} аккаунт(ов).`);
|
warnings.push(`Нет прав инвайта у ${noRights.length} аккаунт(ов).`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (task.invite_via_admins) {
|
if (task.invite_via_admins) {
|
||||||
warnings.push("Режим инвайта через админов включен.");
|
warnings.push("Режим инвайта через админов включен.");
|
||||||
}
|
}
|
||||||
@ -596,6 +598,7 @@ ipcMain.handle("tasks:status", (_event, id) => {
|
|||||||
const runner = taskRunners.get(id);
|
const runner = taskRunners.get(id);
|
||||||
const queueCount = store.getPendingCount(id);
|
const queueCount = store.getPendingCount(id);
|
||||||
const dailyUsed = store.countInvitesToday(id);
|
const dailyUsed = store.countInvitesToday(id);
|
||||||
|
const unconfirmedCount = store.countInvitesByStatus(id, "unconfirmed");
|
||||||
const task = store.getTask(id);
|
const task = store.getTask(id);
|
||||||
const monitorInfo = telegram.getTaskMonitorInfo(id);
|
const monitorInfo = telegram.getTaskMonitorInfo(id);
|
||||||
const warnings = [];
|
const warnings = [];
|
||||||
@ -686,14 +689,16 @@ ipcMain.handle("tasks:status", (_event, id) => {
|
|||||||
const disconnected = parsed.filter((row) => row.reason === "Сессия не подключена").length;
|
const disconnected = parsed.filter((row) => row.reason === "Сессия не подключена").length;
|
||||||
const isChannel = parsed.some((row) => row.targetType === "channel");
|
const isChannel = parsed.some((row) => row.targetType === "channel");
|
||||||
const checkedAt = task.task_invite_access_at || "";
|
const checkedAt = task.task_invite_access_at || "";
|
||||||
|
if (task.invite_via_admins) {
|
||||||
warnings.push(`Права инвайта: ${canInvite}/${total} аккаунтов могут добавлять.${checkedAt ? ` Проверка: ${formatTimestamp(checkedAt)}.` : ""}`);
|
warnings.push(`Права инвайта: ${canInvite}/${total} аккаунтов могут добавлять.${checkedAt ? ` Проверка: ${formatTimestamp(checkedAt)}.` : ""}`);
|
||||||
|
}
|
||||||
if (disconnected) {
|
if (disconnected) {
|
||||||
warnings.push(`Сессия не подключена: ${disconnected} аккаунт(ов).`);
|
warnings.push(`Сессия не подключена: ${disconnected} аккаунт(ов).`);
|
||||||
}
|
}
|
||||||
if (isChannel) {
|
if (isChannel && task.invite_via_admins) {
|
||||||
warnings.push("Цель — канал: добавлять участников могут только админы.");
|
warnings.push("Цель — канал: добавлять участников могут только админы.");
|
||||||
}
|
}
|
||||||
if (canInvite === 0) {
|
if (canInvite === 0 && task.invite_via_admins) {
|
||||||
readiness.ok = false;
|
readiness.ok = false;
|
||||||
readiness.reasons.push("Нет аккаунтов с правами инвайта.");
|
readiness.reasons.push("Нет аккаунтов с правами инвайта.");
|
||||||
}
|
}
|
||||||
@ -708,6 +713,7 @@ ipcMain.handle("tasks:status", (_event, id) => {
|
|||||||
running: runner ? runner.isRunning() : false,
|
running: runner ? runner.isRunning() : false,
|
||||||
queueCount,
|
queueCount,
|
||||||
dailyUsed,
|
dailyUsed,
|
||||||
|
unconfirmedCount,
|
||||||
dailyLimit: effectiveLimit,
|
dailyLimit: effectiveLimit,
|
||||||
dailyRemaining: task ? Math.max(0, Number(effectiveLimit || 0) - dailyUsed) : 0,
|
dailyRemaining: task ? Math.max(0, Number(effectiveLimit || 0) - dailyUsed) : 0,
|
||||||
cycleCompetitors: task ? Boolean(task.cycle_competitors) : false,
|
cycleCompetitors: task ? Boolean(task.cycle_competitors) : false,
|
||||||
@ -808,13 +814,14 @@ ipcMain.handle("logs:export", async (_event, taskId) => {
|
|||||||
startedAt: log.startedAt,
|
startedAt: log.startedAt,
|
||||||
finishedAt: log.finishedAt,
|
finishedAt: log.finishedAt,
|
||||||
invitedCount: log.invitedCount,
|
invitedCount: log.invitedCount,
|
||||||
|
unconfirmedCount: log.meta && log.meta.unconfirmedCount != null ? log.meta.unconfirmedCount : "",
|
||||||
cycleLimit: log.meta && log.meta.cycleLimit ? log.meta.cycleLimit : "",
|
cycleLimit: log.meta && log.meta.cycleLimit ? log.meta.cycleLimit : "",
|
||||||
queueCount: log.meta && log.meta.queueCount != null ? log.meta.queueCount : "",
|
queueCount: log.meta && log.meta.queueCount != null ? log.meta.queueCount : "",
|
||||||
batchSize: log.meta && log.meta.batchSize != null ? log.meta.batchSize : "",
|
batchSize: log.meta && log.meta.batchSize != null ? log.meta.batchSize : "",
|
||||||
successIds: JSON.stringify(log.successIds || []),
|
successIds: JSON.stringify(log.successIds || []),
|
||||||
errors: JSON.stringify(log.errors || [])
|
errors: JSON.stringify(log.errors || [])
|
||||||
}));
|
}));
|
||||||
const csv = toCsv(logs, ["taskId", "startedAt", "finishedAt", "invitedCount", "cycleLimit", "queueCount", "batchSize", "successIds", "errors"]);
|
const csv = toCsv(logs, ["taskId", "startedAt", "finishedAt", "invitedCount", "unconfirmedCount", "cycleLimit", "queueCount", "batchSize", "successIds", "errors"]);
|
||||||
fs.writeFileSync(filePath, csv, "utf8");
|
fs.writeFileSync(filePath, csv, "utf8");
|
||||||
return { ok: true, filePath };
|
return { ok: true, filePath };
|
||||||
});
|
});
|
||||||
@ -860,6 +867,7 @@ ipcMain.handle("invites:exportProblems", async (_event, taskId) => {
|
|||||||
const time = invite.invitedAt ? new Date(invite.invitedAt).getTime() : 0;
|
const time = invite.invitedAt ? new Date(invite.invitedAt).getTime() : 0;
|
||||||
if (!Number.isFinite(time) || time < cutoff) return false;
|
if (!Number.isFinite(time) || time < cutoff) return false;
|
||||||
if (invite.status === "success") return false;
|
if (invite.status === "success") return false;
|
||||||
|
if (invite.status === "unconfirmed") return true;
|
||||||
if (!invite.error && !invite.skippedReason) return false;
|
if (!invite.error && !invite.skippedReason) return false;
|
||||||
return true;
|
return true;
|
||||||
}).map((invite) => ({
|
}).map((invite) => ({
|
||||||
|
|||||||
@ -898,6 +898,18 @@ function initStore(userDataPath) {
|
|||||||
).get(dayStart, accountId, taskId || 0).count;
|
).get(dayStart, accountId, taskId || 0).count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function countInvitesByStatus(taskId, status) {
|
||||||
|
if (!status) return 0;
|
||||||
|
if (taskId == null) {
|
||||||
|
return db.prepare(
|
||||||
|
"SELECT COUNT(*) as count FROM invites WHERE status = ? AND archived = 0"
|
||||||
|
).get(status).count;
|
||||||
|
}
|
||||||
|
return db.prepare(
|
||||||
|
"SELECT COUNT(*) as count FROM invites WHERE status = ? AND task_id = ? AND archived = 0"
|
||||||
|
).get(status, taskId || 0).count;
|
||||||
|
}
|
||||||
|
|
||||||
function addLog(entry) {
|
function addLog(entry) {
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
INSERT INTO logs (task_id, started_at, finished_at, invited_count, success_ids, error_summary, meta)
|
INSERT INTO logs (task_id, started_at, finished_at, invited_count, success_ids, error_summary, meta)
|
||||||
@ -1042,6 +1054,7 @@ function initStore(userDataPath) {
|
|||||||
recordInvite,
|
recordInvite,
|
||||||
countInvitesToday,
|
countInvitesToday,
|
||||||
countInvitesTodayByAccount,
|
countInvitesTodayByAccount,
|
||||||
|
countInvitesByStatus,
|
||||||
addLog
|
addLog
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -81,6 +81,7 @@ class TaskRunner {
|
|||||||
const errors = [];
|
const errors = [];
|
||||||
const successIds = [];
|
const successIds = [];
|
||||||
let invitedCount = 0;
|
let invitedCount = 0;
|
||||||
|
let unconfirmedCount = 0;
|
||||||
this.nextRunAt = "";
|
this.nextRunAt = "";
|
||||||
this.nextInviteAccountId = 0;
|
this.nextInviteAccountId = 0;
|
||||||
const accountMap = new Map(
|
const accountMap = new Map(
|
||||||
@ -204,10 +205,16 @@ class TaskRunner {
|
|||||||
sourceChat: item.source_chat
|
sourceChat: item.source_chat
|
||||||
});
|
});
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
|
const isConfirmed = result.confirmed === true;
|
||||||
|
if (isConfirmed) {
|
||||||
invitedCount += 1;
|
invitedCount += 1;
|
||||||
successIds.push(item.user_id);
|
successIds.push(item.user_id);
|
||||||
this.store.markInviteStatus(item.id, "invited");
|
} else {
|
||||||
|
unconfirmedCount += 1;
|
||||||
|
}
|
||||||
|
this.store.markInviteStatus(item.id, isConfirmed ? "invited" : "unconfirmed");
|
||||||
this.lastInviteAccountId = result.accountId || this.lastInviteAccountId;
|
this.lastInviteAccountId = result.accountId || this.lastInviteAccountId;
|
||||||
|
const inviteStatus = isConfirmed ? "success" : "unconfirmed";
|
||||||
this.store.recordInvite(
|
this.store.recordInvite(
|
||||||
this.task.id,
|
this.task.id,
|
||||||
item.user_id,
|
item.user_id,
|
||||||
@ -215,7 +222,7 @@ class TaskRunner {
|
|||||||
result.accountId,
|
result.accountId,
|
||||||
result.accountPhone,
|
result.accountPhone,
|
||||||
item.source_chat,
|
item.source_chat,
|
||||||
"success",
|
inviteStatus,
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
"invite",
|
"invite",
|
||||||
@ -226,7 +233,7 @@ class TaskRunner {
|
|||||||
result.strategyMeta,
|
result.strategyMeta,
|
||||||
this.task.our_group,
|
this.task.our_group,
|
||||||
result.targetType,
|
result.targetType,
|
||||||
result.confirmed !== false,
|
result.confirmed === true,
|
||||||
result.confirmError || ""
|
result.confirmError || ""
|
||||||
);
|
);
|
||||||
if (result.confirmed === false) {
|
if (result.confirmed === false) {
|
||||||
@ -363,7 +370,7 @@ class TaskRunner {
|
|||||||
invitedCount,
|
invitedCount,
|
||||||
successIds,
|
successIds,
|
||||||
errors,
|
errors,
|
||||||
meta: { cycleLimit: perCycleLimit, ...(this.cycleMeta || {}) }
|
meta: { cycleLimit: perCycleLimit, unconfirmedCount, ...(this.cycleMeta || {}) }
|
||||||
});
|
});
|
||||||
|
|
||||||
this._scheduleNext();
|
this._scheduleNext();
|
||||||
|
|||||||
@ -550,23 +550,63 @@ class TelegramManager {
|
|||||||
let targetEntity = null;
|
let targetEntity = null;
|
||||||
let targetType = "";
|
let targetType = "";
|
||||||
let resolvedUser = null;
|
let resolvedUser = null;
|
||||||
const confirmMembership = async (user) => {
|
const buildConfirmDetail = (code, message, sourceLabel) => {
|
||||||
|
if (!code) return message || "";
|
||||||
|
const base = message ? `${code}: ${message}` : code;
|
||||||
|
return sourceLabel ? `${base} (${sourceLabel})` : base;
|
||||||
|
};
|
||||||
|
const confirmMembership = async (user, confirmClient = client, sourceLabel = "") => {
|
||||||
if (!targetEntity || targetEntity.className !== "Channel") {
|
if (!targetEntity || targetEntity.className !== "Channel") {
|
||||||
return { confirmed: true, error: "" };
|
return { confirmed: true, error: "", detail: "" };
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await client.invoke(new Api.channels.GetParticipant({
|
await confirmClient.invoke(new Api.channels.GetParticipant({
|
||||||
channel: targetEntity,
|
channel: targetEntity,
|
||||||
participant: user
|
participant: user
|
||||||
}));
|
}));
|
||||||
return { confirmed: true, error: "" };
|
return { confirmed: true, error: "", detail: "" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorText = error.errorMessage || error.message || String(error);
|
const errorText = error.errorMessage || error.message || String(error);
|
||||||
if (errorText.includes("USER_NOT_PARTICIPANT")) {
|
if (errorText.includes("USER_NOT_PARTICIPANT")) {
|
||||||
return { confirmed: false, error: "not in group" };
|
return {
|
||||||
|
confirmed: false,
|
||||||
|
error: "USER_NOT_PARTICIPANT",
|
||||||
|
detail: buildConfirmDetail("USER_NOT_PARTICIPANT", "пользователь не в группе", sourceLabel)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return { confirmed: false, error: errorText };
|
if (errorText.includes("CHAT_ADMIN_REQUIRED")) {
|
||||||
|
return {
|
||||||
|
confirmed: null,
|
||||||
|
error: "CHAT_ADMIN_REQUIRED",
|
||||||
|
detail: buildConfirmDetail("CHAT_ADMIN_REQUIRED", "нет прав для подтверждения участия", sourceLabel)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
confirmed: null,
|
||||||
|
error: errorText,
|
||||||
|
detail: buildConfirmDetail(errorText, "ошибка подтверждения участия", sourceLabel)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const confirmMembershipWithFallback = async (user) => {
|
||||||
|
const attempts = [];
|
||||||
|
const direct = await confirmMembership(user, client, "проверка этим аккаунтом");
|
||||||
|
if (direct.detail) {
|
||||||
|
attempts.push({ strategy: "confirm", ok: direct.confirmed === true, detail: direct.detail });
|
||||||
|
}
|
||||||
|
if (direct.confirmed !== null) {
|
||||||
|
return { ...direct, attempts };
|
||||||
|
}
|
||||||
|
const masterId = Number(task.invite_admin_master_id || 0);
|
||||||
|
const masterEntry = masterId ? this.clients.get(masterId) : null;
|
||||||
|
if (masterEntry && masterEntry.client && masterEntry.client !== client) {
|
||||||
|
const adminConfirm = await confirmMembership(user, masterEntry.client, "проверка админом");
|
||||||
|
if (adminConfirm.detail) {
|
||||||
|
attempts.push({ strategy: "confirm_admin", ok: adminConfirm.confirmed === true, detail: adminConfirm.detail });
|
||||||
|
}
|
||||||
|
return { ...adminConfirm, attempts };
|
||||||
|
}
|
||||||
|
return { ...direct, attempts };
|
||||||
};
|
};
|
||||||
const attemptInvite = async (user) => {
|
const attemptInvite = async (user) => {
|
||||||
if (!targetEntity) {
|
if (!targetEntity) {
|
||||||
@ -630,6 +670,8 @@ class TelegramManager {
|
|||||||
user = null;
|
user = null;
|
||||||
attempts.push({ strategy: "access_hash", ok: false, detail: "invalid access_hash" });
|
attempts.push({ strategy: "access_hash", ok: false, detail: "invalid access_hash" });
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
attempts.push({ strategy: "access_hash", ok: false, detail: "not provided" });
|
||||||
}
|
}
|
||||||
if (!user && sourceChat) {
|
if (!user && sourceChat) {
|
||||||
const resolved = await this._resolveUserFromSource(client, sourceChat, userId);
|
const resolved = await this._resolveUserFromSource(client, sourceChat, userId);
|
||||||
@ -647,6 +689,8 @@ class TelegramManager {
|
|||||||
} else {
|
} else {
|
||||||
attempts.push({ strategy: "participants", ok: false, detail: resolved ? resolved.detail : "no result" });
|
attempts.push({ strategy: "participants", ok: false, detail: resolved ? resolved.detail : "no result" });
|
||||||
}
|
}
|
||||||
|
} else if (!user && !sourceChat) {
|
||||||
|
attempts.push({ strategy: "participants", ok: false, detail: "source chat not provided" });
|
||||||
}
|
}
|
||||||
if (!user && providedUsername) {
|
if (!user && providedUsername) {
|
||||||
const username = providedUsername.startsWith("@") ? providedUsername : `@${providedUsername}`;
|
const username = providedUsername.startsWith("@") ? providedUsername : `@${providedUsername}`;
|
||||||
@ -657,6 +701,8 @@ class TelegramManager {
|
|||||||
user = null;
|
user = null;
|
||||||
attempts.push({ strategy: "username", ok: false, detail: "resolve failed" });
|
attempts.push({ strategy: "username", ok: false, detail: "resolve failed" });
|
||||||
}
|
}
|
||||||
|
} else if (!user && !providedUsername) {
|
||||||
|
attempts.push({ strategy: "username", ok: false, detail: "username not provided" });
|
||||||
}
|
}
|
||||||
if (!user) {
|
if (!user) {
|
||||||
const resolvedUser = await client.getEntity(userId);
|
const resolvedUser = await client.getEntity(userId);
|
||||||
@ -694,7 +740,10 @@ class TelegramManager {
|
|||||||
await this._grantTempInviteAdmin(masterEntry.client, targetEntity, account);
|
await this._grantTempInviteAdmin(masterEntry.client, targetEntity, account);
|
||||||
lastAttempts.push({ strategy: "temp_admin", ok: true, detail: "granted" });
|
lastAttempts.push({ strategy: "temp_admin", ok: true, detail: "granted" });
|
||||||
await attemptInvite(user);
|
await attemptInvite(user);
|
||||||
const confirm = await confirmMembership(user);
|
const confirm = await confirmMembershipWithFallback(user);
|
||||||
|
if (confirm.attempts && confirm.attempts.length) {
|
||||||
|
lastAttempts.push(...confirm.attempts);
|
||||||
|
}
|
||||||
lastAttempts.push({ strategy: "temp_admin_invite", ok: true, detail: "invite" });
|
lastAttempts.push({ strategy: "temp_admin_invite", ok: true, detail: "invite" });
|
||||||
this.store.updateAccountStatus(account.id, "ok", "");
|
this.store.updateAccountStatus(account.id, "ok", "");
|
||||||
return {
|
return {
|
||||||
@ -705,7 +754,7 @@ class TelegramManager {
|
|||||||
strategyMeta: JSON.stringify(lastAttempts),
|
strategyMeta: JSON.stringify(lastAttempts),
|
||||||
targetType,
|
targetType,
|
||||||
confirmed: confirm.confirmed,
|
confirmed: confirm.confirmed,
|
||||||
confirmError: confirm.error
|
confirmError: confirm.detail || ""
|
||||||
};
|
};
|
||||||
} catch (adminError) {
|
} catch (adminError) {
|
||||||
const adminText = adminError.errorMessage || adminError.message || String(adminError);
|
const adminText = adminError.errorMessage || adminError.message || String(adminError);
|
||||||
@ -727,7 +776,10 @@ class TelegramManager {
|
|||||||
const masterEntry = masterId ? this.clients.get(masterId) : null;
|
const masterEntry = masterId ? this.clients.get(masterId) : null;
|
||||||
const adminClient = masterEntry ? masterEntry.client : client;
|
const adminClient = masterEntry ? masterEntry.client : client;
|
||||||
await attemptAdminInvite(user, adminClient);
|
await attemptAdminInvite(user, adminClient);
|
||||||
const confirm = await confirmMembership(user);
|
const confirm = await confirmMembershipWithFallback(user);
|
||||||
|
if (confirm.attempts && confirm.attempts.length) {
|
||||||
|
lastAttempts.push(...confirm.attempts);
|
||||||
|
}
|
||||||
lastAttempts.push({ strategy: "admin_invite", ok: true, detail: "editAdmin" });
|
lastAttempts.push({ strategy: "admin_invite", ok: true, detail: "editAdmin" });
|
||||||
this.store.updateAccountStatus(account.id, "ok", "");
|
this.store.updateAccountStatus(account.id, "ok", "");
|
||||||
return {
|
return {
|
||||||
@ -738,7 +790,7 @@ class TelegramManager {
|
|||||||
strategyMeta: JSON.stringify(lastAttempts),
|
strategyMeta: JSON.stringify(lastAttempts),
|
||||||
targetType,
|
targetType,
|
||||||
confirmed: confirm.confirmed,
|
confirmed: confirm.confirmed,
|
||||||
confirmError: confirm.error
|
confirmError: confirm.detail || ""
|
||||||
};
|
};
|
||||||
} catch (adminError) {
|
} catch (adminError) {
|
||||||
const adminText = adminError.errorMessage || adminError.message || String(adminError);
|
const adminText = adminError.errorMessage || adminError.message || String(adminError);
|
||||||
@ -761,7 +813,10 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await attemptInvite(user);
|
await attemptInvite(user);
|
||||||
const confirm = await confirmMembership(user);
|
const confirm = await confirmMembershipWithFallback(user);
|
||||||
|
if (confirm.attempts && confirm.attempts.length) {
|
||||||
|
lastAttempts.push(...confirm.attempts);
|
||||||
|
}
|
||||||
|
|
||||||
this.store.updateAccountStatus(account.id, "ok", "");
|
this.store.updateAccountStatus(account.id, "ok", "");
|
||||||
const last = lastAttempts.filter((item) => item.ok).slice(-1)[0];
|
const last = lastAttempts.filter((item) => item.ok).slice(-1)[0];
|
||||||
@ -773,7 +828,7 @@ class TelegramManager {
|
|||||||
strategyMeta: JSON.stringify(lastAttempts),
|
strategyMeta: JSON.stringify(lastAttempts),
|
||||||
targetType,
|
targetType,
|
||||||
confirmed: confirm.confirmed,
|
confirmed: confirm.confirmed,
|
||||||
confirmError: confirm.error
|
confirmError: confirm.detail || ""
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorText = error.errorMessage || error.message || String(error);
|
const errorText = error.errorMessage || error.message || String(error);
|
||||||
@ -834,6 +889,11 @@ class TelegramManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (errorText === "USER_ID_INVALID") {
|
if (errorText === "USER_ID_INVALID") {
|
||||||
|
lastAttempts.push({
|
||||||
|
strategy: "user_id_invalid",
|
||||||
|
ok: false,
|
||||||
|
detail: `username=${options.username || "—"}; hash=${options.userAccessHash || "—"}; source=${options.sourceChat || "—"}`
|
||||||
|
});
|
||||||
const username = options.username ? (options.username.startsWith("@") ? options.username : `@${options.username}`) : "";
|
const username = options.username ? (options.username.startsWith("@") ? options.username : `@${options.username}`) : "";
|
||||||
try {
|
try {
|
||||||
let retryUser = null;
|
let retryUser = null;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -129,24 +129,97 @@ body {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overview .row-header {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview .row-inline {
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview summary {
|
||||||
|
list-style: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible > summary {
|
||||||
|
list-style: none;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
padding-right: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible > summary::after {
|
||||||
|
content: "▾";
|
||||||
|
position: absolute;
|
||||||
|
right: 6px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible[open] > summary::after {
|
||||||
|
transform: translateY(-50%) rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible > summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview .summary-grid {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview .summary-grid.compact {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview .summary-card {
|
||||||
|
padding: 4px 6px;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview .summary-value {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview .live-label {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
.summary-grid {
|
.summary-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-grid.compact {
|
.summary-grid.compact {
|
||||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-card {
|
.summary-card {
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 10px 12px;
|
padding: 6px 8px;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
@ -157,7 +230,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.summary-value {
|
.summary-value {
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,10 +247,10 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.live-label {
|
.live-label {
|
||||||
font-size: 11px;
|
font-size: 10px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
text-transform: uppercase;
|
text-transform: none;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.live-value {
|
.live-value {
|
||||||
@ -260,16 +333,34 @@ body {
|
|||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions button {
|
|
||||||
min-width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
.header-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: stretch;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row .card {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row .card {
|
||||||
|
min-height: 360px;
|
||||||
|
max-height: 360px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row details[open] {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.global-actions {
|
.global-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@ -279,12 +370,47 @@ body {
|
|||||||
min-width: 140px;
|
min-width: 140px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-invite-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-invite-grid .input-row {
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-invite-master {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.admin-invite-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-invite-master {
|
||||||
|
grid-column: span 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sticky-tabs {
|
||||||
|
position: sticky;
|
||||||
|
top: 12px;
|
||||||
|
z-index: 3;
|
||||||
|
background: #f1f5f9;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
box-shadow: 0 12px 18px rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
@ -433,6 +559,13 @@ body {
|
|||||||
color: #64748b;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.autosave-note {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #16a34a;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||||
@ -441,17 +574,23 @@ body {
|
|||||||
|
|
||||||
.layout {
|
.layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 240px minmax(0, 1fr) 360px;
|
grid-template-columns: 240px minmax(0, 1fr);
|
||||||
|
grid-template-areas: "left main";
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
|
grid-area: main;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main > * {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -459,11 +598,24 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar.left {
|
.sidebar.left {
|
||||||
order: 0;
|
grid-area: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar.right {
|
.sidebar.right {
|
||||||
order: 2;
|
grid-area: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-columns {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.2fr) minmax(0, 0.8fr);
|
||||||
|
gap: 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.task-columns {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sticky {
|
.sticky {
|
||||||
@ -518,22 +670,75 @@ body {
|
|||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons .cta {
|
.action-buttons {
|
||||||
margin-left: auto;
|
display: grid;
|
||||||
padding: 4px 8px;
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||||
border-radius: 999px;
|
gap: 8px;
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons .cta + .cta {
|
.action-buttons .cta {
|
||||||
margin-left: 8px;
|
padding: 8px 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
box-shadow: 0 6px 14px rgba(37, 99, 235, 0.2);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons .danger.cta {
|
||||||
|
box-shadow: 0 6px 14px rgba(185, 28, 28, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons button {
|
.action-buttons button {
|
||||||
padding: 4px 8px;
|
padding: 6px 10px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-editor-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-editor-grid .section {
|
||||||
|
border-bottom: none;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-editor-grid .section summary {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.task-editor-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.row-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.top-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.status-pill {
|
.status-pill {
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
@ -660,7 +865,7 @@ textarea {
|
|||||||
|
|
||||||
.row-header {
|
.row-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@ -699,6 +904,28 @@ textarea {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section > summary,
|
||||||
|
.status-details > summary {
|
||||||
|
position: relative;
|
||||||
|
padding-right: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section > summary::after,
|
||||||
|
.status-details > summary::after {
|
||||||
|
content: "▾";
|
||||||
|
position: absolute;
|
||||||
|
right: 4px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section[open] > summary::after,
|
||||||
|
.status-details[open] > summary::after {
|
||||||
|
transform: translateY(-50%) rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
.row-inline {
|
.row-inline {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@ -1004,8 +1231,7 @@ button.danger {
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1046,8 +1272,8 @@ button.danger {
|
|||||||
|
|
||||||
.task-meta-row {
|
.task-meta-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
flex-direction: column;
|
||||||
flex-wrap: wrap;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-meta.monitor {
|
.task-meta.monitor {
|
||||||
@ -1105,6 +1331,13 @@ button.danger {
|
|||||||
color: #475569;
|
color: #475569;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-badge-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.task-badge.ok {
|
.task-badge.ok {
|
||||||
background: #dcfce7;
|
background: #dcfce7;
|
||||||
color: #15803d;
|
color: #15803d;
|
||||||
@ -1115,6 +1348,11 @@ button.danger {
|
|||||||
color: #64748b;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-badge.warn {
|
||||||
|
background: #ffedd5;
|
||||||
|
color: #b45309;
|
||||||
|
}
|
||||||
|
|
||||||
.match-badge {
|
.match-badge {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@ -1307,6 +1545,36 @@ label .hint {
|
|||||||
color: #b91c1c;
|
color: #b91c1c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.log-result.unconfirmed {
|
||||||
|
color: #b45309;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-status {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-status.success {
|
||||||
|
color: #0f766e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-status.failed {
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-status.skipped {
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-status.unconfirmed {
|
||||||
|
color: #b45309;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-stats {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #475569;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.invite-details {
|
.invite-details {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
|
|||||||
@ -43,7 +43,15 @@ function LogsTab({
|
|||||||
accountById,
|
accountById,
|
||||||
formatAccountLabel,
|
formatAccountLabel,
|
||||||
expandedInviteId,
|
expandedInviteId,
|
||||||
setExpandedInviteId
|
setExpandedInviteId,
|
||||||
|
inviteStats,
|
||||||
|
selectedTask,
|
||||||
|
taskAccountRoles,
|
||||||
|
accessStatus,
|
||||||
|
inviteAccessStatus,
|
||||||
|
selectedTaskName,
|
||||||
|
roleSummary,
|
||||||
|
mutualContactDiagnostics
|
||||||
}) {
|
}) {
|
||||||
const strategyLabel = (strategy) => {
|
const strategyLabel = (strategy) => {
|
||||||
switch (strategy) {
|
switch (strategy) {
|
||||||
@ -100,6 +108,24 @@ function LogsTab({
|
|||||||
if (!Number.isFinite(startMs) || !Number.isFinite(finishMs)) return null;
|
if (!Number.isFinite(startMs) || !Number.isFinite(finishMs)) return null;
|
||||||
return Math.max(0, finishMs - startMs);
|
return Math.max(0, finishMs - startMs);
|
||||||
};
|
};
|
||||||
|
const hasBothRoles = (accountId) => {
|
||||||
|
if (!accountId || !taskAccountRoles) return false;
|
||||||
|
const roles = taskAccountRoles[String(accountId)] || taskAccountRoles[accountId];
|
||||||
|
return Boolean(roles && roles.monitor && roles.invite);
|
||||||
|
};
|
||||||
|
const formatInviteStatus = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case "success":
|
||||||
|
return "Успех";
|
||||||
|
case "skipped":
|
||||||
|
return "Пропуск";
|
||||||
|
case "unconfirmed":
|
||||||
|
return "Не подтверждено";
|
||||||
|
case "failed":
|
||||||
|
default:
|
||||||
|
return "Ошибка";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="card logs">
|
<section className="card logs">
|
||||||
@ -157,6 +183,13 @@ function LogsTab({
|
|||||||
>
|
>
|
||||||
Fallback
|
Fallback
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`tab ${logsTab === "diagnostics" ? "active" : ""}`}
|
||||||
|
onClick={() => setLogsTab("diagnostics")}
|
||||||
|
>
|
||||||
|
Диагностика
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{logsTab === "logs" && (
|
{logsTab === "logs" && (
|
||||||
<>
|
<>
|
||||||
@ -270,6 +303,11 @@ function LogsTab({
|
|||||||
)}
|
)}
|
||||||
{logsTab === "invites" && (
|
{logsTab === "invites" && (
|
||||||
<>
|
<>
|
||||||
|
{inviteStats && (
|
||||||
|
<div className="invite-stats">
|
||||||
|
Всего: {inviteStats.total} | Успех: {inviteStats.success} | Ошибка: {inviteStats.failed} | Пропуск: {inviteStats.skipped} | Не подтверждено: {inviteStats.unconfirmed}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="row-inline">
|
<div className="row-inline">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -341,6 +379,16 @@ function LogsTab({
|
|||||||
>
|
>
|
||||||
Пропуск
|
Пропуск
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`chip ${inviteFilter === "unconfirmed" ? "active" : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
setInviteFilter("unconfirmed");
|
||||||
|
setInvitePage(1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Не подтверждено
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{pagedInvites.length === 0 && <div className="empty">История пока пустая.</div>}
|
{pagedInvites.length === 0 && <div className="empty">История пока пустая.</div>}
|
||||||
{pagedInvites.map((invite) => (
|
{pagedInvites.map((invite) => (
|
||||||
@ -348,17 +396,13 @@ function LogsTab({
|
|||||||
<div className="log-time">
|
<div className="log-time">
|
||||||
<div>{formatTimestamp(invite.invitedAt)}</div>
|
<div>{formatTimestamp(invite.invitedAt)}</div>
|
||||||
<div>
|
<div>
|
||||||
{invite.status === "success"
|
<span className={`log-status ${invite.status}`}>{formatInviteStatus(invite.status)}</span>
|
||||||
? "Успех"
|
|
||||||
: invite.status === "skipped"
|
|
||||||
? "Пропуск"
|
|
||||||
: "Ошибка"}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="log-details">
|
<div className="log-details">
|
||||||
<div>ID: {invite.userId}</div>
|
<div>ID: {invite.userId}</div>
|
||||||
<div className="log-users wrap">
|
<div className="log-users wrap">
|
||||||
Ник: {invite.username ? `@${invite.username}` : "—"}
|
Ник: {invite.username ? `@${invite.username}` : "— (нет username в источнике)"}
|
||||||
</div>
|
</div>
|
||||||
<div className="log-users wrap">
|
<div className="log-users wrap">
|
||||||
Источник: {invite.sourceChat || "—"}
|
Источник: {invite.sourceChat || "—"}
|
||||||
@ -379,6 +423,12 @@ function LogsTab({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{invite.watcherAccountId && invite.accountId && invite.watcherAccountId !== invite.accountId
|
||||||
|
&& selectedTask && selectedTask.randomAccounts && hasBothRoles(invite.watcherAccountId) && (
|
||||||
|
<div className="log-users">
|
||||||
|
Примечание: у наблюдателя стоят обе роли, но включен случайный выбор — инвайт выполнен другим аккаунтом.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="log-users">Наблюдатель: {(() => {
|
<div className="log-users">Наблюдатель: {(() => {
|
||||||
const account = accountById.get(invite.watcherAccountId);
|
const account = accountById.get(invite.watcherAccountId);
|
||||||
return account ? formatAccountLabel(account) : (invite.watcherPhone || "—");
|
return account ? formatAccountLabel(account) : (invite.watcherPhone || "—");
|
||||||
@ -394,9 +444,9 @@ function LogsTab({
|
|||||||
Вероятная причина: {explainInviteError(invite.error) || "Причина не определена"}
|
Вероятная причина: {explainInviteError(invite.error) || "Причина не определена"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{invite.status === "success" && invite.confirmed === false && (
|
{invite.confirmError && (
|
||||||
<div className="log-errors">
|
<div className="log-errors">
|
||||||
Подтверждение: не найден в группе{invite.confirmError ? ` (${invite.confirmError})` : ""}
|
Подтверждение: {invite.confirmError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{invite.strategy && (
|
{invite.strategy && (
|
||||||
@ -427,9 +477,14 @@ function LogsTab({
|
|||||||
<div>Цель: {invite.targetChat || "—"}</div>
|
<div>Цель: {invite.targetChat || "—"}</div>
|
||||||
<div>Тип цели: {formatTargetType(invite.targetType)}</div>
|
<div>Тип цели: {formatTargetType(invite.targetType)}</div>
|
||||||
<div>Действие: {invite.action || "invite"}</div>
|
<div>Действие: {invite.action || "invite"}</div>
|
||||||
<div>Статус: {invite.status}</div>
|
<div>Статус: {formatInviteStatus(invite.status)}</div>
|
||||||
<div>Пропуск: {invite.skippedReason || "—"}</div>
|
<div>Пропуск: {invite.skippedReason || "—"}</div>
|
||||||
<div>Ошибка: {invite.error || "—"}</div>
|
<div>Ошибка: {invite.error || "—"}</div>
|
||||||
|
<div>Подтверждение: {invite.confirmError || "—"}</div>
|
||||||
|
{invite.watcherAccountId && invite.accountId && invite.watcherAccountId !== invite.accountId
|
||||||
|
&& selectedTask && selectedTask.randomAccounts && hasBothRoles(invite.watcherAccountId) && (
|
||||||
|
<div>Примечание: у наблюдателя стоят обе роли, но включен случайный выбор — инвайт выполнен другим аккаунтом.</div>
|
||||||
|
)}
|
||||||
<div>Вероятная причина: {explainInviteError(invite.error) || "Причина не определена"}</div>
|
<div>Вероятная причина: {explainInviteError(invite.error) || "Причина не определена"}</div>
|
||||||
<div>Стратегия: {invite.strategy || "—"}</div>
|
<div>Стратегия: {invite.strategy || "—"}</div>
|
||||||
<div className="pre-line">
|
<div className="pre-line">
|
||||||
@ -560,6 +615,95 @@ function LogsTab({
|
|||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{logsTab === "diagnostics" && (
|
||||||
|
<>
|
||||||
|
<div className="log-users">Для: {selectedTaskName}</div>
|
||||||
|
{accessStatus && accessStatus.length > 0 && (
|
||||||
|
<div className="access-block">
|
||||||
|
<div className="access-title">Доступ к группам</div>
|
||||||
|
<div className="access-list">
|
||||||
|
{accessStatus.map((item, index) => (
|
||||||
|
<div key={`${item.value}-${index}`} className={`access-row ${item.ok ? "ok" : "fail"}`}>
|
||||||
|
<div className="access-title">
|
||||||
|
{item.type === "our" ? "Наша" : "Конкурент"}: {item.title || item.value}
|
||||||
|
</div>
|
||||||
|
<div className="access-status">
|
||||||
|
{item.ok ? "Доступ есть" : "Нет доступа"}
|
||||||
|
</div>
|
||||||
|
{!item.ok && <div className="access-error">{item.details}</div>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{inviteAccessStatus && inviteAccessStatus.length > 0 && (
|
||||||
|
<div className="access-block">
|
||||||
|
<div className="access-title">Права инвайта</div>
|
||||||
|
<div className="access-subtitle">
|
||||||
|
Проверяются аккаунты с ролью инвайта: {roleSummary ? roleSummary.invite.length : "—"}
|
||||||
|
</div>
|
||||||
|
<div className="access-list">
|
||||||
|
{inviteAccessStatus.map((item, index) => (
|
||||||
|
<div key={`${item.accountId}-${index}`} className={`access-row ${item.canInvite ? "ok" : "fail"}`}>
|
||||||
|
<div className="access-title">
|
||||||
|
{(() => {
|
||||||
|
const account = accountById.get(item.accountId);
|
||||||
|
return account ? formatAccountLabel(account) : (item.accountPhone || item.accountId);
|
||||||
|
})()}: {item.title || item.targetChat}
|
||||||
|
{item.targetType ? ` (${formatTargetType(item.targetType)})` : ""}
|
||||||
|
</div>
|
||||||
|
<div className="access-status">
|
||||||
|
{item.canInvite ? "Можно инвайтить" : "Нет прав"}
|
||||||
|
</div>
|
||||||
|
{!item.canInvite && <div className="access-error">{item.reason || "—"}</div>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{mutualContactDiagnostics && (
|
||||||
|
<div className="access-block">
|
||||||
|
<div className="access-title">USER_NOT_MUTUAL_CONTACT</div>
|
||||||
|
<div className="access-subtitle">
|
||||||
|
Ошибок в истории: {mutualContactDiagnostics.count}
|
||||||
|
</div>
|
||||||
|
<div className="access-list">
|
||||||
|
<div className="access-row">
|
||||||
|
<div className="access-title">
|
||||||
|
Цель: {(() => {
|
||||||
|
const entry = inviteAccessStatus && inviteAccessStatus[0];
|
||||||
|
if (entry) {
|
||||||
|
const label = entry.title || entry.targetChat || (selectedTask ? selectedTask.ourGroup : "—");
|
||||||
|
const typeLabel = formatTargetType(entry.targetType);
|
||||||
|
return typeLabel ? `${label} (${typeLabel})` : label;
|
||||||
|
}
|
||||||
|
return selectedTask ? selectedTask.ourGroup : "—";
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
<div className="access-status">
|
||||||
|
{inviteAccessStatus && inviteAccessStatus.length ? "Права проверены" : "Нет данных проверки"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{mutualContactDiagnostics.recent.map((item) => (
|
||||||
|
<div key={`mutual-${item.id}`} className="access-row fail">
|
||||||
|
<div className="access-title">
|
||||||
|
Пользователь: {item.userId}{item.username ? ` (@${item.username})` : ""}
|
||||||
|
</div>
|
||||||
|
<div className="access-status">{formatTimestamp(item.invitedAt)}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="access-error">
|
||||||
|
Возможные причины: Telegram ограничивает инвайт, если пользователь скрывает приём приглашений,
|
||||||
|
целевая группа требует взаимного контакта, или у пользователя есть приватные ограничения.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!accessStatus?.length && !inviteAccessStatus?.length && (
|
||||||
|
<div className="empty">Диагностика пока пустая.</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user