telegram-invite-automation/src/renderer/App.jsx
2026-02-11 17:04:20 +04:00

1130 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useDeferredValue, useMemo, useRef } from "react";
import { emptyTaskForm, normalizeIntervals, sanitizeTaskForm } from "./appDefaults.js";
import { formatAccountLabel, formatAccountStatus, formatTimestamp, formatCountdown } from "./utils/formatters.js";
import { copyToClipboard } from "./utils/clipboard.js";
import { explainInviteError, explainTdataError } from "./utils/errorHints.js";
import AppOverlays from "./components/AppOverlays.jsx";
import AppSidebar from "./components/AppSidebar.jsx";
import AppMain from "./components/AppMain.jsx";
import useTaskStatusView from "./hooks/useTaskStatusView.js";
import useNotifications from "./hooks/useNotifications.js";
import useLogsView from "./hooks/useLogsView.js";
import useAccountImport from "./hooks/useAccountImport.js";
import useTaskActions from "./hooks/useTaskActions.js";
import useAccessChecks from "./hooks/useAccessChecks.js";
import useAccountManagement from "./hooks/useAccountManagement.js";
import useTaskPresets from "./hooks/useTaskPresets.js";
import useTaskSelection from "./hooks/useTaskSelection.js";
import useInviteImport from "./hooks/useInviteImport.js";
import useSettingsActions from "./hooks/useSettingsActions.js";
import useTaskLoaders from "./hooks/useTaskLoaders.js";
import useTaskFormActions from "./hooks/useTaskFormActions.js";
import useAccountComputed from "./hooks/useAccountComputed.js";
import useAppOrchestration from "./hooks/useAppOrchestration.js";
import useTaskSelectors from "./hooks/useTaskSelectors.js";
import useCriticalEvents from "./hooks/useCriticalEvents.js";
import useUiComputed from "./hooks/useUiComputed.js";
import useMainUiProps from "./hooks/useMainUiProps.js";
import useAppTabGroups from "./hooks/useAppTabGroups.js";
import useAppLoaders from "./hooks/useAppLoaders.js";
import useAppState from "./hooks/useAppState.js";
import useAppTaskDerived from "./hooks/useAppTaskDerived.js";
import useOpenLogsTabListener from "./hooks/useOpenLogsTabListener.js";
import useAppOutsideClicks from "./hooks/useAppOutsideClicks.js";
import { APP_VERSION } from "./constants/consts.js";
export default function App() {
const {
settings,
setSettings,
accounts,
setAccounts,
accountStats,
setAccountStats,
accountAssignments,
setAccountAssignments,
globalStatus,
setGlobalStatus,
logs,
setLogs,
invites,
setInvites,
fallbackList,
setFallbackList,
confirmQueue,
setConfirmQueue,
tasks,
setTasks,
selectedTaskId,
setSelectedTaskId,
taskForm,
setTaskForm,
competitorText,
setCompetitorText,
selectedAccountIds,
setSelectedAccountIds,
taskAccountRoles,
setTaskAccountRoles,
activePreset,
setActivePreset,
presetSignatureRef,
taskStatus,
setTaskStatus,
taskStatusMap,
setTaskStatusMap,
membershipStatus,
setMembershipStatus,
groupVisibility,
setGroupVisibility,
accessStatus,
setAccessStatus,
inviteAccessStatus,
setInviteAccessStatus,
inviteAccessCheckedAt,
setInviteAccessCheckedAt,
confirmAccessStatus,
setConfirmAccessStatus,
confirmAccessCheckedAt,
setConfirmAccessCheckedAt,
accountEvents,
setAccountEvents,
apiTraceLogs,
setApiTraceLogs,
taskAudit,
setTaskAudit,
testRun,
setTestRun,
queueItems,
setQueueItems,
queueStats,
setQueueStats,
fileImportForm,
setFileImportForm,
fileImportResult,
setFileImportResult,
taskActionLoading,
setTaskActionLoading,
taskNotice,
setTaskNotice,
autosaveNote,
setAutosaveNote,
settingsNotice,
setSettingsNotice,
tdataNotice,
setTdataNotice,
notificationsOpen,
setNotificationsOpen,
notificationsModalRef,
importModalOpen,
setImportModalOpen,
nowExpanded,
setNowExpanded,
moreActionsOpen,
setMoreActionsOpen,
moreActionsRef,
checklistOpen,
setChecklistOpen,
manualLoginOpen,
setManualLoginOpen,
taskSearch,
setTaskSearch,
taskFilter,
setTaskFilter,
infoOpen,
setInfoOpen,
infoTab,
setInfoTab,
activeTab,
setActiveTab,
logsTab,
setLogsTab,
taskSort,
setTaskSort,
expandedInviteId,
setExpandedInviteId,
liveConfirmOpen,
setLiveConfirmOpen,
liveConfirmContext,
setLiveConfirmContext,
now,
setNow,
isVisible,
setIsVisible,
bellRef,
settingsAutosaveReady,
taskAutosaveReady,
taskAutosaveTimer,
autosaveNoteTimer,
tasksPollInFlight,
accountsPollInFlight,
logsPollInFlight,
eventsPollInFlight
} = useAppState();
const liveConfirmResolver = useRef(null);
const {
competitorGroups,
hasSelectedTask,
selectedTask,
selectedTaskName,
canSaveTask
} = useAppTaskDerived({
tasks,
selectedTaskId,
taskForm,
competitorText
});
const appendTestEvent = async (action, details) => {
if (!window.api) return;
try {
await window.api.addAccountEvent({
accountId: 0,
action,
details
});
} catch (error) {
// ignore logging errors
}
};
const runTest = async (mode) => {
const startedAt = new Date().toISOString();
setTestRun({
status: "running",
mode,
steps: [],
startedAt: formatTimestamp(startedAt),
finishedAt: "",
summary: ""
});
await appendTestEvent("test_run_start", `${selectedTaskName} · режим: ${mode}`);
const steps = [];
const pushStep = (title, status, details) => {
const step = { title, status, details };
steps.push(step);
setTestRun((prev) => ({ ...prev, steps: [...steps] }));
appendTestEvent("test_run_step", `${title} · ${status}${details ? ` · ${details}` : ""}`);
};
if (!hasSelectedTask) {
pushStep("Выбрана задача", "error", "Задача не выбрана");
} else {
pushStep("Выбрана задача", "ok", selectedTaskName);
}
const missing = [];
if (!taskForm.name.trim()) missing.push("название");
if (!taskForm.ourGroup.trim()) missing.push("наша группа");
if (competitorGroups.length === 0) missing.push("группы конкурентов");
if (missing.length) {
pushStep("Заполнены поля задачи", "error", `Не заполнено: ${missing.join(", ")}`);
} else {
pushStep("Заполнены поля задачи", "ok", "Минимально достаточно");
}
if (selectedAccountIds.length === 0) {
pushStep("Аккаунты в задаче", "error", "Не выбраны аккаунты");
} else {
pushStep("Аккаунты в задаче", "ok", `Выбрано: ${selectedAccountIds.length}`);
}
const inviteCount = roleSummary.invite.length;
const monitorCount = roleSummary.monitor.length;
const confirmCount = roleSummary.confirm.length;
const roleDetails = `мониторинг: ${monitorCount}, инвайт: ${inviteCount}, подтверждение: ${confirmCount}`;
if (inviteCount === 0) {
pushStep("Роли аккаунтов", "error", `Нет роли инвайта (${roleDetails})`);
} else {
pushStep("Роли аккаунтов", "ok", roleDetails);
}
const membershipIds = Object.keys(membershipStatus || {});
const inOurGroup = membershipIds.filter((id) => membershipStatus[id]?.ourGroupMember).length;
pushStep("Участие в нашей группе", inOurGroup > 0 ? "ok" : "warn", `В нашей группе: ${inOurGroup}`);
if (!inviteAccessStatus || inviteAccessStatus.length === 0) {
pushStep("Проверка прав инвайта", "warn", "Права не проверялись");
} else {
const okCount = inviteAccessStatus.filter((item) => item && item.ok).length;
pushStep("Проверка прав инвайта", okCount > 0 ? "ok" : "warn", `OK: ${okCount} / ${inviteAccessStatus.length}`);
}
pushStep("Очередь", queueStats.total > 0 ? "ok" : "warn", `В очереди: ${queueStats.total}`);
const minInterval = Number(taskForm.minIntervalMinutes || 0);
const maxInterval = Number(taskForm.maxIntervalMinutes || 0);
const intervalOk = minInterval > 0 && maxInterval > 0 && minInterval <= maxInterval;
pushStep("Интервалы и лимиты", intervalOk ? "ok" : "warn", `Мин: ${minInterval || "—"} · Макс: ${maxInterval || "—"}`);
if (taskForm.inviteViaAdmins) {
pushStep("Инвайт через админов", taskForm.inviteAdminMasterId ? "ok" : "warn", taskForm.inviteAdminMasterId ? "Мастер-админ выбран" : "Не выбран мастер-админ");
} else {
pushStep("Инвайт через админов", "ok", "Отключено");
}
if (mode === "live") {
pushStep("Live: проверка прав групп", "ok", "Запущена");
await checkAccess();
pushStep("Live: проверка прав инвайта", "ok", "Запущена");
await checkInviteAccess();
pushStep("Live: проверка видимости конкурентов", "ok", "Запущена");
const visibilityResult = await window.api.groupVisibilityByTask(selectedTaskId);
if (visibilityResult && visibilityResult.ok && Array.isArray(visibilityResult.result)) {
const items = visibilityResult.result;
const openCount = items.filter((item) => item && item.status === "open").length;
const closedCount = items.filter((item) => item && item.status === "closed").length;
const unknownCount = items.filter((item) => !item || !item.status || item.status === "unknown").length;
pushStep("Live: видимость конкурентов", "ok", `Открытые: ${openCount} · Закрытые: ${closedCount} · Неизвестные: ${unknownCount}`);
const closedLinks = items
.filter((item) => item && item.status === "closed")
.map((item) => item.link || item.source || "")
.filter(Boolean);
if (closedLinks.length) {
const preview = closedLinks.slice(0, 5).join(", ");
const suffix = closedLinks.length > 5 ? ` и ещё ${closedLinks.length - 5}` : "";
pushStep("Live: закрытые конкуренты", "warn", `${preview}${suffix}`);
}
} else {
pushStep("Live: видимость конкурентов", "warn", "Не удалось определить видимость");
}
pushStep("Live: обновление участия", "ok", "Запущено");
await refreshMembership("test");
if (queueStats.total > 0) {
const confirmed = await new Promise((resolve) => {
liveConfirmResolver.current = resolve;
const item = (queueItems || [])[0] || null;
setLiveConfirmContext(item ? {
userId: item.user_id,
username: item.username,
sourceChat: item.source_chat
} : null);
setLiveConfirmOpen(true);
});
if (confirmed) {
pushStep("Live: тестовый инвайт", "ok", "Запуск");
const liveResult = await window.api.testInviteOnce({ taskId: selectedTaskId });
if (liveResult && liveResult.ok) {
const confirmLabel = liveResult.confirmed === true
? "подтверждено"
: liveResult.confirmed === false
? `не подтверждено${liveResult.confirmError ? ` (${liveResult.confirmError})` : ""}`
: "не проверено";
pushStep("Live: результат инвайта", "ok", `Успешно · ${confirmLabel}`);
} else {
pushStep("Live: результат инвайта", "warn", liveResult && liveResult.error ? liveResult.error : "Ошибка инвайта");
}
} else {
pushStep("Live: тестовый инвайт", "warn", "Отменено пользователем");
}
} else {
pushStep("Live: тестовый инвайт", "warn", "Очередь пуста");
}
}
const hasError = steps.some((step) => step.status === "error");
const hasWarn = steps.some((step) => step.status === "warn");
const finishedAt = new Date().toISOString();
const status = hasError ? "error" : hasWarn ? "warn" : "ok";
const summary = hasError ? "Есть ошибки" : hasWarn ? "Есть предупреждения" : "Всё готово";
setTestRun((prev) => ({
...prev,
status,
finishedAt: formatTimestamp(finishedAt),
summary
}));
await appendTestEvent("test_run_finish", `${status} · ${summary}`);
};
const {
toasts,
notifications,
setNotifications,
notificationFilter,
setNotificationFilter,
filteredNotifications,
showNotification,
dismissToast
} = useNotifications();
const {
logSearch,
setLogSearch,
inviteSearch,
setInviteSearch,
fallbackSearch,
setFallbackSearch,
auditSearch,
setAuditSearch,
confirmSearch,
setConfirmSearch,
logPage,
setLogPage,
invitePage,
setInvitePage,
fallbackPage,
setFallbackPage,
auditPage,
setAuditPage,
confirmPage,
setConfirmPage,
queueSearch,
setQueueSearch,
queuePage,
setQueuePage,
inviteFilter,
setInviteFilter,
logPageCount,
invitePageCount,
fallbackPageCount,
auditPageCount,
confirmPageCount,
queuePageCount,
pagedLogs,
pagedInvites,
pagedFallback,
pagedAudit,
pagedConfirmQueue,
pagedQueue,
inviteStats,
mutualContactDiagnostics
} = useLogsView({
logs,
invites,
fallbackList,
taskAudit,
confirmQueue,
queueItems
});
const confirmStats = useMemo(() => {
const stats = {
total: confirmQueue.length,
pending: 0,
confirmed: 0,
failed: 0
};
(confirmQueue || []).forEach((item) => {
if (!item) return;
if (item.status === "confirmed") stats.confirmed += 1;
else if (item.status === "failed") stats.failed += 1;
else stats.pending += 1;
});
return stats;
}, [confirmQueue]);
const { checkAccess, checkInviteAccess, checkConfirmAccess } = useAccessChecks({
selectedTaskId,
setAccessStatus,
setInviteAccessStatus,
setInviteAccessCheckedAt,
setConfirmAccessStatus,
setConfirmAccessCheckedAt,
setTaskNotice,
showNotification
});
const {
loadTasks,
loadAccountAssignments,
loadTaskStatuses,
loadBase
} = useAppLoaders({
selectedTaskId,
setTasks,
setSelectedTaskId,
setAccountAssignments,
setTaskStatusMap,
setSettings,
setAccounts,
setAccountEvents,
setAccountStats,
setGlobalStatus
});
const deferredTaskSearch = useDeferredValue(taskSearch);
const { onSettingsChange, saveSettings } = useSettingsActions({
settings,
setSettings,
setSettingsNotice,
showNotification
});
const { createTask, selectTask } = useTaskSelection({
taskAutosaveReady,
selectedTaskId,
setSelectedTaskId,
setTaskForm,
setCompetitorText,
setSelectedAccountIds,
setTaskAccountRoles,
setAccessStatus,
setMembershipStatus
});
const { importInviteFile } = useInviteImport({
fileImportForm,
setFileImportForm,
setFileImportResult,
hasSelectedTask,
selectedTaskId,
showNotification,
setInvites,
setFallbackList,
loadTaskStatuses
});
const { loadSelectedTask, refreshMembership } = useTaskLoaders({
taskAutosaveReady,
setTaskForm,
setCompetitorText,
setSelectedAccountIds,
setTaskAccountRoles,
setLogs,
setInvites,
setFallbackList,
setConfirmQueue,
setGroupVisibility,
setTaskStatus,
setMembershipStatus,
showNotification,
selectedTaskId
});
const {
saveTask,
deleteTask,
startTask,
stopTask,
startAllTasks,
stopAllTasks,
parseHistory,
checkAll,
joinGroupsForTask,
clearLogs,
clearInvites,
clearAccountEvents,
exportLogs,
exportTaskBundle,
exportInvites,
exportProblemInvites,
exportFallback,
updateFallbackStatus,
clearFallback,
clearConfirmQueue,
clearQueue,
clearAllTaskLogsAndQueue,
clearDatabase,
resetSessions
} = useTaskActions({
taskForm,
setTaskForm,
sanitizeTaskForm,
taskAccountRoles,
setTaskAccountRoles,
selectedAccountIds,
setSelectedAccountIds,
accounts,
selectedTaskId,
selectedTaskName,
competitorGroups,
hasSelectedTask,
setTaskNotice,
showNotification,
setAutosaveNote,
autosaveNoteTimer,
loadTasks,
loadAccountAssignments,
loadTaskStatuses,
refreshMembership,
checkInviteAccess,
checkAccess,
setLogs,
setInvites,
setTaskStatus,
setTaskAudit,
setSelectedTaskId,
resetTaskForm: () => setTaskForm(emptyTaskForm),
setCompetitorText,
resetSelectedAccountIds: setSelectedAccountIds,
resetTaskAccountRoles: setTaskAccountRoles,
setFallbackList,
setConfirmQueue,
setAccountEvents,
setTaskActionLoading,
taskActionLoading,
loadBase,
createTask,
setActiveTab,
checkConfirmAccess
});
const {
accountById,
accountStatsMap,
roleSummary,
roleIntersectionCount,
assignedAccountCount,
assignedAccountMap,
filterFreeAccounts,
accountBuckets,
perAccountInviteSum
} = useAccountComputed({
accounts,
accountStats,
taskAccountRoles,
accountAssignments,
selectedTaskId,
tasks
});
const {
persistAccountRoles,
computeAdminConfirmConfigRisk,
fixAdminConfirmConfigRisk,
computeConfirmAccessRisk,
fixConfirmAccessRisk,
resetCooldown,
deleteAccount,
refreshIdentity,
updateAccountRole,
updateAccountInviteLimit,
setInviteLimitForAllInviters,
setAccountRolesAll,
applyRolePreset,
computeWatcherInviteRisk,
fixWatcherInviteRisk,
assignAccountsToTask,
moveAccountToTask,
removeAccountFromTask
} = useAccountManagement({
selectedTaskId,
taskAccountRoles,
setTaskAccountRoles,
setTaskForm,
selectedAccountIds,
setSelectedAccountIds,
accounts,
accountBuckets,
taskForm,
hasSelectedTask,
loadAccountAssignments,
showNotification,
setTaskNotice,
setAccounts,
membershipStatus,
refreshMembership,
confirmAccessStatus
});
const { applyTaskPreset } = useTaskPresets({
hasSelectedTask,
accounts,
selectedAccountIds,
taskForm,
setTaskForm,
setTaskAccountRoles,
setSelectedAccountIds,
persistAccountRoles,
showNotification,
setTaskNotice,
setActivePreset,
setActiveTab,
selectedTaskId,
presetSignatureRef
});
const {
loginForm,
setLoginForm,
tdataForm,
setTdataForm,
loginStatus,
tdataResult,
tdataLoading,
startLogin,
completeLogin,
importTdata
} = useAccountImport({
selectedTaskId,
hasSelectedTask,
assignAccountsToTask,
setAccounts,
showNotification,
explainTdataError,
setTdataNotice
});
const { taskSummary, filteredTasks } = useTaskSelectors({
tasks,
taskStatusMap,
deferredTaskSearch,
taskFilter,
taskSort
});
useAppOutsideClicks({
notificationsOpen,
notificationsModalRef,
bellRef,
setNotificationsOpen,
moreActionsOpen,
moreActionsRef,
setMoreActionsOpen
});
const formatCountdownWithNowLocal = (value) => formatCountdown(value, now);
const { criticalErrorAccounts } = useCriticalEvents({
accountEvents,
accounts
});
const {
monitorLabels,
inviteLabels,
nowLine,
primaryIssue,
openFixTab,
checklistItems,
checklistStats,
inviteAccessChecked,
inviteAccessOk,
inviteAccessWarn,
lastEvents
} = useTaskStatusView({
taskStatus,
taskAccountRoles,
accountById,
formatAccountLabel,
setActiveTab,
checkInviteAccess,
checkConfirmAccess,
parseHistory,
refreshMembership,
assignedAccountCount,
roleSummary,
accountEvents,
formatCountdownWithNow: formatCountdownWithNowLocal,
inviteAccessStatus,
confirmAccessStatus,
membershipStatus,
selectedTask,
computeWatcherInviteRisk,
fixWatcherInviteRisk
});
const {
pauseReason,
hasPerAccountInviteLimits,
formatCountdownWithNow
} = useUiComputed({
taskStatus,
assignedAccountCount,
roleSummary,
inviteAccessWarn,
taskAccountRoles,
taskStatusMap,
tasks,
formatCountdown,
now
});
const refreshQueue = React.useCallback(async () => {
if (!window.api || selectedTaskId == null) return;
try {
const queueData = await window.api.listQueue({ limit: 200, taskId: selectedTaskId });
if (queueData && Array.isArray(queueData.items)) setQueueItems(queueData.items);
if (queueData && queueData.stats) setQueueStats(queueData.stats);
} catch (_error) {
// noop: queue refresh errors are non-blocking for UI actions
}
}, [selectedTaskId, setQueueItems, setQueueStats]);
useAppOrchestration({
activeTab,
selectedTaskId,
isVisible,
setIsVisible,
setNow,
tasksPollInFlight,
accountsPollInFlight,
logsPollInFlight,
eventsPollInFlight,
setTasks,
loadTaskStatuses,
setTaskStatus,
setAccounts,
setAccountAssignments,
setAccountStats,
setGlobalStatus,
setLogs,
setInvites,
setFallbackList,
setConfirmQueue,
setTaskAudit,
setAccountEvents,
setApiTraceLogs,
setQueueItems,
setQueueStats,
loadBase,
loadSelectedTask,
setAccessStatus,
setInviteAccessStatus,
setMembershipStatus,
setTaskNotice,
setActivePreset,
checkAccess,
checkInviteAccess,
activePreset,
taskForm,
taskAccountRoles,
presetSignatureRef,
taskStatus,
setTaskStatusMap,
checklistStats,
setChecklistOpen,
hasSelectedTask,
roleIntersectionCount,
roleSummary,
setTaskForm,
sanitizeTaskForm,
taskNotice,
settingsNotice,
tdataNotice,
setSettingsNotice,
setTdataNotice,
showNotification,
settings,
settingsAutosaveReady,
taskAutosaveReady,
taskAutosaveTimer,
competitorText,
selectedAccountIds,
canSaveTask,
saveTask,
setSettings
});
const clearApiTrace = async () => {
if (!window.api) return;
try {
await window.api.clearApiTrace(selectedTaskId || 0);
setApiTraceLogs(await window.api.listApiTrace({ limit: 300, taskId: selectedTaskId || 0 }));
showNotification("API трассировка очищена.", "success");
} catch (error) {
showNotification(error.message || String(error), "error");
}
};
const toggleApiTrace = async () => {
if (!window.api) return;
try {
const next = !Boolean(settings && settings.apiTraceEnabled);
const updated = await window.api.saveSettings({ ...settings, apiTraceEnabled: next });
setSettings(updated);
showNotification(`Трассировка API ${next ? "включена" : "выключена"}.`, next ? "success" : "info");
} catch (error) {
showNotification(error.message || String(error), "error");
}
};
const exportApiTraceJson = async () => {
if (!window.api) return;
try {
const result = await window.api.exportApiTraceJson(selectedTaskId || 0);
if (result && result.ok) {
showNotification(`API трассировка выгружена в JSON: ${result.filePath}`, "success");
}
} catch (error) {
showNotification(error.message || String(error), "error");
}
};
const exportApiTraceCsv = async () => {
if (!window.api) return;
try {
const result = await window.api.exportApiTraceCsv(selectedTaskId || 0);
if (result && result.ok) {
showNotification(`API трассировка выгружена в CSV: ${result.filePath}`, "success");
}
} catch (error) {
showNotification(error.message || String(error), "error");
}
};
const { applyRoleMode } = useTaskFormActions({
taskForm,
setTaskForm
});
const { quickActions, nowStatus, checklist, tabs } = useMainUiProps({
selectedTaskName,
autosaveNote,
taskStatus,
hasSelectedTask,
canSaveTask,
taskActionLoading,
saveTask,
parseHistory,
joinGroupsForTask,
checkAll,
startTask,
stopTask,
moreActionsOpen,
setMoreActionsOpen,
moreActionsRef,
clearQueue,
clearAllTaskLogsAndQueue,
startAllTasks,
stopAllTasks,
clearDatabase,
resetSessions,
pauseReason,
setActiveTab,
tasksLength: tasks.length,
runTestSafe: () => runTest("safe"),
exportTaskBundle,
refreshMembership,
refreshIdentity,
apiTraceEnabled: Boolean(settings && settings.apiTraceEnabled),
toggleApiTrace,
setInfoOpen,
setInfoTab,
nowLine,
nowExpanded,
setNowExpanded,
primaryIssue,
openFixTab,
monitorLabels,
inviteLabels,
roleSummary,
groupVisibility,
lastEvents,
formatTimestamp,
checklistOpen,
setChecklistOpen,
checklistStats,
checklistItems,
activeTab,
logsTab,
setLogsTab,
confirmStats
});
const { taskSettings, accountsTab, logsTab: logsTabGroup, queueTab: queueTabGroup, apiTraceTab, eventsTab, settingsTab } = useAppTabGroups({
selectedTaskId,
refreshQueue,
selectedTaskName,
taskForm,
setTaskForm,
activePreset,
setActivePreset,
applyTaskPreset,
formatAccountLabel,
accountById,
competitorText,
setCompetitorText,
applyRoleMode,
normalizeIntervals,
taskStatus,
perAccountInviteSum,
hasSelectedTask,
inviteAccessStatus,
inviteAccessCheckedAt,
confirmAccessStatus,
confirmAccessCheckedAt,
formatTimestamp,
checkInviteAccess,
checkConfirmAccess,
accounts,
showNotification,
copyToClipboard,
sanitizeTaskForm,
hasPerAccountInviteLimits,
fileImportResult,
importInviteFile,
fileImportForm,
setFileImportForm,
criticalErrorAccounts,
accountStatsMap,
settings,
membershipStatus,
assignedAccountMap,
accountBuckets,
filterFreeAccounts,
selectedAccountIds,
taskAccountRoles,
inviteAdminMasterId: taskForm.inviteAdminMasterId,
refreshMembership,
refreshIdentity,
formatAccountStatus,
resetCooldown,
deleteAccount,
updateAccountRole,
updateAccountInviteLimit,
setInviteLimitForAllInviters,
setAccountRolesAll,
applyRolePreset,
computeWatcherInviteRisk,
fixWatcherInviteRisk,
removeAccountFromTask,
moveAccountToTask,
logsTab,
setLogsTab,
exportLogs,
clearLogs,
exportInvites,
exportProblemInvites,
exportFallback,
updateFallbackStatus,
clearFallback,
clearInvites,
logSearch,
setLogSearch,
logPage,
setLogPage,
logPageCount,
pagedLogs,
inviteSearch,
setInviteSearch,
invitePage,
setInvitePage,
invitePageCount,
inviteFilter,
setInviteFilter,
pagedInvites,
fallbackSearch,
setFallbackSearch,
fallbackPage,
setFallbackPage,
fallbackPageCount,
pagedFallback,
confirmQueue,
confirmSearch,
setConfirmSearch,
confirmPage,
setConfirmPage,
confirmPageCount,
pagedConfirmQueue,
confirmStats,
queueItems,
queueStats,
queueSearch,
setQueueSearch,
queuePage,
setQueuePage,
queuePageCount,
pagedQueue,
clearConfirmQueue,
auditSearch,
setAuditSearch,
auditPage,
setAuditPage,
auditPageCount,
pagedAudit,
explainInviteError,
expandedInviteId,
setExpandedInviteId,
inviteStats,
invites,
selectedTask,
accessStatus,
roleSummary,
mutualContactDiagnostics,
apiTraceLogs,
clearApiTrace,
exportApiTraceJson,
exportApiTraceCsv,
accountEvents,
clearAccountEvents,
onSettingsChange,
saveSettings
});
useOpenLogsTabListener(setActiveTab);
return (
<div className="app">
<AppOverlays
importModalOpen={importModalOpen}
setImportModalOpen={setImportModalOpen}
manualLoginOpen={manualLoginOpen}
setManualLoginOpen={setManualLoginOpen}
hasSelectedTask={hasSelectedTask}
loginForm={loginForm}
setLoginForm={setLoginForm}
startLogin={startLogin}
completeLogin={completeLogin}
loginStatus={loginStatus}
tdataForm={tdataForm}
setTdataForm={setTdataForm}
importTdata={importTdata}
tdataLoading={tdataLoading}
tdataResult={tdataResult}
explainTdataError={explainTdataError}
notificationsOpen={notificationsOpen}
setNotificationsOpen={setNotificationsOpen}
notificationsModalRef={notificationsModalRef}
notifications={notifications}
filteredNotifications={filteredNotifications}
notificationFilter={notificationFilter}
setNotificationFilter={setNotificationFilter}
setNotifications={setNotifications}
infoOpen={infoOpen}
setInfoOpen={setInfoOpen}
infoTab={infoTab}
setInfoTab={setInfoTab}
liveConfirmOpen={liveConfirmOpen}
setLiveConfirmOpen={setLiveConfirmOpen}
liveConfirmContext={liveConfirmContext}
onConfirmLiveInvite={() => {
setLiveConfirmOpen(false);
setLiveConfirmContext(null);
if (liveConfirmResolver.current) {
liveConfirmResolver.current(true);
liveConfirmResolver.current = null;
}
}}
onCancelLiveInvite={() => {
setLiveConfirmOpen(false);
setLiveConfirmContext(null);
if (liveConfirmResolver.current) {
liveConfirmResolver.current(false);
liveConfirmResolver.current = null;
}
}}
toasts={toasts}
dismissToast={dismissToast}
/>
<div className="layout">
<AppSidebar
taskSummary={taskSummary}
globalStatus={globalStatus}
selectedTaskName={selectedTaskName}
competitorGroups={competitorGroups}
assignedAccountCount={assignedAccountCount}
taskStatus={taskStatus}
notificationsOpen={notificationsOpen}
setNotificationsOpen={setNotificationsOpen}
infoOpen={infoOpen}
setInfoOpen={setInfoOpen}
bellRef={bellRef}
notificationsCount={notifications.length}
onOpenImport={() => setImportModalOpen(true)}
createTask={createTask}
taskSearch={taskSearch}
setTaskSearch={setTaskSearch}
taskFilter={taskFilter}
setTaskFilter={setTaskFilter}
taskSort={taskSort}
setTaskSort={setTaskSort}
filteredTasks={filteredTasks}
taskStatusMap={taskStatusMap}
selectedTaskId={selectedTaskId}
selectTask={selectTask}
deleteTask={deleteTask}
hasSelectedTask={hasSelectedTask}
formatCountdown={formatCountdownWithNow}
formatTimestamp={formatTimestamp}
accountById={accountById}
formatAccountLabel={formatAccountLabel}
/>
<AppMain
quickActions={quickActions}
nowStatus={nowStatus}
testRun={testRun}
runTestLive={() => runTest("live")}
checklist={checklist}
tabs={tabs}
taskSettings={taskSettings}
accountsTab={accountsTab}
logsTab={logsTabGroup}
queueTab={queueTabGroup}
apiTraceTab={apiTraceTab}
eventsTab={eventsTab}
settingsTab={settingsTab}
/>
</div>
<div className="app-footer">Версия: {APP_VERSION}</div>
</div>
);
}