1130 lines
32 KiB
JavaScript
1130 lines
32 KiB
JavaScript
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>
|
||
);
|
||
}
|