import {
  Activity,
  Archive,
  CheckCircle2,
  ChevronDown,
  CircleAlert,
  Copy,
  Database,
  Download,
  ExternalLink,
  FileText,
  FolderCheck,
  FolderOpen,
  GitBranch,
  Image,
  KeyRound,
  Loader2,
  Plug,
  Plus,
  RefreshCw,
  Save,
  Search,
  Settings,
  Server,
  Trash2,
  UploadCloud,
  X
} from "lucide-react";
import type { ChangeEvent, DragEvent, FormEvent } from "react";
import { useEffect, useRef, useState } from "react";
import {
  copyProject,
  deleteProject,
  downloadSemanticAnalysisExport,
  fetchExternalServiceSettings,
  fetchHealth,
  fetchLatestMarketEnrichment,
  fetchLatestSemanticAnalysis,
  fetchLatestProjectScan,
  fetchProjects,
  getPreviewPageUrl,
  importBrowserFolder,
  openPageWorkspace,
  renameProject,
  resetPageWorkspace,
  runMarketEnrichment,
  runProjectScan,
  runSemanticAnalysis,
  savePageWorkspace,
  updateExternalServiceSettings,
  updateProjectPageSelection,
  type BrowserFolderFileInput,
  type ExternalServiceSettings,
  type ExternalServiceUpdateInput,
  type HealthResponse,
  type MarketEnrichmentContract,
  type PageWorkspace,
  type ProjectScanSummary,
  type ProjectSummary,
  type SemanticAnalysisRun,
  type SemanticExportFormat
} from "./api";
import ProjectScopeFlow from "./ProjectScopeFlow";
import "./styles.css";

const pipeline = [
  "Источник проекта",
  "Конфигурация проекта",
  "Рабочая зона страницы",
  "Семантика",
  "Сбор частотности",
  "Карта ключей",
  "Рерайт",
  "Валидация",
  "Применение"
];

type WorkspaceMode = "auto" | "dev";
type ExternalService = ExternalServiceSettings["services"][number];
type ExternalServiceDraft = ExternalServiceUpdateInput;

const stageMeta = [
  {
    title: "Источник проекта",
    sectionTitle: "Создать проект",
    description: "Подключаем папку сайта, называем проект и держим список источников под рукой."
  },
  {
    title: "Конфигурация проекта",
    sectionTitle: "Карта SEO scope",
    description: "Сканируем проект, собираем публичные страницы и технический слой в визуальную карту выбора."
  },
  {
    title: "Рабочая зона страницы",
    sectionTitle: "Рабочая зона страниц",
    description: "Открываем preview, секции страницы и draft-редактор без изменения исходного сайта."
  },
  {
    title: "Семантический анализ",
    sectionTitle: "SEO-аналитика",
    description: "Считаем покрытие продуктовой онтологии NodeDC, legacy-мусор, gaps и стартовые сиды."
  },
  {
    title: "Сбор частотности",
    sectionTitle: "Сбор частотности",
    description: "Будущий этап проверки сидов и ключей через частотность."
  },
  {
    title: "Карта ключей",
    sectionTitle: "Карта ключей",
    description: "Будущий этап распределения ключей по страницам и секциям."
  },
  {
    title: "Рерайт",
    sectionTitle: "Рерайт",
    description: "Будущий этап подготовки правок текста и meta-полей."
  },
  {
    title: "Валидация",
    sectionTitle: "Валидация",
    description: "Будущий этап проверки качества изменений перед применением."
  },
  {
    title: "Применение",
    sectionTitle: "Применение",
    description: "Будущий безопасный Apply с diff и контролем конфликтов."
  }
];

const AUDIT_ISSUE_PAGE_SIZE = 8;
const NOTICE_AUTO_HIDE_MS = 8000;

const dateFormatter = new Intl.DateTimeFormat("ru-RU", {
  dateStyle: "medium",
  timeStyle: "short"
});
const numberFormatter = new Intl.NumberFormat("ru-RU");

function getErrorMessage(error: unknown, fallback: string) {
  return error instanceof Error ? error.message : fallback;
}

function formatDate(value: string) {
  return dateFormatter.format(new Date(value));
}

function formatCount(value: number | null) {
  return value === null ? "n/a" : numberFormatter.format(value);
}

function normalizeProjectName(value: string) {
  return value.trim().toLocaleLowerCase("ru-RU");
}

function isProjectCardInteractionTarget(target: EventTarget | null) {
  if (!(target instanceof Element)) {
    return false;
  }

  return Boolean(
    target.closest(
      [
        "button",
        "a",
        "input",
        "textarea",
        "select",
        "label",
        "summary",
        "details",
        "[contenteditable='true']",
        ".project-title",
        ".project-title-input",
        ".project-delete-confirmation",
        ".project-action-message",
        ".scan-selection",
        ".audit-summary",
        ".page-workspace-panel",
        ".workspace-help",
        ".scan-help"
      ].join(", ")
    )
  );
}

function getSourceDescription(project: ProjectSummary) {
  if (!project.source) {
    return "Источник ещё не указан";
  }

  const importedAt =
    typeof project.source.config.importedAt === "string" ? project.source.config.importedAt : project.source.createdAt;
  const importDateText = importedAt ? `, ${formatDate(importedAt)}` : "";

  if (project.source.path) {
    return `${project.source.path}${importDateText}`;
  }

  const rootName = project.source.config.rootName;
  const fileCount = project.source.config.fileCount;

  if (typeof rootName === "string") {
    return typeof fileCount === "number"
      ? `Импортированная папка «${rootName}», файлов: ${fileCount}${importDateText}`
      : `Импортированная папка «${rootName}»${importDateText}`;
  }

  return project.source.type;
}

function getScanStatusLabel(status: string) {
  const statusLabels: Record<string, string> = {
    cancelled: "отменён",
    done: "готов",
    failed: "ошибка",
    queued: "в очереди",
    running: "идёт"
  };

  return statusLabels[status] ?? status;
}

function getPageTitle(page: ProjectScanSummary["pages"][number]) {
  if (page.scope === "template_block" && page.structureLabel) {
    return page.structureLabel;
  }

  if (isHomePage(page)) {
    return page.title || "Главная страница";
  }

  return page.title || page.urlPath || page.sourcePath;
}

function isHomePage(page: ProjectScanSummary["pages"][number]) {
  return page.urlPath === "/" && /(^|\/)index\.html?$/i.test(page.sourcePath);
}

function getProjectSiteTitle(scan: ProjectScanSummary | null | undefined) {
  if (!scan) {
    return "Название сайта появится после анализа";
  }

  const homePageTitle = scan.pages.find(isHomePage)?.title?.trim();

  if (homePageTitle) {
    return homePageTitle;
  }

  const firstParsedTitle = scan.pages.find((page) => page.title?.trim())?.title?.trim();

  if (firstParsedTitle) {
    return firstParsedTitle;
  }

  return "Тайтл главной страницы не найден";
}

function getPagePathLabel(page: ProjectScanSummary["pages"][number]) {
  if (isHomePage(page)) {
    return "index.html · главная страница";
  }

  return page.urlPath && page.urlPath !== page.sourcePath ? `${page.urlPath} · ${page.sourcePath}` : page.sourcePath;
}

function isIndexableScanPage(page: ProjectScanSummary["pages"][number]) {
  return page.scope === "indexable_page";
}

function isWorkspaceScopePage(page: ProjectScanSummary["pages"][number]) {
  if (!page.selected) {
    return false;
  }

  if (page.previewKind === "none" && !page.previewSourcePath && !page.previewTargetSelector) {
    return false;
  }

  return true;
}

function getWorkspaceScopeOrder(page: ProjectScanSummary["pages"][number], originalIndex: number) {
  if (isHomePage(page)) {
    return -1;
  }

  if (page.structureOrder != null) {
    return page.structureOrder;
  }

  return 1000 + originalIndex;
}

function getScanPageScopeLabel(scope: ProjectScanSummary["pages"][number]["scope"]) {
  const labels = {
    admin_page: "админка",
    indexable_page: "посадочная",
    technical_page: "техническая",
    template_block: "шаблон"
  };

  return labels[scope];
}

function getScanScopeCounts(scan: ProjectScanSummary) {
  return scan.pages.reduce(
    (counts, page) => {
      if (page.scope === "indexable_page") counts.indexable += 1;
      if (page.scope === "template_block") counts.template += 1;
      if (page.scope === "admin_page") counts.admin += 1;
      if (page.scope === "technical_page") counts.technical += 1;
      if (page.selected && page.scope === "indexable_page") counts.selectedIndexable += 1;
      if (page.selected && page.scope !== "indexable_page") counts.selectedExcluded += 1;
      return counts;
    },
    { admin: 0, indexable: 0, selectedExcluded: 0, selectedIndexable: 0, technical: 0, template: 0 }
  );
}

function getScanStatusCounts(scan: ProjectScanSummary) {
  const items = [...scan.pages, ...scan.assets];

  return items.reduce(
    (accumulator, item) => {
      if (item.reconcileStatus === "matched") accumulator.matched += 1;
      if (item.reconcileStatus === "changed") accumulator.changed += 1;
      if (item.reconcileStatus === "new") accumulator.new += 1;
      return accumulator;
    },
    { changed: 0, matched: 0, new: 0 }
  );
}

function getScanResultMessage(scan: ProjectScanSummary) {
  const totalItems = scan.counts.pages + scan.counts.assets;
  const statusCounts = getScanStatusCounts(scan);

  if (scan.status === "failed") {
    return scan.errorMessage || "Скан завершился с ошибкой.";
  }

  if (totalItems === 0) {
    return "Скан успешен, но HTML-страницы и ассеты пока не найдены.";
  }

  if (statusCounts.changed > 0) {
    return `Повторный скан успешен: изменилось ${statusCounts.changed}, совпало ${statusCounts.matched}, новых ${statusCounts.new}.`;
  }

  if (statusCounts.matched === totalItems) {
    return "Повторный скан успешен: всё уже было просканировано, изменений не найдено.";
  }

  if (statusCounts.new === totalItems) {
    return "Первый скан успешен: страницы и ассеты добавлены в индекс.";
  }

  return `Скан успешен: новых ${statusCounts.new}, совпало ${statusCounts.matched}, изменилось ${statusCounts.changed}.`;
}

function getAuditResultMessage(scan: ProjectScanSummary) {
  if (!scan.audit) {
    return "Базовый аудит ещё не запускался.";
  }

  if (scan.audit.counts.total === 0) {
    return "Базовый аудит: критичных замечаний не найдено.";
  }

  return `Базовый аудит: замечаний ${scan.audit.counts.total}, предупреждений ${scan.audit.counts.warnings}, инфо ${scan.audit.counts.info}.`;
}

function getProjectScanActionMessage(scan: ProjectScanSummary) {
  const totalItems = scan.counts.pages + scan.counts.assets;
  const statusCounts = getScanStatusCounts(scan);
  const scanMessage =
    totalItems > 0 && statusCounts.matched === totalItems
      ? "Повторный скан: изменений не найдено."
      : getScanResultMessage(scan);

  return [
    scanMessage,
    `Страниц: ${scan.counts.pages}, ассетов: ${scan.counts.assets}.`,
    getAuditResultMessage(scan)
  ].join("\n");
}

function getProjectScanStatusMessage(scan: ProjectScanSummary) {
  const completedAt = scan.completedAt ? ` · ${formatDate(scan.completedAt)}` : "";

  return `Скан запускался: ${getScanStatusLabel(scan.status)} · страниц: ${scan.counts.pages}, ассетов: ${scan.counts.assets}${completedAt}.`;
}

function getAuditIssueCounts(issues: AuditIssue[]) {
  return issues.reduce(
    (counts, issue) => {
      counts.total += 1;
      if (issue.severity === "error") counts.errors += 1;
      if (issue.severity === "warning") counts.warnings += 1;
      if (issue.severity === "info") counts.info += 1;
      return counts;
    },
    { errors: 0, info: 0, total: 0, warnings: 0 }
  );
}

function getAuditFilterCount(counts: ReturnType<typeof getAuditIssueCounts>, filter: AuditSeverityFilter) {
  if (filter === "all") return counts.total;
  if (filter === "error") return counts.errors;
  if (filter === "warning") return counts.warnings;
  return counts.info;
}

function getSelectedAuditResultMessage(selectedPagesCount: number, counts: ReturnType<typeof getAuditIssueCounts>) {
  if (selectedPagesCount === 0) {
    return "Базовый аудит выбранных страниц: страницы пока не выбраны.";
  }

  if (counts.total === 0) {
    return "Базовый аудит выбранных страниц: ошибок не найдено.";
  }

  return `Базовый аудит выбранных страниц: ошибок ${counts.total}, предупреждений ${counts.warnings}, инфо ${counts.info}.`;
}

function getAuditSeverityLabel(severity: NonNullable<ProjectScanSummary["audit"]>["issues"][number]["severity"]) {
  const labels = {
    error: "Блокер",
    info: "Инфо",
    warning: "Предупреждение"
  };

  return labels[severity];
}

function getSemanticClusterStatusLabel(status: SemanticAnalysisRun["clusters"][number]["status"]) {
  const labels = {
    covered: "Покрыто",
    missing: "Пусто",
    weak: "Слабо"
  };

  return labels[status];
}

function getSemanticPriorityLabel(priority: SemanticAnalysisRun["clusters"][number]["priority"]) {
  const labels = {
    high: "high",
    low: "low",
    medium: "medium"
  };

  return labels[priority];
}

function getSemanticQualityLabel(level: SemanticAnalysisRun["qualitySignals"][number]["level"]) {
  const labels = {
    ok: "OK",
    problem: "Проблема",
    warning: "Риск"
  };

  return labels[level];
}

function getSemanticVerdictConfidenceLabel(
  confidence: NonNullable<SemanticAnalysisRun["analysisVerdict"]>["confidence"]
) {
  const labels = {
    high: "Высокое доверие",
    low: "Низкое доверие",
    medium: "Среднее доверие"
  };

  return labels[confidence];
}

function getSemanticVerdictObjectivityLabel(
  objectivity: NonNullable<SemanticAnalysisRun["analysisVerdict"]>["marketObjectivity"]
) {
  const labels = {
    internal_only: "Только внутренний анализ",
    market_ready: "Рыночно подтверждено",
    partial: "Частично подтверждено"
  };

  return labels[objectivity];
}

function getSemanticVerdictAssessmentLabel(
  assessment: NonNullable<SemanticAnalysisRun["analysisVerdict"]>["clusterAssessments"][number]["assessment"]
) {
  const labels = {
    confirmed_in_content: "Есть в контенте",
    missing_in_content: "Пусто",
    needs_market_validation: "Нужен рынок",
    weak_in_content: "Слабо"
  };

  return labels[assessment];
}

function getSemanticEvidenceLevelLabel(level: NonNullable<SemanticAnalysisRun["clusters"][number]["evidence"]>["level"]) {
  const labels = {
    moderate: "moderate evidence",
    none: "no evidence",
    strong: "strong evidence",
    thin: "thin evidence"
  };

  return labels[level];
}

function getLandingOpportunityActionLabel(
  action: NonNullable<SemanticAnalysisRun["landingOpportunities"]>[number]["action"]
) {
  const labels = {
    create_target_page: "создать посадочную",
    keep_home: "оставить главную + расширить",
    strengthen_existing: "усилить страницу",
    validate_external: "проверить спрос"
  };

  return labels[action];
}

function getLandingPagePlanActionLabel(action: NonNullable<SemanticAnalysisRun["landingPagePlan"]>[number]["action"]) {
  const labels = {
    create: "создать",
    strengthen: "усилить",
    validate: "проверить"
  };

  return labels[action];
}

type SemanticBrief = NonNullable<SemanticAnalysisRun["landingPageBriefs"]>[number];
type SemanticBriefQuality = NonNullable<SemanticBrief["qualityContract"]>;

function getBriefQualityLevelLabel(level: SemanticBriefQuality["level"]) {
  const labels = {
    blocked: "blocked",
    needs_work: "needs work",
    ready: "ready"
  };

  return labels[level];
}

function getBriefQualityCheckLabel(status: SemanticBriefQuality["checks"][number]["status"]) {
  const labels = {
    fail: "fail",
    pass: "pass",
    warn: "warn"
  };

  return labels[status];
}

function getMarketStateLabel(state: MarketEnrichmentContract["state"]) {
  const labels = {
    not_collected: "не собрано",
    not_configured: "provider не настроен",
    not_ready: "не готово",
    partial_ready: "частично готово"
  };

  return labels[state];
}

function getMarketProviderStatusLabel(status: MarketEnrichmentContract["providers"][number]["status"]) {
  const labels = {
    connected: "connected",
    disabled: "disabled",
    not_configured: "not configured",
    not_implemented: "not implemented"
  };

  return labels[status];
}

function getMarketEvidenceStatusLabel(status: MarketEnrichmentContract["seedQueue"][number]["evidenceStatus"]) {
  const labels = {
    collection_failed: "failed",
    collected: "collected",
    collected_no_signal: "no signal",
    collected_related_only: "related only",
    deterministic_only: "deterministic",
    not_collected: "not collected",
    not_configured: "not configured",
    ready_for_collection: "ready"
  };

  return labels[status];
}

function getExternalServiceStatusLabel(status: ExternalService["status"]) {
  const labels = {
    configured: "configured",
    disabled: "disabled",
    missing_required: "needs fields",
    not_implemented: "planned"
  };

  return labels[status];
}

function getExternalServiceShortName(serviceId: ExternalService["id"]) {
  const labels = {
    yandex_cloud: "Yandex Cloud credentials",
    yandex_metrica: "Yandex Metrica",
    yandex_search_serp: "Yandex SERP",
    yandex_webmaster: "Yandex Webmaster",
    yandex_wordstat: "Yandex Wordstat"
  };

  return labels[serviceId];
}

function getExternalMissingFieldLabel(fieldId: string) {
  const labels: Record<string, string> = {
    apiKey: "API key",
    folderId: "folderId",
    iamToken: "IAM token",
    yandex_cloud: "Yandex Cloud credentials"
  };

  return labels[fieldId] ?? fieldId;
}

function buildExternalServiceDraft(service: ExternalService): ExternalServiceDraft {
  return {
    enabled: service.enabled,
    mode: service.mode,
    publicConfig: { ...service.publicConfig },
    secrets: Object.fromEntries(service.secretFields.map((field) => [field.id, ""]))
  };
}

function buildExternalServiceDrafts(settings: ExternalServiceSettings) {
  return Object.fromEntries(settings.services.map((service) => [service.id, buildExternalServiceDraft(service)]));
}

function getVisiblePublicFields(service: ExternalService, draft: ExternalServiceDraft | undefined) {
  if (service.id === "yandex_wordstat" && draft?.mode === "mcp_kv") {
    return service.publicFields.filter((field) => field.id === "mcpEndpoint");
  }

  if (service.id === "yandex_wordstat") {
    return service.publicFields.filter((field) => field.id !== "mcpEndpoint");
  }

  return service.publicFields;
}

function getVisibleSecretFields(service: ExternalService, draft: ExternalServiceDraft | undefined) {
  if (service.id === "yandex_cloud") {
    if (draft?.mode === "api_key") {
      return service.secretFields.filter((field) => field.id === "apiKey");
    }

    if (draft?.mode === "iam_token") {
      return service.secretFields.filter((field) => field.id === "iamToken");
    }

    return [];
  }

  if (service.id === "yandex_wordstat" && draft?.mode === "mcp_kv") {
    return [];
  }

  return service.secretFields;
}

function getExternalServiceDependencies(service: ExternalService, draft: ExternalServiceDraft | undefined) {
  const mode = draft?.mode ?? service.mode;

  if ((service.id === "yandex_wordstat" || service.id === "yandex_search_serp") && mode === "yandex_search_api") {
    return ["yandex_cloud"] satisfies ExternalService["id"][];
  }

  return service.dependsOn;
}

function getLandingOpportunityPageTypeLabel(
  pageType: NonNullable<SemanticAnalysisRun["landingOpportunities"]>[number]["pageType"]
) {
  const labels = {
    conversion: "conversion",
    deployment: "deployment",
    docs: "docs",
    home: "home",
    integration: "integration",
    landing: "landing",
    module: "module",
    security: "security",
    use_case: "use-case"
  };

  return labels[pageType];
}

function getSemanticScopeLabel(
  scope: SemanticAnalysisRun["pages"][number]["scope"] | SemanticAnalysisRun["clusters"][number]["sourceRefs"][number]["scope"]
) {
  const labels = {
    admin_page: "админка",
    content_source: "контент",
    indexable_page: "посадочная",
    technical_page: "техничка",
    template_block: "шаблон"
  };

  return labels[scope];
}

function getSemanticAnalysisMessage(analysis: SemanticAnalysisRun) {
  const legacyText =
    analysis.summary.legacyFindings > 0
      ? `Legacy-блокеров: ${analysis.summary.legacyFindings}.`
      : "Legacy-упоминаний не найдено.";
  const lexicalText =
    typeof analysis.summary.lexicalCoverageScore === "number"
      ? ` Lexical: ${analysis.summary.lexicalCoverageScore}%, evidence: ${analysis.summary.evidenceScore ?? analysis.summary.coverageScore}%.`
      : "";

  return `Dev readiness: ${analysis.summary.coverageScore}%.${lexicalText} Посадочных: ${analysis.pageScope.indexablePages}, исключено HTML: ${analysis.pageScope.excludedDocuments}. ${legacyText}`;
}

function getSemanticRunDate(analysis: SemanticAnalysisRun | null | undefined) {
  if (!analysis) {
    return "Анализ ещё не запускался.";
  }

  return `Run ${analysis.runId.slice(0, 8)} · ${formatDate(analysis.generatedAt)}`;
}

function getAuditIssueExplanation(code: string) {
  const explanations: Record<string, { why: string; action: string }> = {
    duplicate_alt: {
      why: "Одинаковые alt у разных изображений хуже объясняют, что именно показано на каждом медиа.",
      action: "Проверить изображения: если они разные, дать каждому свой описательный alt; если декоративные, позже пометить как decorative."
    },
    duplicate_description: {
      why: "Одинаковые description у разных страниц хуже объясняют Яндексу и пользователю, чем эти страницы отличаются.",
      action: "Сделать description уникальным для каждой важной страницы: коротко описать именно её смысл, раздел или оффер."
    },
    duplicate_title: {
      why: "Одинаковые title мешают отличать страницы друг от друга в поиске и в дальнейшем усложняют карту ключей.",
      action: "Развести title по назначению страниц: главная, раздел, шаблон, услуга или конкретная сущность."
    },
    heading_level_jump: {
      why: "Скачки в структуре заголовков могут путать смысловую иерархию страницы.",
      action: "Проверить порядок H1-H3 и убедиться, что заголовки идут как структура, а не только как визуальный стиль."
    },
    long_description: {
      why: "Description может использоваться Яндексом для описания страницы в сниппете, но слишком длинный текст легко теряет фокус.",
      action: "Сжать описание до короткого релевантного summary страницы, без набивки ключами."
    },
    long_title: {
      why: "Title помогает сформировать заголовок документа в поиске Яндекса; слишком длинный title может быть нечитабельным и хуже объяснять страницу.",
      action: "Сделать title короче и яснее: главный смысл страницы + бренд/уточнение, если нужно."
    },
    missing_alt: {
      why: "ALT помогает Яндексу и пользователям понять, что изображено на картинке, особенно если изображение важно для смысла страницы.",
      action: "Добавить естественный alt, который описывает изображение в контексте блока. Не набивать ключами."
    },
    missing_description: {
      why: "Meta description может участвовать в описании страницы в сниппете Яндекса и помогает пользователю понять, о чём страница.",
      action: "Добавить короткое описание страницы как мини-питч: что это, для кого, почему релевантно."
    },
    missing_h1: {
      why: "Главный заголовок и структура H1-H6 помогают пользователю и поиску понять основной смысл страницы.",
      action: "Проверить, есть ли видимый главный заголовок. Если он есть нестандартной разметкой, позже можно будет вручную подтвердить."
    },
    multiple_h1: {
      why: "Несколько H1 не всегда критично, но часто размывает главный смысл страницы.",
      action: "Оставить один главный H1, остальные крупные заголовки при необходимости перевести в H2/H3."
    },
    short_description: {
      why: "Короткий description может быть недостаточно информативным для сниппета.",
      action: "Добавить больше конкретики: продукт, ценность, аудитория или действие."
    },
    short_title: {
      why: "Короткий title может быть слишком общим и плохо объяснять релевантность страницы.",
      action: "Добавить конкретный смысл страницы, не превращая title в длинную простыню."
    },
    thin_visible_text: {
      why: "Мало видимого текста может затруднять понимание страницы и сбор смыслов для дальнейших SEO-этапов.",
      action: "Проверить, не скрыт ли основной контент скриптами/шаблонами; позже рабочая зона покажет текстовые блоки подробнее."
    },
    video_without_poster: {
      why: "Poster помогает видео выглядеть понятнее до воспроизведения и даёт контекст пользователю.",
      action: "Добавить poster или убедиться, что видео декоративное и не требует SEO-контекста."
    },
    weak_alt: {
      why: "Слишком короткий alt часто не описывает изображение достаточно полезно.",
      action: "Заменить на короткое, но осмысленное описание изображения в контексте секции."
    },
    weak_filename: {
      why: "Осмысленное имя файла может быть дополнительным контекстом для изображения.",
      action: "Позже в Media SEO предложим rename plan и обновление ссылок, но применять это будем только через diff и Apply."
    }
  };

  return (
    explanations[code] ?? {
      why: "Это базовая эвристика аудита, которую стоит проверить перед SEO-правками.",
      action: "Открыть страницу в рабочей зоне и решить: исправлять, игнорировать или отложить."
    }
  );
}

function getAuditScopeLabel(issue: NonNullable<ProjectScanSummary["audit"]>["issues"][number]) {
  const scopeLabels = {
    media: "медиа",
    page: "страница",
    project: "проект"
  };

  return `${scopeLabels[issue.scope]}${issue.field ? ` · ${issue.field}` : ""}`;
}

type AuditIssue = NonNullable<ProjectScanSummary["audit"]>["issues"][number];
type AuditSeverityFilter = "all" | AuditIssue["severity"];

type AuditIssuePageGroup = {
  key: string;
  title: string;
  subtitle: string;
  issues: AuditIssue[];
};

function getAuditSeverityFilterLabel(filter: AuditSeverityFilter) {
  const labels = {
    all: "Все",
    error: "Блокеры",
    info: "Инфо",
    warning: "Предупреждения"
  };

  return labels[filter];
}

function filterAuditIssuesBySeverity(issues: AuditIssue[], filter: AuditSeverityFilter) {
  return filter === "all" ? issues : issues.filter((issue) => issue.severity === filter);
}

function getSelectedAuditIssues(scan: ProjectScanSummary, issues: AuditIssue[]) {
  const selectedPageOrder = new Map(
    scan.pages.filter((page) => page.selected).map((page, index) => [page.pageId, index])
  );
  const originalIssueOrder = new Map(issues.map((issue, index) => [issue.id, index]));

  return issues
    .filter((issue) => issue.pageId && selectedPageOrder.has(issue.pageId))
    .sort((leftIssue, rightIssue) => {
      const leftPageOrder = selectedPageOrder.get(leftIssue.pageId ?? "") ?? 0;
      const rightPageOrder = selectedPageOrder.get(rightIssue.pageId ?? "") ?? 0;

      return leftPageOrder - rightPageOrder || (originalIssueOrder.get(leftIssue.id) ?? 0) - (originalIssueOrder.get(rightIssue.id) ?? 0);
    });
}

function buildSelectedAuditIssueGroups(scan: ProjectScanSummary, issues: AuditIssue[]): AuditIssuePageGroup[] {
  const issuesByPageId = new Map<string, AuditIssue[]>();

  for (const issue of issues) {
    if (!issue.pageId) {
      continue;
    }

    issuesByPageId.set(issue.pageId, [...(issuesByPageId.get(issue.pageId) ?? []), issue]);
  }

  return scan.pages
    .filter((page) => page.selected)
    .map((page) => ({
      key: page.pageId,
      title: getPageTitle(page),
      subtitle: getPagePathLabel(page),
      issues: issuesByPageId.get(page.pageId) ?? []
    }))
    .filter((group) => group.issues.length > 0);
}

function getPageTreeGroups(pages: ProjectScanSummary["pages"]) {
  const groupDefinitions: Array<{
    key: ProjectScanSummary["pages"][number]["scope"];
    name: string;
    description: string;
    defaultOpen: boolean;
  }> = [
    {
      key: "indexable_page",
      name: "Публичные посадочные",
      description: "Идут в SEO-аудит, рабочую зону, семантику и Wordstat.",
      defaultOpen: true
    },
    {
      key: "admin_page",
      name: "Исключено · админка",
      description: "Не участвует в органической выдаче.",
      defaultOpen: false
    },
    {
      key: "template_block",
      name: "Исключено · шаблоны и блоки",
      description: "Нужны для диагностики сборки, но не являются отдельными посадочными.",
      defaultOpen: false
    },
    {
      key: "technical_page",
      name: "Исключено · технические HTML",
      description: "Внутренние заготовки и служебные страницы.",
      defaultOpen: false
    }
  ];

  return groupDefinitions
    .map((definition) => ({
      ...definition,
      pages: pages.filter((page) => page.scope === definition.key)
    }))
    .filter((group) => group.pages.length > 0);
}

function getSelectedPagesCount(scan: ProjectScanSummary) {
  return scan.pages.filter((page) => page.selected).length;
}

function getPageScopeId(page: ProjectScanSummary["pages"][number]) {
  return `page:${page.pageId}`;
}

function getSectionScopeId(section: ProjectScanSummary["siteSections"][number]) {
  return `section:${section.sectionId}`;
}

type WorkspaceScopeItem = {
  scopeId: string;
  kind: "page" | "section";
  pageId: string;
  sectionId?: string;
  title: string;
  pathLabel: string;
  sourcePath: string;
  previewSourcePath: string | null;
  previewTargetSelector: string | null;
  previewTargetIndex: number | null;
  previewKind: ProjectScanSummary["pages"][number]["previewKind"];
  order: number;
};

function getPageWorkspaceScopeItem(page: ProjectScanSummary["pages"][number], index: number): WorkspaceScopeItem {
  return {
    kind: "page",
    order: getWorkspaceScopeOrder(page, index),
    pageId: page.pageId,
    pathLabel: getPagePathLabel(page),
    previewKind: page.previewKind,
    previewSourcePath: page.previewSourcePath ?? page.sourcePath,
    previewTargetIndex: null,
    previewTargetSelector: page.previewTargetSelector,
    scopeId: getPageScopeId(page),
    sourcePath: page.sourcePath,
    title: getPageTitle(page)
  };
}

function getSectionWorkspaceScopeItem(
  section: ProjectScanSummary["siteSections"][number],
  fallbackOrder: number
): WorkspaceScopeItem | null {
  if (!section.pageId || !section.enabled || !section.selected) {
    return null;
  }

  if (section.previewKind === "none" && !section.previewSourcePath && !section.previewTargetSelector) {
    return null;
  }

  const indexSuffix = section.previewTargetIndex != null ? ` #${section.previewTargetIndex + 1}` : "";

  return {
    kind: "section",
    order: section.order ?? fallbackOrder,
    pageId: section.pageId,
    pathLabel: `${section.sourcePath} · секция главной${indexSuffix}`,
    previewKind: section.previewKind,
    previewSourcePath: section.previewSourcePath,
    previewTargetIndex: section.previewTargetIndex,
    previewTargetSelector: section.previewTargetSelector,
    scopeId: getSectionScopeId(section),
    sectionId: section.sectionId,
    sourcePath: section.sourcePath,
    title: section.title
  };
}

function getSelectedWorkspaceScopeItems(scan: ProjectScanSummary): WorkspaceScopeItem[] {
  const sectionPageIds = new Set(
    scan.siteSections
      .filter((section) => section.selected && section.enabled && section.pageId)
      .map((section) => section.pageId!)
  );
  const pageItems = scan.pages
    .map((page, index) => ({ index, page }))
    .filter(({ page }) => isWorkspaceScopePage(page))
    .filter(({ page }) => page.scope !== "template_block" || !sectionPageIds.has(page.pageId))
    .map(({ page, index }) => getPageWorkspaceScopeItem(page, index));
  const sectionItems = scan.siteSections
    .map((section, index) => getSectionWorkspaceScopeItem(section, index))
    .filter((item): item is WorkspaceScopeItem => Boolean(item));

  return [...pageItems, ...sectionItems].sort((left, right) => {
    if (left.kind !== right.kind) {
      if (left.kind === "page" && left.order === -1) return -1;
      if (right.kind === "page" && right.order === -1) return 1;
    }

    return left.order - right.order || left.title.localeCompare(right.title, "ru");
  });
}

function getSelectedWorkspaceIssueCount(scan: ProjectScanSummary | null | undefined, scopeItems: WorkspaceScopeItem[]) {
  if (!scan?.audit) {
    return 0;
  }

  const pageIds = new Set(scopeItems.map((item) => item.pageId));

  return scan.audit.issues.filter((issue) => issue.pageId && pageIds.has(issue.pageId)).length;
}

function getWorkspaceKey(projectId: string, scopeId: string) {
  return `${projectId}:${scopeId}`;
}

function getWorkspaceUiKeyFromWorkspaceKey(workspaceKey: string) {
  return workspaceKey.split(":")[0] ?? workspaceKey;
}

type WorkspaceSection = PageWorkspace["sections"][number];
type WorkspaceSectionTextMap = Record<string, string>;

type WorkspaceTarget = {
  id: string;
  number: number;
  sectionId: string;
  selector: string | null;
  fallbackSelector: string | null;
  markerSelectors: string[];
  markerFallbackSelector: string | null;
  kind: WorkspaceSection["kind"];
  title: string;
  subtitle: string;
  originalText: string;
  workingText: string;
  issues: AuditIssue[];
};

type WorkspacePreviewMode = "desktop" | "mobile";
type WorkspaceDrawerPanel = "pages" | "issues";

const WORKSPACE_DRAWER_ORDER: WorkspaceDrawerPanel[] = ["pages", "issues"];

const WORKSPACE_PREVIEW_MODES: Array<{ id: WorkspacePreviewMode; label: string }> = [
  { id: "desktop", label: "Desktop" },
  { id: "mobile", label: "Mobile" }
];

type PreviewMarker = {
  targetId: string;
  number: number;
  label: string;
  top: number;
  left: number;
};

function getInitialSectionTextMap(workspace: PageWorkspace): WorkspaceSectionTextMap {
  return Object.fromEntries(workspace.sections.map((section) => [section.id, section.workingText]));
}

function buildWorkingTextFromSections(workspace: PageWorkspace, sectionTexts: WorkspaceSectionTextMap | undefined) {
  return workspace.sections
    .map((section) => {
      const text = sectionTexts?.[section.id] ?? section.workingText;
      return `## ${section.title}\n${text.trim()}`;
    })
    .join("\n\n")
    .trim();
}

function getSectionKindLabel(kind: WorkspaceSection["kind"]) {
  const labels: Record<WorkspaceSection["kind"], string> = {
    content_block: "Блок страницы",
    heading_group: "Группа по заголовку",
    media: "Медиа",
    meta: "SEO-поле"
  };

  return labels[kind];
}

function normalizeIssueLookupText(value: string | null | undefined) {
  return (value ?? "").replace(/\s+/g, " ").trim().toLocaleLowerCase();
}

function getQuotedIssueText(message: string) {
  return /(?:\u00ab|")([^"\u00bb]+)(?:\u00bb|")/.exec(message)?.[1] ?? null;
}

function getWorkspaceFallbackSection(sections: WorkspaceSection[] = []) {
  return (
    sections.find((section) => section.kind === "content_block" && section.selector && section.sourceHint === "<main>") ??
    sections.find((section) => section.kind === "content_block" && section.selector && section.charCount > 200) ??
    sections.find((section) => section.kind === "content_block" && section.selector) ??
    sections.find((section) => section.kind === "heading_group" && section.selector) ??
    sections.find((section) => section.selector) ??
    sections[0] ??
    null
  );
}

function uniqueWorkspaceSections(sections: WorkspaceSection[]) {
  const seen = new Set<string>();

  return sections.filter((section) => {
    if (seen.has(section.id)) {
      return false;
    }

    seen.add(section.id);
    return true;
  });
}

function getFilenameFromHint(value: string | null | undefined) {
  const cleanValue = (value ?? "").split(/[?#]/)[0] ?? "";
  return cleanValue.split("/").filter(Boolean).at(-1) ?? "";
}

function getMediaSectionsByIssueSource(issue: AuditIssue, mediaSections: WorkspaceSection[]) {
  const normalizedMessage = normalizeIssueLookupText(issue.message);
  const exactMatches = mediaSections.filter((section) => {
    const normalizedSource = normalizeIssueLookupText(section.sourceHint);
    return normalizedSource && normalizedMessage.includes(normalizedSource);
  });
  const filenameMatches = mediaSections.filter((section) => {
    const filename = normalizeIssueLookupText(getFilenameFromHint(section.sourceHint));
    return filename && normalizedMessage.includes(filename);
  });

  return uniqueWorkspaceSections([...exactMatches, ...filenameMatches]);
}

function getIssueTargetSections(issue: AuditIssue, sections: WorkspaceSection[] = []) {
  if (issue.field === "title") {
    return sections.filter((section) => section.id === "seo-title");
  }

  if (issue.field === "description") {
    return sections.filter((section) => section.id === "seo-description");
  }

  if (issue.field === "h1" || issue.field === "headings") {
    const exactSections = sections.filter((section) => section.id === "seo-h1" || section.id === "heading-group-1");
    return exactSections.length > 0 ? exactSections : sections.filter((section) => section.kind === "heading_group").slice(0, 1);
  }

  if (issue.field === "content") {
    return sections.filter((section) => section.kind === "content_block" || section.kind === "heading_group").slice(0, 1);
  }

  if (issue.field === "alt" || issue.field === "poster" || issue.field === "filename") {
    const mediaSections = sections.filter((section) => section.kind === "media");
    const sourceMatches = getMediaSectionsByIssueSource(issue, mediaSections);
    const quotedAlt = normalizeIssueLookupText(getQuotedIssueText(issue.message));
    const altMatches = quotedAlt
      ? mediaSections.filter((section) => normalizeIssueLookupText(section.originalText) === quotedAlt)
      : [];

    if (issue.code === "missing_alt") {
      const emptySourceMatches = sourceMatches.filter((section) => normalizeIssueLookupText(section.originalText) === "");
      return emptySourceMatches.length > 0 ? uniqueWorkspaceSections([...emptySourceMatches, ...sourceMatches]) : sourceMatches;
    }

    if (issue.code === "duplicate_alt") {
      return altMatches;
    }

    return sourceMatches.length > 0 ? sourceMatches : altMatches;
  }

  return [];
}

function buildWorkspaceTargets(workspace: PageWorkspace, issues: AuditIssue[]): WorkspaceTarget[] {
  const fallbackSection = getWorkspaceFallbackSection(workspace.sections);

  return issues.map((issue, index) => {
    const targetSections = getIssueTargetSections(issue, workspace.sections);
    const sectionId = targetSections[0]?.id ?? fallbackSection?.id ?? workspace.sections[0]?.id ?? issue.id;
    const section = workspace.sections.find((item) => item.id === sectionId) ?? fallbackSection ?? workspace.sections[0];
    const fallbackSelector = fallbackSection?.selector ?? section?.selector ?? null;
    const markerSections = targetSections.length > 0 ? targetSections : (section ? [section] : []);
    const markerSelectors = Array.from(new Set(markerSections.flatMap((item) => item.selector ? [item.selector] : [])));
    const markerFallbackSelector = issue.scope === "media" && markerSelectors.length > 0 ? null : fallbackSelector;

    return {
      id: `issue-${issue.id}`,
      number: index + 1,
      sectionId,
      selector: section?.selector ?? fallbackSelector,
      fallbackSelector,
      markerSelectors,
      markerFallbackSelector,
      kind: section?.kind ?? "content_block",
      title: section?.title ?? getAuditScopeLabel(issue),
      subtitle: section?.sourceHint ?? getAuditScopeLabel(issue),
      originalText: section?.originalText ?? "",
      workingText: section?.workingText ?? "",
      issues: [issue]
    };
  });
}

function getWorkspaceFieldDomId(workspaceKey: string, targetId: string) {
  return `workspace-field-${workspaceKey}-${targetId}`.replace(/[^a-zA-Z0-9_-]/g, "-");
}

function getPageIssues(scan: ProjectScanSummary | null | undefined, pageId: string | null) {
  if (!scan?.audit || !pageId) {
    return [];
  }

  return scan.audit.issues.filter((issue) => issue.pageId === pageId);
}

function getPageAuditLine(scan: ProjectScanSummary, page: ProjectScanSummary["pages"][number]) {
  if (!scan.audit) {
    return "Базовый аудит ещё не готов.";
  }

  const pageIssues = getPageIssues(scan, page.pageId);
  const counts = getAuditIssueCounts(pageIssues);
  const mediaIssuesCount = pageIssues.filter((issue) => issue.scope === "media").length;

  if (counts.total === 0) {
    return "Замечаний по странице не найдено.";
  }

  return [
    counts.errors > 0 ? `Блокеры: ${counts.errors}` : null,
    counts.warnings > 0 ? `Предупреждения: ${counts.warnings}` : null,
    counts.info > 0 ? `Инфо: ${counts.info}` : null,
    mediaIssuesCount > 0 ? `Медиа: ${mediaIssuesCount}` : null
  ]
    .filter(Boolean)
    .join(" · ");
}

function getPageAuditPreview(scan: ProjectScanSummary, page: ProjectScanSummary["pages"][number]) {
  if (!scan.audit) {
    return "Идёт сбор title, description, H1 и медиа-полей.";
  }

  const pageIssues = getPageIssues(scan, page.pageId);
  return pageIssues[0]?.message ?? "Можно переходить к рабочей зоне страницы.";
}

type PickedFolderFile = {
  file: File;
  relativePath: string;
};

type PickedFolder = {
  rootName: string;
  files: PickedFolderFile[];
};

type DataTransferItemWithEntry = DataTransferItem & {
  webkitGetAsEntry?: () => FileSystemEntry | null;
};

type DirectoryPickerFileHandle = {
  kind: "file";
  name: string;
  getFile: () => Promise<File>;
};

type DirectoryPickerDirectoryHandle = {
  kind: "directory";
  name: string;
  values: () => AsyncIterable<DirectoryPickerFileHandle | DirectoryPickerDirectoryHandle>;
};

type WindowWithDirectoryPicker = Window & {
  showDirectoryPicker?: (options?: { mode?: "read" }) => Promise<DirectoryPickerDirectoryHandle>;
};

function formatBytes(bytes: number) {
  if (bytes < 1024) return `${bytes} Б`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} КБ`;
  return `${(bytes / (1024 * 1024)).toFixed(1)} МБ`;
}

function getFolderStats(folder: PickedFolder | null) {
  if (!folder) {
    return {
      fileCount: 0,
      totalBytes: 0
    };
  }

  return {
    fileCount: folder.files.length,
    totalBytes: folder.files.reduce((sum, item) => sum + item.file.size, 0)
  };
}

function isFileSystemEntry(entry: FileSystemEntry | null | undefined): entry is FileSystemEntry {
  return Boolean(entry);
}

function readDirectoryEntries(reader: FileSystemDirectoryReader): Promise<FileSystemEntry[]> {
  return new Promise((resolve, reject) => {
    const entries: FileSystemEntry[] = [];

    function readBatch() {
      reader.readEntries((batch) => {
        if (batch.length === 0) {
          resolve(entries);
          return;
        }

        entries.push(...batch);
        readBatch();
      }, reject);
    }

    readBatch();
  });
}

function readFileEntry(entry: FileSystemFileEntry): Promise<File> {
  return new Promise((resolve, reject) => {
    entry.file(resolve, reject);
  });
}

async function collectEntryFiles(entry: FileSystemEntry, prefix = ""): Promise<PickedFolderFile[]> {
  if (entry.isFile) {
    const file = await readFileEntry(entry as FileSystemFileEntry);
    return [
      {
        file,
        relativePath: `${prefix}${file.name}`
      }
    ];
  }

  if (!entry.isDirectory) {
    return [];
  }

  const directory = entry as FileSystemDirectoryEntry;
  const entries = await readDirectoryEntries(directory.createReader());
  const childFiles = await Promise.all(
    entries.map((childEntry) => collectEntryFiles(childEntry, `${prefix}${directory.name}/`))
  );

  return childFiles.flat();
}

async function collectDirectoryHandleFiles(
  directory: DirectoryPickerDirectoryHandle,
  prefix: string
): Promise<PickedFolderFile[]> {
  const childFiles: PickedFolderFile[] = [];

  for await (const handle of directory.values()) {
    if (handle.kind === "file") {
      const file = await handle.getFile();
      childFiles.push({
        file,
        relativePath: `${prefix}${file.name}`
      });
      continue;
    }

    childFiles.push(...(await collectDirectoryHandleFiles(handle, `${prefix}${handle.name}/`)));
  }

  return childFiles.sort((firstFile, secondFile) => firstFile.relativePath.localeCompare(secondFile.relativePath));
}

async function folderFromDirectoryPicker(directory: DirectoryPickerDirectoryHandle): Promise<PickedFolder> {
  return {
    rootName: directory.name,
    files: await collectDirectoryHandleFiles(directory, `${directory.name}/`)
  };
}

function folderFromInputFiles(files: FileList | null): PickedFolder | null {
  const pickedFiles = Array.from(files ?? []);

  if (pickedFiles.length === 0) {
    return null;
  }

  const firstRelativePath = pickedFiles[0]?.webkitRelativePath || pickedFiles[0]?.name || "browser-folder";
  const rootName = firstRelativePath.split(/[\\/]+/).filter(Boolean)[0] ?? "browser-folder";

  return {
    rootName,
    files: pickedFiles.map((file) => ({
      file,
      relativePath: file.webkitRelativePath || `${rootName}/${file.name}`
    }))
  };
}

async function folderFromDrop(event: DragEvent<HTMLElement>): Promise<PickedFolder | null> {
  const entries = Array.from(event.dataTransfer.items)
    .map((item) => (item as DataTransferItemWithEntry).webkitGetAsEntry?.())
    .filter(isFileSystemEntry);
  const directoryEntry = entries.find((entry) => entry.isDirectory);

  if (directoryEntry) {
    return {
      rootName: directoryEntry.name,
      files: await collectEntryFiles(directoryEntry)
    };
  }

  return folderFromInputFiles(event.dataTransfer.files);
}

function fileToBase64(file: File): Promise<string> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onerror = () => reject(new Error(`Не удалось прочитать файл ${file.name}.`));
    reader.onload = () => {
      const result = typeof reader.result === "string" ? reader.result : "";
      resolve(result.split(",", 2)[1] ?? "");
    };
    reader.readAsDataURL(file);
  });
}

async function serializeFolderFiles(folder: PickedFolder): Promise<BrowserFolderFileInput[]> {
  return Promise.all(
    folder.files.map(async ({ file, relativePath }) => ({
      relativePath,
      contentBase64: await fileToBase64(file),
      contentType: file.type || "application/octet-stream",
      size: file.size,
      lastModified: file.lastModified
    }))
  );
}

export function App() {
  const folderInputRef = useRef<HTMLInputElement | null>(null);
  const titleInputRef = useRef<HTMLInputElement | null>(null);
  const previewFrameRefs = useRef<Record<string, HTMLIFrameElement | null>>({});
  const renameCancelRef = useRef(false);
  const committingRenameProjectIdRef = useRef<string | null>(null);
  const autoScannedProjectIdsRef = useRef(new Set<string>());
  const [workspaceMode, setWorkspaceMode] = useState<WorkspaceMode>("dev");
  const [activeStageIndex, setActiveStageIndex] = useState(0);
  const [health, setHealth] = useState<HealthResponse | null>(null);
  const [healthError, setHealthError] = useState<string | null>(null);
  const [projects, setProjects] = useState<ProjectSummary[]>([]);
  const [activeProjectId, setActiveProjectId] = useState<string | null>(null);
  const [scanSummaries, setScanSummaries] = useState<Record<string, ProjectScanSummary | null>>({});
  const [semanticAnalyses, setSemanticAnalyses] = useState<Record<string, SemanticAnalysisRun | null>>({});
  const [marketEnrichments, setMarketEnrichments] = useState<Record<string, MarketEnrichmentContract | null>>({});
  const [activeSemanticClusterIds, setActiveSemanticClusterIds] = useState<Record<string, string>>({});
  const [projectsError, setProjectsError] = useState<string | null>(null);
  const [projectsLoading, setProjectsLoading] = useState(true);
  const [pickedFolder, setPickedFolder] = useState<PickedFolder | null>(null);
  const [createError, setCreateError] = useState<string | null>(null);
  const [createMessage, setCreateMessage] = useState<string | null>(null);
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [isFolderDragActive, setIsFolderDragActive] = useState(false);
  const [folderPickerMessage, setFolderPickerMessage] = useState<string | null>(null);
  const [editingProjectId, setEditingProjectId] = useState<string | null>(null);
  const [editingProjectName, setEditingProjectName] = useState("");
  const [busyProjectId, setBusyProjectId] = useState<string | null>(null);
  const [pendingDeleteProjectId, setPendingDeleteProjectId] = useState<string | null>(null);
  const [scanningProjectId, setScanningProjectId] = useState<string | null>(null);
  const [analyzingProjectId, setAnalyzingProjectId] = useState<string | null>(null);
  const [enrichingMarketProjectId, setEnrichingMarketProjectId] = useState<string | null>(null);
  const [exportingSemanticKey, setExportingSemanticKey] = useState<string | null>(null);
  const [selectingPagesProjectId, setSelectingPagesProjectId] = useState<string | null>(null);
  const [auditIssueLimits, setAuditIssueLimits] = useState<Record<string, number>>({});
  const [auditSeverityFilters, setAuditSeverityFilters] = useState<Record<string, AuditSeverityFilter>>({});
  const [openPageAuditDetails, setOpenPageAuditDetails] = useState<Record<string, boolean>>({});
  const [activeWorkspaceScopeIds, setActiveWorkspaceScopeIds] = useState<Record<string, string>>({});
  const [activeWorkspaceSectionIds, setActiveWorkspaceSectionIds] = useState<Record<string, string>>({});
  const [activeWorkspaceTargetIds, setActiveWorkspaceTargetIds] = useState<Record<string, string>>({});
  const [workspacePreviewMarkers, setWorkspacePreviewMarkers] = useState<Record<string, PreviewMarker[]>>({});
  const [workspaceDrawers, setWorkspaceDrawers] = useState<Record<string, WorkspaceDrawerPanel[]>>({});
  const [workspacePreviewScopeIds, setWorkspacePreviewScopeIds] = useState<Record<string, string>>({});
  const [pageWorkspaces, setPageWorkspaces] = useState<Record<string, PageWorkspace>>({});
  const [workspaceDraftTexts, setWorkspaceDraftTexts] = useState<Record<string, string>>({});
  const [workspaceSectionTexts, setWorkspaceSectionTexts] = useState<Record<string, WorkspaceSectionTextMap>>({});
  const [workspacePreviewModes, setWorkspacePreviewModes] = useState<Record<string, WorkspacePreviewMode>>({});
  const [busyWorkspaceKey, setBusyWorkspaceKey] = useState<string | null>(null);
  const [projectActionError, setProjectActionError] = useState<string | null>(null);
  const [projectActionMessage, setProjectActionMessage] = useState<string | null>(null);
  const [projectActionProjectId, setProjectActionProjectId] = useState<string | null>(null);
  const [isExternalServicesOpen, setIsExternalServicesOpen] = useState(false);
  const [externalServices, setExternalServices] = useState<ExternalServiceSettings | null>(null);
  const [externalServiceDrafts, setExternalServiceDrafts] = useState<Record<string, ExternalServiceDraft>>({});
  const [externalServicesLoading, setExternalServicesLoading] = useState(false);
  const [savingExternalServiceId, setSavingExternalServiceId] = useState<string | null>(null);
  const [externalServicesError, setExternalServicesError] = useState<string | null>(null);
  const [externalServicesMessage, setExternalServicesMessage] = useState<string | null>(null);

  async function loadScanSummaries(projectList: ProjectSummary[]) {
    const scanEntries = await Promise.all(
      projectList.map(async (project) => {
        try {
          const result = await fetchLatestProjectScan(project.id);
          return [project.id, result.scan] as const;
        } catch {
          return [project.id, null] as const;
        }
      })
    );

    setScanSummaries(Object.fromEntries(scanEntries));
  }

  async function loadSemanticAnalyses(projectList: ProjectSummary[]) {
    const analysisEntries = await Promise.all(
      projectList.map(async (project) => {
        try {
          const result = await fetchLatestSemanticAnalysis(project.id);
          return [project.id, result.analysis] as const;
        } catch {
          return [project.id, null] as const;
        }
      })
    );

    setSemanticAnalyses(Object.fromEntries(analysisEntries));
  }

  async function loadMarketEnrichments(projectList: ProjectSummary[]) {
    const marketEntries = await Promise.all(
      projectList.map(async (project) => {
        try {
          const result = await fetchLatestMarketEnrichment(project.id);
          return [project.id, result.marketEnrichment] as const;
        } catch {
          return [project.id, null] as const;
        }
      })
    );

    setMarketEnrichments(Object.fromEntries(marketEntries));
  }

  async function loadExternalServices() {
    setExternalServicesLoading(true);
    setExternalServicesError(null);
    setExternalServicesMessage(null);

    try {
      const result = await fetchExternalServiceSettings();

      setExternalServices(result.externalServices);
      setExternalServiceDrafts(buildExternalServiceDrafts(result.externalServices));
    } catch (error: unknown) {
      setExternalServicesError(getErrorMessage(error, "Не удалось загрузить внешние сервисы."));
    } finally {
      setExternalServicesLoading(false);
    }
  }

  function openExternalServicesModal() {
    setIsExternalServicesOpen(true);
    void loadExternalServices();
  }

  function updateExternalServiceDraft(serviceId: string, updater: (draft: ExternalServiceDraft) => ExternalServiceDraft) {
    setExternalServiceDrafts((currentDrafts) => {
      const currentDraft = currentDrafts[serviceId];

      if (!currentDraft) {
        return currentDrafts;
      }

      return {
        ...currentDrafts,
        [serviceId]: updater(currentDraft)
      };
    });
  }

  async function handleSaveExternalService(service: ExternalService) {
    const draft = externalServiceDrafts[service.id];

    if (!draft) {
      return;
    }

    setSavingExternalServiceId(service.id);
    setExternalServicesError(null);
    setExternalServicesMessage(null);

    try {
      const secrets = Object.fromEntries(
        Object.entries(draft.secrets ?? {}).filter(([, value]) => value.trim().length > 0)
      );
      const result = await updateExternalServiceSettings(service.id, {
        enabled: draft.enabled,
        mode: draft.mode,
        publicConfig: draft.publicConfig,
        secrets
      });

      setExternalServices((currentSettings) =>
        currentSettings
          ? {
              ...currentSettings,
              services: currentSettings.services.map((currentService) =>
                currentService.id === service.id ? result.service : currentService
              )
            }
          : currentSettings
      );
      setExternalServiceDrafts((currentDrafts) => ({
        ...currentDrafts,
        [service.id]: buildExternalServiceDraft(result.service)
      }));
      setExternalServicesMessage(`${result.service.label}: настройки сохранены.`);

      if (service.id === "yandex_wordstat") {
        await loadMarketEnrichments(projects);
      }
    } catch (error: unknown) {
      setExternalServicesError(getErrorMessage(error, "Не удалось сохранить внешний сервис."));
    } finally {
      setSavingExternalServiceId(null);
    }
  }

  async function loadProjects() {
    setProjectsLoading(true);
    setProjectsError(null);

    try {
      const result = await fetchProjects();
      setProjects(result.projects);
      await Promise.all([loadScanSummaries(result.projects), loadSemanticAnalyses(result.projects), loadMarketEnrichments(result.projects)]);
    } catch (error: unknown) {
      setProjectsError(getErrorMessage(error, "Не удалось загрузить проекты."));
    } finally {
      setProjectsLoading(false);
    }
  }

  useEffect(() => {
    let cancelled = false;

    fetchHealth()
      .then((result) => {
        if (!cancelled) {
          setHealth(result);
          setHealthError(null);
        }
      })
      .catch((error: unknown) => {
        if (!cancelled) {
          setHealth(null);
          setHealthError(getErrorMessage(error, "Неизвестная ошибка"));
        }
      });

    return () => {
      cancelled = true;
    };
  }, []);

  useEffect(() => {
    void loadProjects();
  }, []);

  useEffect(() => {
    if (activeStageIndex !== 2) {
      return;
    }

    const scrollToWorkspace = () => {
      document.querySelector(".page-workspace-panel")?.scrollIntoView({ behavior: "smooth", block: "start" });
    };
    const firstTimer = window.setTimeout(scrollToWorkspace, 120);
    const secondTimer = window.setTimeout(scrollToWorkspace, 650);

    return () => {
      window.clearTimeout(firstTimer);
      window.clearTimeout(secondTimer);
    };
  }, [activeProjectId, activeStageIndex]);

  useEffect(() => {
    const timers: number[] = [];
    const projectId = activeProjectId ?? "";
    const scanSummary = projectId ? scanSummaries[projectId] : null;

    if (activeStageIndex !== 2 || !projectId || !scanSummary) {
      return () => undefined;
    }

    const scopeItems = getSelectedWorkspaceScopeItems(scanSummary);
    const activeScopeId = activeWorkspaceScopeIds[projectId] ?? scopeItems[0]?.scopeId ?? "";
    const activeScopeItem = scopeItems.find((item) => item.scopeId === activeScopeId) ?? scopeItems[0] ?? null;
    const workspaceKey = activeScopeItem ? getWorkspaceKey(projectId, activeScopeItem.scopeId) : "";
    const workspace = workspaceKey ? pageWorkspaces[workspaceKey] : null;

    if (!workspaceKey || !workspace) {
      return () => undefined;
    }

    const issues = getPageIssues(scanSummary, activeScopeItem?.pageId ?? null);
    const targets = buildWorkspaceTargets(workspace, issues);

    timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopeItems, activeScopeItem), 80));
    timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopeItems, activeScopeItem), 360));
    timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopeItems, activeScopeItem), 1000));

    return () => {
      timers.forEach((timer) => window.clearTimeout(timer));
    };
  }, [activeProjectId, activeStageIndex, activeWorkspaceScopeIds, pageWorkspaces, scanSummaries, workspacePreviewModes]);

  function scrollWorkspaceFieldIntoView(workspaceKey: string, targetId: string, attempt = 0) {
    window.requestAnimationFrame(() => {
      const element = document.getElementById(getWorkspaceFieldDomId(workspaceKey, targetId));

      if (element) {
        element.scrollIntoView({ behavior: "smooth", block: "center" });
        return;
      }

      if (attempt < 8) {
        scrollWorkspaceFieldIntoView(workspaceKey, targetId, attempt + 1);
      }
    });
  }

  useEffect(() => {
    function handlePreviewMessage(event: MessageEvent) {
      const data = event.data;

      if (!data || typeof data !== "object") {
        return;
      }

      if (data.type !== "seo-mode:preview-markers") {
        if (data?.type === "seo-mode:active-scope-page") {
          const sourceWorkspaceKey = typeof data.workspaceKey === "string" ? data.workspaceKey : "";
          const pageId = typeof data.pageId === "string" ? data.pageId : "";
          const scopeId =
            typeof data.scopeId === "string" && data.scopeId.length > 0
              ? data.scopeId
              : pageId
                ? `page:${pageId}`
                : "";
          const [projectId = ""] = sourceWorkspaceKey.split(":");

          if (!projectId || !scopeId) {
            return;
          }

          const nextWorkspaceKey = getWorkspaceKey(projectId, scopeId);

          if (!pageWorkspaces[nextWorkspaceKey]) {
            const project = projects.find((item) => item.id === projectId);
            const scanSummary = scanSummaries[projectId];
            const scopeItem = scanSummary ? getSelectedWorkspaceScopeItems(scanSummary).find((item) => item.scopeId === scopeId) : null;

            if (project && scopeItem && busyWorkspaceKey !== nextWorkspaceKey) {
              void handleOpenPageWorkspace(project, scopeItem, {
                deferActiveUntilLoaded: true,
                navigatePreview: false,
                silent: true
              });
            }

            return;
          }

          setActiveWorkspaceScopeIds((currentScopeIds) => {
            if (currentScopeIds[projectId] === scopeId) {
              return currentScopeIds;
            }

            return {
              ...currentScopeIds,
              [projectId]: scopeId
            };
          });
          return;
        }

        if (data?.type !== "seo-mode:activate-target") {
          return;
        }

        const workspaceKey = typeof data.workspaceKey === "string" ? data.workspaceKey : "";
        const targetId = typeof data.targetId === "string" ? data.targetId : "";
        const sectionId = typeof data.sectionId === "string" ? data.sectionId : "";

        if (!workspaceKey || !targetId) {
          return;
        }

        setActiveWorkspaceTargetIds((currentTargetIds) => ({
          ...currentTargetIds,
          [workspaceKey]: targetId
        }));
        setActiveWorkspaceSectionIds((currentSectionIds) => ({
          ...currentSectionIds,
          [workspaceKey]: sectionId || targetId
        }));
        openWorkspaceDrawer(getWorkspaceUiKeyFromWorkspaceKey(workspaceKey), "issues");

        scrollWorkspaceFieldIntoView(workspaceKey, targetId);
        return;
      }

      const workspaceKey = typeof data.workspaceKey === "string" ? data.workspaceKey : "";

      if (!workspaceKey || !Array.isArray(data.markers)) {
        return;
      }

      const markers: PreviewMarker[] = data.markers.flatMap((marker: unknown) => {
        if (!marker || typeof marker !== "object") {
          return [];
        }

        const markerRecord = marker as Record<string, unknown>;

        if (
          typeof markerRecord.targetId !== "string" ||
          typeof markerRecord.label !== "string" ||
          typeof markerRecord.number !== "number" ||
          typeof markerRecord.top !== "number" ||
          typeof markerRecord.left !== "number"
        ) {
          return [];
        }

        return [
          {
            targetId: markerRecord.targetId,
            number: markerRecord.number,
            label: markerRecord.label,
            top: markerRecord.top,
            left: markerRecord.left
          }
        ];
      });

      setWorkspacePreviewMarkers((currentMarkers) => ({
        ...currentMarkers,
        [workspaceKey]: markers
      }));
    }

    window.addEventListener("message", handlePreviewMessage);

    return () => {
      window.removeEventListener("message", handlePreviewMessage);
    };
  }, [busyWorkspaceKey, pageWorkspaces, projects, scanSummaries]);

  useEffect(() => {
    if (projects.length === 0) {
      setActiveProjectId(null);
      return;
    }

    if (activeProjectId && projects.some((project) => project.id === activeProjectId)) {
      return;
    }

    setActiveProjectId(projects[0].id);
  }, [activeProjectId, projects]);

  useEffect(() => {
    folderInputRef.current?.setAttribute("webkitdirectory", "");
    folderInputRef.current?.setAttribute("directory", "");
  }, []);

  useEffect(() => {
    if (!createMessage) {
      return;
    }

    const timeoutId = window.setTimeout(() => {
      setCreateMessage(null);
    }, NOTICE_AUTO_HIDE_MS);

    return () => {
      window.clearTimeout(timeoutId);
    };
  }, [createMessage]);

  useEffect(() => {
    if (!projectActionMessage) {
      return;
    }

    const timeoutId = window.setTimeout(() => {
      setProjectActionMessage(null);
      setProjectActionProjectId(null);
    }, NOTICE_AUTO_HIDE_MS);

    return () => {
      window.clearTimeout(timeoutId);
    };
  }, [projectActionMessage]);

  useEffect(() => {
    if (!editingProjectId) {
      return;
    }

    titleInputRef.current?.focus();
    titleInputRef.current?.select();
  }, [editingProjectId]);

  function applyPickedFolder(folder: PickedFolder | null) {
    setFolderPickerMessage(null);
    setCreateError(null);

    if (!folder || folder.files.length === 0) {
      setFolderPickerMessage("В выбранной папке нет файлов для импорта.");
      return;
    }

    const stats = getFolderStats(folder);

    setPickedFolder(folder);
    setFolderPickerMessage(
      `Выбрана папка «${folder.rootName}»: ${stats.fileCount} файлов, ${formatBytes(stats.totalBytes)}.`
    );
  }

  function clearPickedFolder() {
    setPickedFolder(null);
    setFolderPickerMessage(null);
    setCreateError(null);

    if (folderInputRef.current) {
      folderInputRef.current.value = "";
    }
  }

  async function handlePickFolder() {
    const showDirectoryPicker = (window as WindowWithDirectoryPicker).showDirectoryPicker;

    if (!showDirectoryPicker) {
      folderInputRef.current?.click();
      return;
    }

    setFolderPickerMessage(null);
    setCreateError(null);

    try {
      const directory = await showDirectoryPicker({ mode: "read" });
      applyPickedFolder(await folderFromDirectoryPicker(directory));
    } catch (error: unknown) {
      if (error instanceof DOMException && error.name === "AbortError") {
        return;
      }

      setFolderPickerMessage(getErrorMessage(error, "Не удалось прочитать выбранную папку."));
    }
  }

  function handleFolderInputChange(event: ChangeEvent<HTMLInputElement>) {
    applyPickedFolder(folderFromInputFiles(event.target.files));
    event.target.value = "";
  }

  function handleFolderDragOver(event: DragEvent<HTMLElement>) {
    event.preventDefault();
    event.dataTransfer.dropEffect = "copy";
    setIsFolderDragActive(true);
  }

  function handleFolderDragLeave() {
    setIsFolderDragActive(false);
  }

  async function handleFolderDrop(event: DragEvent<HTMLElement>) {
    event.preventDefault();
    setIsFolderDragActive(false);

    try {
      applyPickedFolder(await folderFromDrop(event));
    } catch (error: unknown) {
      setFolderPickerMessage(getErrorMessage(error, "Не удалось прочитать перетащенную папку."));
    }
  }

  async function handleCreateProject(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setCreateError(null);
    setCreateMessage(null);

    if (!pickedFolder) {
      setCreateError("Выбери папку сайта или перетащи её в область импорта.");
      return;
    }

    setIsSubmitting(true);

    try {
      const result = await importBrowserFolder({
        name: pickedFolder.rootName,
        rootName: pickedFolder.rootName,
        files: await serializeFolderFiles(pickedFolder)
      });

      setPickedFolder(null);
      setFolderPickerMessage(null);
      setActiveProjectId(result.project.id);
      setProjects((currentProjects) => [
        result.project,
        ...currentProjects.filter((currentProject) => currentProject.id !== result.project.id)
      ]);
      setScanSummaries((currentSummaries) => ({
        ...currentSummaries,
        [result.project.id]: null
      }));
      setMarketEnrichments((currentEnrichments) => ({
        ...currentEnrichments,
        [result.project.id]: null
      }));
      setCreateMessage(
        `Проект «${result.project.name}» создан, папка импортирована в хранилище.\nПервичный скан запущен автоматически.`
      );
      await handleProjectScan(result.project, { hideSuccessMessage: true });
      await loadProjects();
    } catch (error: unknown) {
      setCreateError(getErrorMessage(error, "Не удалось создать проект."));
    } finally {
      setIsSubmitting(false);
    }
  }

  function startProjectRename(project: ProjectSummary) {
    renameCancelRef.current = false;
    setEditingProjectId(project.id);
    setEditingProjectName(project.name);
    setProjectActionError(null);
    setProjectActionMessage(null);
    setProjectActionProjectId(null);
  }

  function cancelProjectRename() {
    renameCancelRef.current = true;
    setEditingProjectId(null);
    setEditingProjectName("");
  }

  async function commitProjectRename(project: ProjectSummary) {
    if (renameCancelRef.current) {
      renameCancelRef.current = false;
      return;
    }

    if (editingProjectId !== project.id || committingRenameProjectIdRef.current === project.id) {
      return;
    }

    const nextProjectName = editingProjectName.trim();

    if (nextProjectName === project.name) {
      setEditingProjectId(null);
      setEditingProjectName("");
      return;
    }

    if (nextProjectName.length === 0) {
      setProjectActionError("Название проекта не должно быть пустым.");
      setProjectActionMessage(null);
      setProjectActionProjectId(project.id);
      titleInputRef.current?.focus();
      return;
    }

    if (hasProjectNameConflict(nextProjectName, project.id)) {
      setProjectActionError("Проект с таким названием уже есть.");
      setProjectActionMessage(null);
      setProjectActionProjectId(project.id);
      titleInputRef.current?.focus();
      return;
    }

    committingRenameProjectIdRef.current = project.id;
    setBusyProjectId(project.id);
    setProjectActionError(null);
    setProjectActionMessage(null);
    setProjectActionProjectId(project.id);

    try {
      const result = await renameProject(project.id, nextProjectName);

      setProjects((currentProjects) =>
        currentProjects.map((currentProject) => (currentProject.id === project.id ? result.project : currentProject))
      );
      setEditingProjectId(null);
      setEditingProjectName("");
      setProjectActionMessage(`Проект переименован в «${result.project.name}».`);
    } catch (error: unknown) {
      setProjectActionError(getErrorMessage(error, "Не удалось переименовать проект."));
      titleInputRef.current?.focus();
    } finally {
      committingRenameProjectIdRef.current = null;
      setBusyProjectId(null);
    }
  }

  useEffect(() => {
    if (!editingProjectId) {
      return;
    }

    function handleDocumentPointerDown(event: PointerEvent) {
      const target = event.target;

      if (!(target instanceof Node) || titleInputRef.current?.contains(target)) {
        return;
      }

      const project = projects.find((currentProject) => currentProject.id === editingProjectId);

      if (project) {
        void commitProjectRename(project);
      }
    }

    document.addEventListener("pointerdown", handleDocumentPointerDown);

    return () => {
      document.removeEventListener("pointerdown", handleDocumentPointerDown);
    };
  }, [editingProjectId, editingProjectName, projects]);

  async function handleProjectCopy(project: ProjectSummary) {
    setBusyProjectId(project.id);
    setProjectActionError(null);
    setProjectActionMessage(null);
    setProjectActionProjectId(project.id);

    try {
      const result = await copyProject(project.id);

      setProjects((currentProjects) => [result.project, ...currentProjects]);
      setScanSummaries((currentSummaries) => ({
        ...currentSummaries,
        [result.project.id]: null
      }));
      setSemanticAnalyses((currentAnalyses) => ({
        ...currentAnalyses,
        [result.project.id]: null
      }));
      setMarketEnrichments((currentEnrichments) => ({
        ...currentEnrichments,
        [result.project.id]: null
      }));
      setProjectActionProjectId(result.project.id);
      setProjectActionMessage(`Создана копия «${result.project.name}».`);
    } catch (error: unknown) {
      setProjectActionError(getErrorMessage(error, "Не удалось скопировать проект."));
    } finally {
      setBusyProjectId(null);
    }
  }

  async function handleProjectScan(project: ProjectSummary, options: { hideSuccessMessage?: boolean } = {}) {
    setScanningProjectId(project.id);
    setProjectActionError(null);
    setProjectActionMessage(null);
    setProjectActionProjectId(project.id);

    try {
      const result = await runProjectScan(project.id);

      setScanSummaries((currentSummaries) => ({
        ...currentSummaries,
        [project.id]: result.scan
      }));
      setSemanticAnalyses((currentAnalyses) => ({
        ...currentAnalyses,
        [project.id]: null
      }));
      setMarketEnrichments((currentEnrichments) => ({
        ...currentEnrichments,
        [project.id]: null
      }));
      if (!options.hideSuccessMessage) {
        setProjectActionMessage(getProjectScanActionMessage(result.scan));
      }
    } catch (error: unknown) {
      setProjectActionError(getErrorMessage(error, "Не удалось просканировать проект."));
    } finally {
      setScanningProjectId(null);
    }
  }

  async function handleSemanticAnalysis(project: ProjectSummary) {
    setAnalyzingProjectId(project.id);
    setProjectActionError(null);
    setProjectActionMessage(null);
    setProjectActionProjectId(project.id);

    try {
      const result = await runSemanticAnalysis(project.id);

      setSemanticAnalyses((currentAnalyses) => ({
        ...currentAnalyses,
        [project.id]: result.analysis
      }));
      setActiveSemanticClusterIds((currentIds) => ({
        ...currentIds,
        [project.id]: result.analysis.clusters[0]?.id ?? ""
      }));
      const marketResult = await fetchLatestMarketEnrichment(project.id);
      setMarketEnrichments((currentEnrichments) => ({
        ...currentEnrichments,
        [project.id]: marketResult.marketEnrichment
      }));
      setProjectActionMessage(getSemanticAnalysisMessage(result.analysis));
    } catch (error: unknown) {
      setProjectActionError(getErrorMessage(error, "Не удалось выполнить semantic analysis."));
    } finally {
      setAnalyzingProjectId(null);
    }
  }

  async function handleSemanticExport(project: ProjectSummary, format: SemanticExportFormat) {
    const exportKey = `${project.id}:${format}`;
    setExportingSemanticKey(exportKey);
    setProjectActionError(null);
    setProjectActionMessage(null);
    setProjectActionProjectId(project.id);

    try {
      const result = await downloadSemanticAnalysisExport(project.id, format);
      const url = URL.createObjectURL(result.blob);
      const link = document.createElement("a");
      link.href = url;
      link.download = result.filename;
      document.body.append(link);
      link.click();
      link.remove();
      URL.revokeObjectURL(url);
      setProjectActionMessage(`Semantic export выгружен: ${result.filename}`);
    } catch (error: unknown) {
      setProjectActionError(getErrorMessage(error, "Не удалось выгрузить semantic export."));
    } finally {
      setExportingSemanticKey(null);
    }
  }

  async function handleMarketEnrichment(project: ProjectSummary) {
    setEnrichingMarketProjectId(project.id);
    setProjectActionError(null);
    setProjectActionMessage(null);
    setProjectActionProjectId(project.id);

    try {
      const result = await runMarketEnrichment(project.id);

      setMarketEnrichments((currentEnrichments) => ({
        ...currentEnrichments,
        [project.id]: result.marketEnrichment
      }));
      setProjectActionMessage(
        result.marketEnrichment.state === "not_configured"
          ? "Market enrichment готов к запуску, но платформенный provider ещё не настроен."
          : `Market enrichment: ${getMarketStateLabel(result.marketEnrichment.state)}.`
      );
    } catch (error: unknown) {
      setProjectActionError(getErrorMessage(error, "Не удалось выполнить market enrichment."));
    } finally {
      setEnrichingMarketProjectId(null);
    }
  }

  async function handleProjectPageSelection(project: ProjectSummary, pageIds: string[], selected: boolean) {
    if (pageIds.length === 0) {
      return;
    }

    setSelectingPagesProjectId(project.id);
    setProjectActionError(null);
    setProjectActionMessage(null);
    setProjectActionProjectId(project.id);

    try {
      const result = await updateProjectPageSelection(project.id, pageIds, selected);

      if (result.scan) {
        setScanSummaries((currentSummaries) => ({
          ...currentSummaries,
          [project.id]: result.scan
        }));
        setProjectActionMessage(`Выбрано страниц: ${getSelectedPagesCount(result.scan)} из ${result.scan.counts.pages}.`);
      }
    } catch (error: unknown) {
      setProjectActionError(getErrorMessage(error, "Не удалось обновить выбор страниц."));
    } finally {
      setSelectingPagesProjectId(null);
    }
  }

  async function handleApplyRecommendedPageScope(project: ProjectSummary, scan: ProjectScanSummary) {
    const indexablePageIds = scan.pages.filter(isIndexableScanPage).map((page) => page.pageId);
    const selectedExcludedPageIds = scan.pages
      .filter((page) => page.selected && !isIndexableScanPage(page))
      .map((page) => page.pageId);

    if (indexablePageIds.length === 0 && selectedExcludedPageIds.length === 0) {
      return;
    }

    setSelectingPagesProjectId(project.id);
    setProjectActionError(null);
    setProjectActionMessage(null);
    setProjectActionProjectId(project.id);

    try {
      let latestScan: ProjectScanSummary | null = null;

      if (selectedExcludedPageIds.length > 0) {
        const result = await updateProjectPageSelection(project.id, selectedExcludedPageIds, false);
        latestScan = result.scan;
      }

      if (indexablePageIds.length > 0) {
        const result = await updateProjectPageSelection(project.id, indexablePageIds, true);
        latestScan = result.scan;
      }

      if (latestScan) {
        const scopeCounts = getScanScopeCounts(latestScan);

        setScanSummaries((currentSummaries) => ({
          ...currentSummaries,
          [project.id]: latestScan
        }));
        setProjectActionMessage(
          `SEO scope: выбрано посадочных ${scopeCounts.selectedIndexable}/${scopeCounts.indexable}; исключённых выбрано ${scopeCounts.selectedExcluded}.`
        );
      }
    } catch (error: unknown) {
      setProjectActionError(getErrorMessage(error, "Не удалось применить рекомендованный SEO scope."));
    } finally {
      setSelectingPagesProjectId(null);
    }
  }

  async function handleOpenPageWorkspace(
    project: ProjectSummary,
    target: ProjectScanSummary["pages"][number] | WorkspaceScopeItem,
    options: { deferActiveUntilLoaded?: boolean; navigatePreview?: boolean; silent?: boolean } = {}
  ) {
    const isScopeItem = "scopeId" in target;
    const pageId = target.pageId;
    const scopeId = isScopeItem ? target.scopeId : getPageScopeId(target);
    const workspaceTitle = isScopeItem ? target.title : getPageTitle(target);
    const workspaceKey = getWorkspaceKey(project.id, scopeId);

    setBusyWorkspaceKey(workspaceKey);

    if (!options.silent) {
      setProjectActionError(null);
      setProjectActionMessage(null);
      setProjectActionProjectId(project.id);
    }

    if (options.navigatePreview !== false) {
      setWorkspacePreviewScopeIds((currentScopeIds) => ({
        ...currentScopeIds,
        [project.id]: scopeId
      }));
    }

    if (!options.deferActiveUntilLoaded) {
      setActiveWorkspaceScopeIds((currentScopeIds) => ({
        ...currentScopeIds,
        [project.id]: scopeId
      }));
    }

    try {
      const result = await openPageWorkspace(project.id, pageId);

      setPageWorkspaces((currentWorkspaces) => ({
        ...currentWorkspaces,
        [workspaceKey]: result.workspace
      }));
      setWorkspaceDraftTexts((currentTexts) => ({
        ...currentTexts,
        [workspaceKey]: result.workspace.item.workingText
      }));
      setWorkspaceSectionTexts((currentTexts) => ({
        ...currentTexts,
        [workspaceKey]: getInitialSectionTextMap(result.workspace)
      }));
      setActiveWorkspaceSectionIds((currentSectionIds) => ({
        ...currentSectionIds,
        [workspaceKey]: currentSectionIds[workspaceKey] ?? result.workspace.sections[0]?.id ?? ""
      }));
      setActiveWorkspaceTargetIds((currentTargetIds) => ({
        ...currentTargetIds,
        [workspaceKey]: currentTargetIds[workspaceKey] ?? result.workspace.sections[0]?.id ?? ""
      }));
      if (options.deferActiveUntilLoaded) {
        setActiveWorkspaceScopeIds((currentScopeIds) => ({
          ...currentScopeIds,
          [project.id]: scopeId
        }));
      }

      if (!options.silent) {
        setProjectActionMessage(`Рабочая зона открыта: ${workspaceTitle}.`);
      }
    } catch (error: unknown) {
      setProjectActionError(getErrorMessage(error, "Не удалось открыть рабочую зону страницы."));
    } finally {
      setBusyWorkspaceKey(null);
    }
  }

  async function openPageEditorFromAudit(project: ProjectSummary, page: ProjectScanSummary["pages"][number]) {
    setActiveProjectId(project.id);
    setActiveStageIndex(2);

    await handleOpenPageWorkspace(project, page);

    window.requestAnimationFrame(() => {
      document.querySelector(".topbar")?.scrollIntoView({ behavior: "smooth", block: "start" });
    });
  }

  async function handleSavePageWorkspace(projectId: string, pageId: string, workspaceKey = getWorkspaceKey(projectId, `page:${pageId}`)) {
    const workspace = pageWorkspaces[workspaceKey];
    const workingText = workspace
      ? buildWorkingTextFromSections(workspace, workspaceSectionTexts[workspaceKey])
      : (workspaceDraftTexts[workspaceKey] ?? "");

    setBusyWorkspaceKey(workspaceKey);
    setProjectActionError(null);
    setProjectActionMessage(null);
    setProjectActionProjectId(projectId);

    try {
      const result = await savePageWorkspace(projectId, pageId, workingText);

      setPageWorkspaces((currentWorkspaces) => ({
        ...currentWorkspaces,
        [workspaceKey]: result.workspace
      }));
      setWorkspaceDraftTexts((currentTexts) => ({
        ...currentTexts,
        [workspaceKey]: result.workspace.item.workingText
      }));
      setWorkspaceSectionTexts((currentTexts) => ({
        ...currentTexts,
        [workspaceKey]: {
          ...getInitialSectionTextMap(result.workspace),
          ...(workspaceSectionTexts[workspaceKey] ?? {})
        }
      }));
      setProjectActionMessage("Working draft сохранён. Исходный проект не изменён.");
    } catch (error: unknown) {
      setProjectActionError(getErrorMessage(error, "Не удалось сохранить рабочий draft."));
    } finally {
      setBusyWorkspaceKey(null);
    }
  }

  async function handleResetPageWorkspace(projectId: string, pageId: string, workspaceKey = getWorkspaceKey(projectId, `page:${pageId}`)) {
    const confirmed = window.confirm("Сбросить working draft к original snapshot? Исходный проект не изменится.");

    if (!confirmed) {
      return;
    }

    setBusyWorkspaceKey(workspaceKey);
    setProjectActionError(null);
    setProjectActionMessage(null);
    setProjectActionProjectId(projectId);

    try {
      const result = await resetPageWorkspace(projectId, pageId);

      setPageWorkspaces((currentWorkspaces) => ({
        ...currentWorkspaces,
        [workspaceKey]: result.workspace
      }));
      setWorkspaceDraftTexts((currentTexts) => ({
        ...currentTexts,
        [workspaceKey]: result.workspace.item.workingText
      }));
      setWorkspaceSectionTexts((currentTexts) => ({
        ...currentTexts,
        [workspaceKey]: getInitialSectionTextMap(result.workspace)
      }));
      setActiveWorkspaceSectionIds((currentSectionIds) => ({
        ...currentSectionIds,
        [workspaceKey]: result.workspace.sections[0]?.id ?? ""
      }));
      setActiveWorkspaceTargetIds((currentTargetIds) => ({
        ...currentTargetIds,
        [workspaceKey]: result.workspace.sections[0]?.id ?? ""
      }));
      setProjectActionMessage("Working draft сброшен к original snapshot.");
    } catch (error: unknown) {
      setProjectActionError(getErrorMessage(error, "Не удалось сбросить рабочий draft."));
    } finally {
      setBusyWorkspaceKey(null);
    }
  }

  function activateWorkspaceTarget(workspaceKey: string, target: WorkspaceTarget) {
    setActiveWorkspaceTargetIds((currentTargetIds) => ({
      ...currentTargetIds,
      [workspaceKey]: target.id
    }));
    setActiveWorkspaceSectionIds((currentSectionIds) => ({
      ...currentSectionIds,
      [workspaceKey]: target.sectionId
    }));
    openWorkspaceDrawer(getWorkspaceUiKeyFromWorkspaceKey(workspaceKey), "issues");

    scrollWorkspaceFieldIntoView(workspaceKey, target.id);
  }

  function openWorkspaceDrawer(workspaceKey: string, drawer: WorkspaceDrawerPanel) {
    setWorkspaceDrawers((currentDrawers) => {
      const currentWorkspaceDrawers = currentDrawers[workspaceKey] ?? [];

      if (currentWorkspaceDrawers.includes(drawer)) {
        return currentDrawers;
      }

      return {
        ...currentDrawers,
        [workspaceKey]: [...currentWorkspaceDrawers, drawer]
      };
    });
  }

  function toggleWorkspaceDrawer(workspaceKey: string, drawer: WorkspaceDrawerPanel) {
    setWorkspaceDrawers((currentDrawers) => {
      const currentWorkspaceDrawers = currentDrawers[workspaceKey] ?? [];
      const nextWorkspaceDrawers = currentWorkspaceDrawers.includes(drawer)
        ? currentWorkspaceDrawers.filter((currentDrawer) => currentDrawer !== drawer)
        : [...currentWorkspaceDrawers, drawer];

      return {
        ...currentDrawers,
        [workspaceKey]: nextWorkspaceDrawers
      };
    });
  }

  function closeWorkspaceDrawer(workspaceKey: string, drawer: WorkspaceDrawerPanel) {
    setWorkspaceDrawers((currentDrawers) => {
      const currentWorkspaceDrawers = currentDrawers[workspaceKey] ?? [];

      if (!currentWorkspaceDrawers.includes(drawer)) {
        return currentDrawers;
      }

      return {
        ...currentDrawers,
        [workspaceKey]: currentWorkspaceDrawers.filter((currentDrawer) => currentDrawer !== drawer)
      };
    });
  }

  function updateWorkspacePreviewMarkers(
    workspaceKey: string,
    targets: WorkspaceTarget[],
    scopeItems: WorkspaceScopeItem[] = [],
    activeScopeItem: WorkspaceScopeItem | null = null
  ) {
    const frameWindow = previewFrameRefs.current[workspaceKey]?.contentWindow;

    if (!frameWindow) {
      return;
    }

    setWorkspacePreviewMarkers((currentMarkers) => ({
      ...currentMarkers,
      [workspaceKey]: []
    }));

    frameWindow.postMessage(
      {
        type: "seo-mode:measure-targets",
        workspaceKey,
        activePageId: activeScopeItem?.pageId ?? "",
        activeScopeId: activeScopeItem?.scopeId ?? "",
        scopePages: scopeItems
          .filter((item) => item.previewTargetSelector)
          .map((item) => ({
            label: item.title,
            pageId: item.pageId,
            scopeId: item.scopeId,
            selector: item.previewTargetSelector,
            selectorIndex: item.previewTargetIndex
          })),
        targets: targets
          .filter((target) => (target.markerSelectors.length > 0 || target.markerFallbackSelector) && target.issues.length > 0)
          .map((target) => {
            const shouldAnchorToRenderedSection = activeScopeItem?.kind === "section" && Boolean(activeScopeItem.previewTargetSelector);
            const sectionFallbackSelector = shouldAnchorToRenderedSection
              ? activeScopeItem?.previewTargetSelector ?? null
              : target.markerFallbackSelector;

            return {
              fallbackSelector: sectionFallbackSelector,
              fallbackSelectorIndex: shouldAnchorToRenderedSection ? activeScopeItem?.previewTargetIndex ?? 0 : null,
              id: target.id,
              label: target.title,
              number: target.number,
              pageStartFallback: !shouldAnchorToRenderedSection && target.kind !== "media",
              preferPageStart:
                !shouldAnchorToRenderedSection &&
                (target.sectionId === "seo-title" || target.sectionId === "seo-description" || target.sectionId === "seo-h1"),
              sectionId: target.sectionId,
              selector: target.selector,
              selectorIndex: null,
              selectors: shouldAnchorToRenderedSection ? [] : target.markerSelectors
            };
          })
      },
      "*"
    );
  }

  function changeWorkspacePreviewMode(
    workspaceKey: string,
    mode: WorkspacePreviewMode,
    targets: WorkspaceTarget[],
    scopeItems: WorkspaceScopeItem[] = [],
    activeScopeItem: WorkspaceScopeItem | null = null
  ) {
    setWorkspacePreviewModes((currentModes) => ({
      ...currentModes,
      [workspaceKey]: mode
    }));

    window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopeItems, activeScopeItem), 120);
    window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopeItems, activeScopeItem), 450);
  }

  function handleWorkspacePreviewLoad(
    workspaceKey: string,
    targets: WorkspaceTarget[],
    scopeItems: WorkspaceScopeItem[] = [],
    activeScopeItem: WorkspaceScopeItem | null = null
  ) {
    updateWorkspacePreviewMarkers(workspaceKey, targets, scopeItems, activeScopeItem);
    window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopeItems, activeScopeItem), 350);
    window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopeItems, activeScopeItem), 1000);
  }

  function setAuditIssuesVisibleCount(scanId: string, visibleCount: number) {
    setAuditIssueLimits((currentLimits) => ({
      ...currentLimits,
      [scanId]: visibleCount
    }));
  }

  function setAuditSeverityFilter(scanId: string, filter: AuditSeverityFilter) {
    setAuditSeverityFilters((currentFilters) => ({
      ...currentFilters,
      [scanId]: filter
    }));
    setAuditIssuesVisibleCount(scanId, AUDIT_ISSUE_PAGE_SIZE);
  }

  function requestProjectDelete(project: ProjectSummary) {
    setPendingDeleteProjectId(project.id);
    setProjectActionError(null);
    setProjectActionMessage(null);
    setProjectActionProjectId(null);
  }

  function cancelProjectDelete() {
    setPendingDeleteProjectId(null);
  }

  async function handleProjectDelete(project: ProjectSummary) {
    setBusyProjectId(project.id);
    setProjectActionError(null);
    setProjectActionMessage(null);
    setProjectActionProjectId(project.id);

    try {
      await deleteProject(project.id);
      setProjects((currentProjects) => currentProjects.filter((currentProject) => currentProject.id !== project.id));
      setScanSummaries((currentSummaries) => {
        const nextSummaries = { ...currentSummaries };
        delete nextSummaries[project.id];
        return nextSummaries;
      });
      setSemanticAnalyses((currentAnalyses) => {
        const nextAnalyses = { ...currentAnalyses };
        delete nextAnalyses[project.id];
        return nextAnalyses;
      });
      setActiveSemanticClusterIds((currentIds) => {
        const nextIds = { ...currentIds };
        delete nextIds[project.id];
        return nextIds;
      });
      setActiveWorkspaceScopeIds((currentScopeIds) => {
        const nextScopeIds = { ...currentScopeIds };
        delete nextScopeIds[project.id];
        return nextScopeIds;
      });
      setWorkspacePreviewScopeIds((currentScopeIds) => {
        const nextScopeIds = { ...currentScopeIds };
        delete nextScopeIds[project.id];
        return nextScopeIds;
      });
      setPageWorkspaces((currentWorkspaces) =>
        Object.fromEntries(
          Object.entries(currentWorkspaces).filter(([workspaceKey]) => !workspaceKey.startsWith(`${project.id}:`))
        )
      );
      setWorkspaceDraftTexts((currentTexts) =>
        Object.fromEntries(Object.entries(currentTexts).filter(([workspaceKey]) => !workspaceKey.startsWith(`${project.id}:`)))
      );
      setWorkspaceSectionTexts((currentTexts) =>
        Object.fromEntries(Object.entries(currentTexts).filter(([workspaceKey]) => !workspaceKey.startsWith(`${project.id}:`)))
      );
      setWorkspaceDrawers((currentDrawers) =>
        Object.fromEntries(
          Object.entries(currentDrawers).filter(
            ([workspaceKey]) => workspaceKey !== project.id && !workspaceKey.startsWith(`${project.id}:`)
          )
        )
      );
      setActiveWorkspaceSectionIds((currentSectionIds) =>
        Object.fromEntries(
          Object.entries(currentSectionIds).filter(([workspaceKey]) => !workspaceKey.startsWith(`${project.id}:`))
        )
      );
      setProjectActionProjectId(null);
      setPendingDeleteProjectId(null);
      setProjectActionMessage(`Проект «${project.name}» удалён.`);

      if (activeProjectId === project.id) {
        setActiveProjectId(null);
        setActiveStageIndex(0);
      }

      if (editingProjectId === project.id) {
        cancelProjectRename();
      }
    } catch (error: unknown) {
      setProjectActionError(getErrorMessage(error, "Не удалось удалить проект."));
    } finally {
      setBusyProjectId(null);
    }
  }

  function hasProjectNameConflict(name: string, excludeProjectId?: string) {
    const normalizedName = normalizeProjectName(name);

    if (!normalizedName) {
      return false;
    }

    return projects.some((project) => {
      return project.id !== excludeProjectId && normalizeProjectName(project.name) === normalizedName;
    });
  }

  const activeProject =
    (activeProjectId ? projects.find((project) => project.id === activeProjectId) : null) ?? projects[0] ?? null;
  const activeProjectScanSummary = activeProject ? scanSummaries[activeProject.id] : null;
  const hasActiveProjectScanEntry = activeProject
    ? Object.prototype.hasOwnProperty.call(scanSummaries, activeProject.id)
    : false;
  const visibleProjects = activeStageIndex === 0 ? projects : activeProject ? [activeProject] : [];

  useEffect(() => {
    if (
      activeStageIndex !== 1 ||
      !activeProject ||
      projectsLoading ||
      scanningProjectId ||
      !hasActiveProjectScanEntry ||
      activeProjectScanSummary ||
      autoScannedProjectIdsRef.current.has(activeProject.id)
    ) {
      return;
    }

    autoScannedProjectIdsRef.current.add(activeProject.id);
    void handleProjectScan(activeProject);
  }, [
    activeProject,
    activeProjectScanSummary,
    activeStageIndex,
    hasActiveProjectScanEntry,
    projectsLoading,
    scanningProjectId
  ]);

  useEffect(() => {
    for (const project of projects) {
      const scanSummary = scanSummaries[project.id];

      if (!scanSummary) {
        continue;
      }

      const selectedWorkspaceScopeItems = getSelectedWorkspaceScopeItems(scanSummary);
      const selectedScopeItem = selectedWorkspaceScopeItems[0];

      if (!selectedScopeItem) {
        continue;
      }

      const activeScopeId = activeWorkspaceScopeIds[project.id] ?? selectedScopeItem.scopeId;
      const scopeItemToOpen =
        selectedWorkspaceScopeItems.find((item) => item.scopeId === activeScopeId) ??
        selectedScopeItem;
      const workspaceKey = getWorkspaceKey(project.id, scopeItemToOpen.scopeId);

      if (pageWorkspaces[workspaceKey] || busyWorkspaceKey === workspaceKey) {
        continue;
      }

      void handleOpenPageWorkspace(project, scopeItemToOpen, {
        navigatePreview: !workspacePreviewScopeIds[project.id],
        silent: true
      });
      break;
    }
  }, [activeWorkspaceScopeIds, busyWorkspaceKey, pageWorkspaces, projects, scanSummaries, workspacePreviewScopeIds]);

  const createNameConflict = pickedFolder ? hasProjectNameConflict(pickedFolder.rootName) : false;
  const canCreateProject = Boolean(pickedFolder) && !isSubmitting && !createNameConflict;
  const activeStage = stageMeta[activeStageIndex] ?? stageMeta[3];

  function openProjectScanStage(project: ProjectSummary) {
    if (editingProjectId === project.id || pendingDeleteProjectId === project.id) {
      return;
    }

    setActiveProjectId(project.id);
    setActiveStageIndex(1);

    window.requestAnimationFrame(() => {
      document.querySelector(".topbar")?.scrollIntoView({ behavior: "smooth", block: "start" });
    });
  }

  return (
    <div className="app-shell">
      <header className="app-header">
        <div className="brand-block" aria-label="seo_mode">
          <div className="brand-mark"><span>sm</span></div>
          <div>
            <h1>seo_mode</h1>
            <p>SEO workspace</p>
          </div>
        </div>

        <nav className="nav-list" aria-label="Основная навигация">
          {pipeline.map((item, index) => (
            <button
              aria-pressed={index === activeStageIndex}
              className={index === activeStageIndex ? "nav-item active" : "nav-item"}
              key={item}
              onClick={() => setActiveStageIndex(index)}
              type="button"
            >
              <span>{String(index + 1).padStart(2, "0")}</span>
              <strong>{item}</strong>
            </button>
          ))}
        </nav>

        <details className="system-status-menu">
          <summary aria-label="Технический статус системы">
            <Settings size={18} />
          </summary>
          <div className="system-status-popover">
            <div className={health ? "status-pill ok" : "status-pill warning"}>
              <Activity size={16} />
              {health ? "Бэкенд работает" : "Бэкенд ждёт запуска"}
            </div>
            <button className="system-settings-action" onClick={openExternalServicesModal} type="button">
              <Plug size={16} />
              <span>Внешние сервисы</span>
            </button>
            <article className="status-card compact">
              <Server size={18} />
              <div>
                <h3>Бэкенд</h3>
                <p>{health ? "Сервер seo_mode отвечает" : (healthError ?? "Ждём ответ от /health")}</p>
              </div>
            </article>
            <article className="status-card compact">
              <Database size={18} />
              <div>
                <h3>Postgres</h3>
                <p>Состояние проекта, версии и решения</p>
              </div>
            </article>
            <article className="status-card compact">
              <Archive size={18} />
              <div>
                <h3>MinIO/S3</h3>
                <p>Артефакты, снимки, отчёты и экспорты</p>
              </div>
            </article>
            <details className="system-subsection">
              <summary>
                <FolderCheck size={16} />
                <span>Ближайший кусок сборки</span>
              </summary>
              <div className="task-table system-task-table">
                <div className="task-row done">
                  <span>01</span>
                  <p>Рабочая папка, TypeScript, проверка бэкенда, React-каркас</p>
                </div>
                <div className="task-row done">
                  <span>02</span>
                  <p>Миграции Postgres и модель источника проекта</p>
                </div>
                <div className="task-row done">
                  <span>03</span>
                  <p>Создание, переименование, копирование и удаление проекта</p>
                </div>
                <div className="task-row done">
                  <span>04</span>
                  <p>Версия скана, список страниц и ассетов, сверка изменений</p>
                </div>
                <div className="task-row active">
                  <span>05</span>
                  <p>projectIndex.json и базовый аудит title/description/headings/media</p>
                </div>
              </div>
            </details>
          </div>
        </details>
      </header>

      {isExternalServicesOpen ? (
        <div
          className="service-modal-backdrop"
          onMouseDown={(event) => {
            if (event.target === event.currentTarget) {
              setIsExternalServicesOpen(false);
            }
          }}
          role="presentation"
        >
          <section aria-labelledby="external-services-title" aria-modal="true" className="service-modal" role="dialog">
            <header className="service-modal-header">
              <div>
                <span>
                  <KeyRound size={15} />
                  Platform integrations
                </span>
                <h2 id="external-services-title">Внешние сервисы</h2>
                <p>
                  Сейчас нужен один Yandex Cloud credential для Wordstat. SERP позже использует его же; Webmaster и
                  Metrica остаются отдельными OAuth-слоями.
                </p>
              </div>
              <button
                aria-label="Закрыть настройки внешних сервисов"
                className="service-modal-close"
                onClick={() => setIsExternalServicesOpen(false)}
                type="button"
              >
                <X size={18} />
              </button>
            </header>

            {externalServicesError ? (
              <p className="form-message error">
                <CircleAlert size={16} />
                <span>{externalServicesError}</span>
              </p>
            ) : null}

            {externalServicesMessage ? (
              <p className="form-message success">
                <CheckCircle2 size={16} />
                <span>{externalServicesMessage}</span>
              </p>
            ) : null}

            {externalServicesLoading ? (
              <div className="empty-state">
                <Loader2 className="spin" size={18} />
                Загружаю настройки внешних сервисов...
              </div>
            ) : externalServices ? (
              <div className="external-service-list">
                {externalServices.services.map((service) => {
                  const draft = externalServiceDrafts[service.id] ?? buildExternalServiceDraft(service);
                  const visiblePublicFields = getVisiblePublicFields(service, draft);
                  const visibleSecretFields = getVisibleSecretFields(service, draft);
                  const visibleDependencies = getExternalServiceDependencies(service, draft);
                  const isSaving = savingExternalServiceId === service.id;

                  return (
                    <article className={`external-service-card ${service.status}`} key={service.id}>
                      <div className="external-service-card-head">
                        <div>
                          <span>{getExternalServiceStatusLabel(service.status)}</span>
                          <strong>{service.label}</strong>
                          <small>{service.description}</small>
                        </div>
                        <label className="service-toggle">
                          <input
                            checked={draft.enabled}
                            onChange={(event) =>
                              updateExternalServiceDraft(service.id, (currentDraft) => ({
                                ...currentDraft,
                                enabled: event.target.checked
                              }))
                            }
                            type="checkbox"
                          />
                          enabled
                        </label>
                      </div>

                      <div className="external-service-docs">
                        {service.docs.map((doc) => (
                          <a href={doc.url} key={doc.url} rel="noreferrer" target="_blank">
                            {doc.label}
                            <ExternalLink size={12} />
                          </a>
                        ))}
                      </div>

                      {visibleDependencies.length > 0 ? (
                        <p className="service-dependency">
                          Uses: {visibleDependencies.map((serviceId) => getExternalServiceShortName(serviceId)).join(", ")}
                        </p>
                      ) : null}

                      <label className="service-field service-field-full">
                        <span>Mode</span>
                        <select
                          onChange={(event) =>
                            updateExternalServiceDraft(service.id, (currentDraft) => ({
                              ...currentDraft,
                              mode: event.target.value
                            }))
                          }
                          value={draft.mode}
                        >
                          {service.modes.map((mode) => (
                            <option key={mode.id} value={mode.id}>
                              {mode.label}
                            </option>
                          ))}
                        </select>
                      </label>

                      {service.missingFields.length > 0 ? (
                        <p className="service-warning">
                          Нужно заполнить: {service.missingFields.map(getExternalMissingFieldLabel).join(", ")}.
                        </p>
                      ) : null}

                      <div className="service-field-grid">
                        {visiblePublicFields.map((field) => (
                          <label className="service-field" key={field.id}>
                            <span>{field.label}</span>
                            <input
                              inputMode={field.type === "number" ? "numeric" : undefined}
                              onChange={(event) =>
                                updateExternalServiceDraft(service.id, (currentDraft) => ({
                                  ...currentDraft,
                                  publicConfig: {
                                    ...currentDraft.publicConfig,
                                    [field.id]: event.target.value
                                  }
                                }))
                              }
                              placeholder={field.placeholder}
                              type={field.type === "number" ? "number" : field.type === "url" ? "url" : "text"}
                              value={draft.publicConfig[field.id] ?? ""}
                            />
                            <small>{field.help}</small>
                          </label>
                        ))}

                        {visibleSecretFields.map((field) => (
                          <label className="service-field" key={field.id}>
                            <span>{field.label}</span>
                            <input
                              autoComplete="off"
                              onChange={(event) =>
                                updateExternalServiceDraft(service.id, (currentDraft) => ({
                                  ...currentDraft,
                                  secrets: {
                                    ...(currentDraft.secrets ?? {}),
                                    [field.id]: event.target.value
                                  }
                                }))
                              }
                              placeholder={
                                service.secrets[field.id]?.configured
                                  ? "сохранено, введи новое для замены"
                                  : field.placeholder
                              }
                              type="password"
                              value={draft.secrets?.[field.id] ?? ""}
                            />
                            <small>{field.help}</small>
                          </label>
                        ))}
                      </div>

                      <div className="service-card-footer">
                        <small>
                          {service.implementationStatus === "active"
                            ? "Этот provider уже подключается к backend pipeline."
                            : "Настройки сохраняются заранее; backend adapter будет подключён отдельным слоем."}
                        </small>
                        <button
                          className="tiny-action primary"
                          disabled={isSaving}
                          onClick={() => void handleSaveExternalService(service)}
                          type="button"
                        >
                          {isSaving ? <Loader2 className="spin" size={14} /> : <Save size={14} />}
                          {isSaving ? "Сохраняю..." : "Сохранить"}
                        </button>
                      </div>
                    </article>
                  );
                })}
              </div>
            ) : (
              <div className="empty-state">
                <Plug size={18} />
                Нажми обновление, чтобы загрузить список внешних сервисов.
              </div>
            )}
          </section>
        </div>
      ) : null}

      <main className={`workspace stage-${activeStageIndex + 1}`}>
        <header className="topbar">
          <div>
            <p className="eyebrow">
              {workspaceMode === "dev" ? `Dev Mode · этап ${activeStageIndex + 1}` : "Auto Mode · целевой слой"}
            </p>
            <h2>{workspaceMode === "dev" ? activeStage.title : "Автоматический SEO-процесс"}</h2>
          </div>
          <div className="workspace-mode-switch" aria-label="Режим SEO-модуля" role="group">
            <button
              aria-pressed={workspaceMode === "dev"}
              className={workspaceMode === "dev" ? "active" : ""}
              onClick={() => setWorkspaceMode("dev")}
              type="button"
            >
              Dev Mode
            </button>
            <button
              aria-pressed={workspaceMode === "auto"}
              className={workspaceMode === "auto" ? "active" : ""}
              onClick={() => setWorkspaceMode("auto")}
              type="button"
            >
              Auto Mode
            </button>
          </div>
        </header>

        {workspaceMode === "auto" ? (
          <section className="auto-mode-shell" aria-labelledby="auto-mode-title">
            <div className="auto-mode-heading">
              <span>Target UX</span>
              <div>
                <h3 id="auto-mode-title">Auto Mode</h3>
                <p>Один экран для импорта сайта, короткого вывода и готовых вариантов SEO-правок.</p>
              </div>
            </div>
            <div className="auto-mode-flow">
              <article>
                <span>01</span>
                <strong>Загрузить сайт</strong>
                <small>Источник, скан, карта страниц и текстовые блоки.</small>
              </article>
              <article>
                <span>02</span>
                <strong>Получить вывод</strong>
                <small>Покрытие, риски, конкуренты и частотность в одном коротком статусе.</small>
              </article>
              <article>
                <span>03</span>
                <strong>Выбрать правку</strong>
                <small>Несколько готовых вариантов с прогнозом и preview.</small>
              </article>
            </div>
            <div className="auto-variant-board">
              <article>
                <span>Вариант A</span>
                <strong>Максимальный охват рынка</strong>
                <small>Подключается после завершения dev-mode ядра и внешних источников.</small>
              </article>
              <article>
                <span>Вариант B</span>
                <strong>Платформа и enterprise intent</strong>
                <small>Будет собираться из семантики, Wordstat, SERP и контекста конкурентов.</small>
              </article>
              <article>
                <span>Вариант C</span>
                <strong>AI-автоматизация бизнес-процессов</strong>
                <small>Целевой rewrite-контур без ручной SEO-аналитики на первом экране.</small>
              </article>
            </div>
          </section>
        ) : null}

        <section className={workspaceMode === "auto" ? "project-setup mode-hidden" : "project-setup"} aria-labelledby="project-setup-title">
          <div className="section-heading">
            <FolderOpen size={20} />
            <div>
              <h3 id="project-setup-title">{activeStage.sectionTitle}</h3>
              {activeStageIndex === 1 && activeProject ? (
                <>
                  <div className="section-project-title-row">
                    {editingProjectId === activeProject.id ? (
                      <input
                        aria-label="Новое название проекта"
                        className="project-title-input section-project-title-input"
                        onBlur={() => void commitProjectRename(activeProject)}
                        onChange={(event) => setEditingProjectName(event.target.value)}
                        onKeyDown={(event) => {
                          if (event.key === "Enter") {
                            event.preventDefault();
                            void commitProjectRename(activeProject);
                          }

                          if (event.key === "Escape") {
                            event.preventDefault();
                            cancelProjectRename();
                          }
                        }}
                        ref={titleInputRef}
                        value={editingProjectName}
                      />
                    ) : (
                      <button
                        className="section-project-title project-title"
                        onDoubleClick={() => startProjectRename(activeProject)}
                        title="Двойной клик, чтобы переименовать"
                        type="button"
                      >
                        {activeProject.name}
                      </button>
                    )}
                    {busyProjectId === activeProject.id && editingProjectId === activeProject.id ? (
                      <Loader2 className="spin project-title-loader" size={16} />
                    ) : null}
                  </div>
                  {editingProjectId === activeProject.id && editingProjectName.trim().length === 0 ? (
                    <small className="field-warning section-title-warning">Название не должно быть пустым.</small>
                  ) : null}
                  {editingProjectId === activeProject.id && hasProjectNameConflict(editingProjectName, activeProject.id) ? (
                    <small className="field-warning section-title-warning">Проект с таким названием уже есть.</small>
                  ) : null}
                </>
              ) : null}
              <p>{activeStage.description}</p>
            </div>
          </div>

          <div className="project-grid">
            <form className="project-form" onSubmit={handleCreateProject}>
              <label className="field">
                <span>Папка сайта</span>
                <div
                  className={isFolderDragActive ? "folder-picker-panel active" : "folder-picker-panel"}
                  onDragLeave={handleFolderDragLeave}
                  onDragOver={handleFolderDragOver}
                  onDrop={handleFolderDrop}
                >
                  <input
                    aria-hidden="true"
                    className="folder-picker-input"
                    multiple
                    onChange={handleFolderInputChange}
                    ref={folderInputRef}
                    tabIndex={-1}
                    type="file"
                  />
                  <button className="folder-pick-action" onClick={() => void handlePickFolder()} type="button">
                    <UploadCloud size={16} />
                    Выбрать папку
                  </button>
                  <span>или перетащи папку сюда, чтобы импортировать файлы</span>
                </div>
                {isSubmitting ? (
                  <div className="folder-picker-message-row folder-upload-status" aria-live="polite">
                    <small className="folder-picker-message">
                      Файлы загружаются<span className="loading-dots" aria-hidden="true" />
                    </small>
                  </div>
                ) : folderPickerMessage ? (
                  <div className="folder-picker-message-row">
                    <small className="folder-picker-message">{folderPickerMessage}</small>
                    {pickedFolder ? (
                      <button
                        aria-label="Убрать выбранную папку"
                        className="folder-picker-clear"
                        onClick={clearPickedFolder}
                        title="Убрать выбранную папку"
                        type="button"
                      >
                        ×
                      </button>
                    ) : null}
                  </div>
                ) : null}
                {createNameConflict && pickedFolder ? (
                  <small className="field-warning">Проект с названием «{pickedFolder.rootName}» уже есть.</small>
                ) : null}
              </label>

              {createError ? (
                <p className="form-message error">
                  <CircleAlert size={16} />
                  {createError}
                </p>
              ) : null}

              {createMessage ? (
                <p className="form-message success create-project-message">
                  <CheckCircle2 size={16} />
                  <span>{createMessage}</span>
                </p>
              ) : null}

              <button className="primary-action" disabled={!canCreateProject} type="submit">
                {isSubmitting ? <Loader2 className="spin" size={16} /> : <Plus size={16} />}
                {isSubmitting ? "Создаю..." : "Создать проект"}
              </button>
            </form>

            <article className="project-list-card">
              {activeStageIndex === 0 ? (
                <div className="list-heading">
                  <div>
                    <h3>Проекты</h3>
                    <p>{projects.length > 0 ? `Всего проектов: ${projects.length}` : "Пока список пустой"}</p>
                  </div>
                  <button
                    aria-label="Обновить проекты"
                    className="ghost-action refresh-projects-action"
                    disabled={projectsLoading}
                    onClick={loadProjects}
                    title="Обновить проекты"
                    type="button"
                  >
                    <RefreshCw className={projectsLoading ? "spin" : undefined} size={16} />
                  </button>
                </div>
              ) : null}

              {!projectActionProjectId && projectActionError ? (
                <p className="form-message error project-action-message">
                  <CircleAlert size={16} />
                  <span>{projectActionError}</span>
                </p>
              ) : null}

              {!projectActionProjectId && projectActionMessage ? (
                <p className="form-message success project-action-message">
                  <CheckCircle2 size={16} />
                  <span>{projectActionMessage}</span>
                </p>
              ) : null}

              {projectsLoading ? (
                <div className="empty-state">
                  <Loader2 className="spin" size={18} />
                  Загружаю проекты...
                </div>
              ) : projectsError ? (
                <div className="empty-state error">
                  <CircleAlert size={18} />
                  {projectsError}
                </div>
              ) : projects.length === 0 ? (
                <div className="empty-state">
                  <FolderCheck size={18} />
                  Создай первый проект, и он появится здесь.
                </div>
              ) : (
                <div className="project-list">
                  {visibleProjects.map((project) => {
                    const scanSummary = scanSummaries[project.id];
                    const semanticAnalysis = semanticAnalyses[project.id] ?? null;
                    const marketEnrichment = marketEnrichments[project.id] ?? null;
                    const activeSemanticClusterId =
                      semanticAnalysis ? (activeSemanticClusterIds[project.id] ?? semanticAnalysis.clusters[0]?.id ?? "") : "";
                    const activeSemanticCluster =
                      semanticAnalysis?.clusters.find((cluster) => cluster.id === activeSemanticClusterId) ??
                      semanticAnalysis?.clusters[0] ??
                      null;
                    const isScanning = scanningProjectId === project.id;
                    const isAnalyzing = analyzingProjectId === project.id;
                    const isEnrichingMarket = enrichingMarketProjectId === project.id;
                    const isExportingSemantic = exportingSemanticKey?.startsWith(`${project.id}:`) ?? false;
                    const isSelectingPages = selectingPagesProjectId === project.id;
                    const selectedPagesCount = scanSummary ? getSelectedPagesCount(scanSummary) : 0;
                    const selectedPages = scanSummary ? scanSummary.pages.filter((page) => page.selected) : [];
                    const selectedWorkspaceScopeItems = scanSummary ? getSelectedWorkspaceScopeItems(scanSummary) : [];
                    const scanScopeCounts = scanSummary ? getScanScopeCounts(scanSummary) : null;
                    const projectSiteTitle = getProjectSiteTitle(scanSummary);
                    const pageTreeGroups = scanSummary ? getPageTreeGroups(scanSummary.pages) : [];
                    const activeWorkspaceScopeId =
                      activeWorkspaceScopeIds[project.id] ?? selectedWorkspaceScopeItems[0]?.scopeId ?? null;
                    const activeWorkspaceScopeItem =
                      selectedWorkspaceScopeItems.find((item) => item.scopeId === activeWorkspaceScopeId) ??
                      selectedWorkspaceScopeItems[0] ??
                      null;
                    const activeWorkspaceKey = activeWorkspaceScopeItem
                      ? getWorkspaceKey(project.id, activeWorkspaceScopeItem.scopeId)
                      : null;
                    const activeWorkspace = activeWorkspaceKey ? pageWorkspaces[activeWorkspaceKey] : null;
                    const activeWorkspaceText =
                      activeWorkspaceKey && activeWorkspace
                        ? (workspaceDraftTexts[activeWorkspaceKey] ?? activeWorkspace.item.workingText)
                        : "";
                    const isWorkspaceBusy = activeWorkspaceKey ? busyWorkspaceKey === activeWorkspaceKey : false;
                    const activeWorkspaceIssues = getPageIssues(scanSummary, activeWorkspaceScopeItem?.pageId ?? null);
                    const activeWorkspaceTargets = activeWorkspace ? buildWorkspaceTargets(activeWorkspace, activeWorkspaceIssues) : [];
                    const activeWorkspaceTargetId =
                      activeWorkspaceKey && activeWorkspaceTargets.length > 0
                        ? (activeWorkspaceTargetIds[activeWorkspaceKey] ?? activeWorkspaceTargets[0]?.id ?? "")
                        : "";
                    const activeWorkspaceTarget =
                      activeWorkspaceTargets.find((target) => target.id === activeWorkspaceTargetId) ??
                      activeWorkspaceTargets[0] ??
                      null;
                    const activeWorkspaceMarkers = activeWorkspaceKey ? (workspacePreviewMarkers[activeWorkspaceKey] ?? []) : [];
                    const activeWorkspacePreviewMode = activeWorkspaceKey ? (workspacePreviewModes[activeWorkspaceKey] ?? "desktop") : "desktop";
                    const activeWorkspaceUiKey = project.id;
                    const activeWorkspaceDrawers = WORKSPACE_DRAWER_ORDER.filter((drawer) =>
                      (workspaceDrawers[activeWorkspaceUiKey] ?? []).includes(drawer)
                    );
                    const previewWorkspaceScopeId = workspacePreviewScopeIds[project.id] ?? activeWorkspaceScopeItem?.scopeId ?? null;
                    const previewWorkspaceScopeItem =
                      selectedWorkspaceScopeItems.find((item) => item.scopeId === previewWorkspaceScopeId) ??
                      activeWorkspaceScopeItem;
                    const activeWorkspacePageIndex = Math.max(
                      0,
                      selectedWorkspaceScopeItems.findIndex((item) => item.scopeId === activeWorkspaceScopeItem?.scopeId)
                    );
                    const activeWorkspacePreviewSourcePath =
                      activeWorkspace && previewWorkspaceScopeItem
                        ? (previewWorkspaceScopeItem.previewSourcePath ?? previewWorkspaceScopeItem.sourcePath)
                        : "";
                    const activeWorkspacePreviewFocusSelector = previewWorkspaceScopeItem?.previewTargetSelector ?? null;
                    const activeWorkspacePreviewFocusIndex = previewWorkspaceScopeItem?.previewTargetIndex ?? null;
                    const activeWorkspaceIssueCounts = getAuditIssueCounts(activeWorkspaceIssues);
                    const selectedCoverageIssueCount = getSelectedWorkspaceIssueCount(scanSummary, selectedWorkspaceScopeItems);
                    const selectedAuditIssuesAll = scanSummary?.audit
                      ? getSelectedAuditIssues(scanSummary, scanSummary.audit.issues)
                      : [];
                    const auditSeverityFilter = auditSeverityFilters[scanSummary?.id ?? ""] ?? "all";
                    const selectedAuditIssues = filterAuditIssuesBySeverity(selectedAuditIssuesAll, auditSeverityFilter);
                    const selectedAuditCounts = getAuditIssueCounts(selectedAuditIssues);
                    const selectedAuditAllCounts = getAuditIssueCounts(selectedAuditIssuesAll);
                    const selectedMediaIssues = selectedAuditIssues.filter((issue) => issue.scope === "media");
                    const auditIssueTotal = selectedAuditIssues.length;
                    const auditIssueLimit = Math.min(
                      auditIssueLimits[scanSummary?.id ?? ""] ?? AUDIT_ISSUE_PAGE_SIZE,
                      auditIssueTotal
                    );
                    const visibleAuditIssues = selectedAuditIssues.slice(0, auditIssueLimit);
                    const auditIssueGroups = scanSummary ? buildSelectedAuditIssueGroups(scanSummary, visibleAuditIssues) : [];
                    const isProjectRowRenaming = activeStageIndex === 0 && editingProjectId === project.id;

                    return (
                      <article
                        aria-label={`Перейти к сканированию проекта ${project.name}`}
                        className={activeStageIndex === 0 ? "project-row project-row-openable" : "project-row"}
                        key={project.id}
                        onClick={(event) => {
                          if (activeStageIndex !== 0 || isProjectCardInteractionTarget(event.target)) {
                            return;
                          }

                          openProjectScanStage(project);
                        }}
                        onKeyDown={(event) => {
                          if (activeStageIndex !== 0 || event.target !== event.currentTarget) {
                            return;
                          }

                          if (event.key === "Enter" || event.key === " ") {
                            event.preventDefault();
                            openProjectScanStage(project);
                          }
                        }}
                        role={activeStageIndex === 0 ? "button" : undefined}
                        tabIndex={activeStageIndex === 0 ? 0 : undefined}
                        title={activeStageIndex === 0 ? "Перейти к сканированию" : undefined}
                      >
                        {activeStageIndex === 0 ? (
                          <span className="project-open-arrow" aria-hidden="true">
                            →
                          </span>
                        ) : null}
                        <div>
                          <div className="project-title-row">
                            {isProjectRowRenaming ? (
                              <input
                                aria-label="Название проекта"
                                autoComplete="off"
                                className="project-title-input"
                                disabled={busyProjectId === project.id}
                                onBlur={() => void commitProjectRename(project)}
                                onChange={(event) => setEditingProjectName(event.target.value)}
                                onKeyDown={(event) => {
                                  if (event.key === "Enter") {
                                    event.preventDefault();
                                    void commitProjectRename(project);
                                  }

                                  if (event.key === "Escape") {
                                    event.preventDefault();
                                    cancelProjectRename();
                                  }
                                }}
                                ref={titleInputRef}
                                value={editingProjectName}
                              />
                            ) : (
                              <h4
                                className="project-title"
                                onDoubleClick={() => {
                                  if (activeStageIndex === 0) {
                                    startProjectRename(project);
                                  }
                                }}
                                title={activeStageIndex === 0 ? "Двойной клик, чтобы переименовать" : undefined}
                              >
                                {activeStageIndex === 0 ? project.name : projectSiteTitle}
                              </h4>
                            )}
                            {busyProjectId === project.id && isProjectRowRenaming ? (
                              <Loader2 className="spin project-title-loader" size={16} />
                            ) : null}
                          </div>
                          {isProjectRowRenaming && editingProjectName.trim().length === 0 ? (
                            <small className="field-warning inline-title-warning">Название не должно быть пустым.</small>
                          ) : null}
                          {isProjectRowRenaming && hasProjectNameConflict(editingProjectName, project.id) ? (
                            <small className="field-warning inline-title-warning">Проект с таким названием уже есть.</small>
                          ) : null}
                          <div className="project-source">
                            <p>{getSourceDescription(project)}</p>
                          </div>
                          <div className="scan-summary muted project-scan-status">
                            {isScanning ? (
                              <Loader2 className="spin" size={14} />
                            ) : scanSummary ? (
                              <CheckCircle2 size={14} />
                            ) : (
                              <Search size={14} />
                            )}
                            {isScanning
                              ? "Скан выполняется..."
                              : scanSummary
                                ? getProjectScanStatusMessage(scanSummary)
                                : "Скан ещё не запускался."}
                          </div>

                          {activeStageIndex === 1 ? (
                            <div className="page-audit-overview">
                              {isScanning && !scanSummary ? (
                                <div className="empty-state page-audit-loading">
                                  <Loader2 className="spin" size={18} />
                                  Запускаю базовый аудит: ищу HTML-страницы, ассеты, title, description, H1 и медиа-поля.
                                </div>
                              ) : null}
                              {scanSummary ? (
                                <>
                                  <div className="page-audit-summary">
                                    <span>
                                      <FileText size={14} />
                                      Страниц: {scanSummary.counts.pages}
                                    </span>
                                    <span>
                                      <Image size={14} />
                                      Ассетов: {scanSummary.counts.assets}
                                    </span>
                                    {scanSummary.audit ? (
                                      <span>Замечаний: {scanSummary.audit.counts.total}</span>
                                    ) : (
                                      <span>Аудит собирается</span>
                                    )}
                                  </div>
                                  {scanSummary.pages.length > 0 ? (
                                    <>
                                      <ProjectScopeFlow
                                        isBusy={isSelectingPages || isScanning}
                                        onApplyRecommendedScope={() => void handleApplyRecommendedPageScope(project, scanSummary)}
                                        onClearSelection={() =>
                                          void handleProjectPageSelection(
                                            project,
                                            scanSummary.pages.map((page) => page.pageId),
                                            false
                                          )
                                        }
                                        onTogglePage={(pageId, selected) =>
                                          void handleProjectPageSelection(project, [pageId], selected)
                                        }
                                        projectId={project.id}
                                        projectName={project.name}
                                        scan={scanSummary}
                                      />
                                      <details className="engineering-page-list">
                                        <summary>Инженерный список аудита</summary>
                                        <div className="page-audit-list">
                                          {scanSummary.pages.map((page) => {
                                            const workspaceKey = getWorkspaceKey(project.id, getPageScopeId(page));
                                            const isOpeningPage = busyWorkspaceKey === workspaceKey;
                                            const auditDetailsKey = `${scanSummary.id}:${page.pageId}`;
                                            const isAuditDetailsOpen = Boolean(openPageAuditDetails[auditDetailsKey]);

                                            return (
                                              <article
                                                className="page-audit-card"
                                                key={`${scanSummary.id}-audit-page-${page.pageId}`}
                                                onClick={(event) => {
                                                  if (isOpeningPage) {
                                                    return;
                                                  }

                                                  if (isProjectCardInteractionTarget(event.target)) {
                                                    return;
                                                  }

                                                  if (isAuditDetailsOpen) {
                                                    setOpenPageAuditDetails((currentDetails) => ({
                                                      ...currentDetails,
                                                      [auditDetailsKey]: false
                                                    }));
                                                    return;
                                                  }

                                                  void openPageEditorFromAudit(project, page);
                                                }}
                                                onKeyDown={(event) => {
                                                  if (isOpeningPage || event.target !== event.currentTarget) {
                                                    return;
                                                  }

                                                  if (event.key === "Enter" || event.key === " ") {
                                                    event.preventDefault();

                                                    if (isAuditDetailsOpen) {
                                                      setOpenPageAuditDetails((currentDetails) => ({
                                                        ...currentDetails,
                                                        [auditDetailsKey]: false
                                                      }));
                                                      return;
                                                    }

                                                    void openPageEditorFromAudit(project, page);
                                                  }
                                                }}
                                                role="button"
                                                tabIndex={isOpeningPage ? -1 : 0}
                                                title="Перейти к рабочей зоне страницы"
                                              >
                                                <div className="page-audit-card-main">
                                                  <div>
                                                    <strong>{getPageTitle(page)}</strong>
                                                    <small>{getPagePathLabel(page)}</small>
                                                  </div>
                                                  {scanSummary.audit ? (
                                                    <div className="page-audit-details">
                                                      <button
                                                        aria-expanded={isAuditDetailsOpen}
                                                        className="page-audit-toggle"
                                                        onClick={() => {
                                                          setOpenPageAuditDetails((currentDetails) => ({
                                                            ...currentDetails,
                                                            [auditDetailsKey]: !currentDetails[auditDetailsKey]
                                                          }));
                                                        }}
                                                        type="button"
                                                      >
                                                        {getPageAuditLine(scanSummary, page)}
                                                      </button>
                                                      {isAuditDetailsOpen ? (
                                                        <div
                                                          className="page-audit-issue-list"
                                                          onClick={() => {
                                                            setOpenPageAuditDetails((currentDetails) => ({
                                                              ...currentDetails,
                                                              [auditDetailsKey]: false
                                                            }));
                                                          }}
                                                        >
                                                          {getPageIssues(scanSummary, page.pageId).length > 0 ? (
                                                            getPageIssues(scanSummary, page.pageId).map((issue) => {
                                                              const explanation = getAuditIssueExplanation(issue.code);

                                                              return (
                                                                <div className={`page-audit-issue ${issue.severity}`} key={issue.id}>
                                                                  <span>{getAuditSeverityLabel(issue.severity)}</span>
                                                                  <strong>{issue.message}</strong>
                                                                  <small>
                                                                    {getAuditScopeLabel(issue)}
                                                                    {issue.sourcePath ? ` · ${issue.sourcePath}` : ""}
                                                                    {issue.field ? ` · ${issue.field}` : ""}
                                                                  </small>
                                                                  <p>
                                                                    <b>Почему важно:</b> {explanation.why}
                                                                  </p>
                                                                  <p>
                                                                    <b>Что делать:</b> {explanation.action}
                                                                  </p>
                                                                </div>
                                                              );
                                                            })
                                                          ) : (
                                                            <small>Замечаний по странице не найдено.</small>
                                                          )}
                                                        </div>
                                                      ) : null}
                                                    </div>
                                                  ) : (
                                                    <p>{getPageAuditLine(scanSummary, page)}</p>
                                                  )}
                                                  <small>{getPageAuditPreview(scanSummary, page)}</small>
                                                </div>
                                                <span className="page-audit-card-arrow" aria-hidden="true">
                                                  {isOpeningPage ? <Loader2 className="spin" size={16} /> : "→"}
                                                </span>
                                              </article>
                                            );
                                          })}
                                        </div>
                                      </details>
                                    </>
                                  ) : (
                                    <div className="empty-state">
                                      <CircleAlert size={18} />
                                      HTML-страницы пока не найдены.
                                    </div>
                                  )}
                                </>
                              ) : !isScanning ? (
                                <div className="empty-state">
                                  <Search size={18} />
                                  Конфигурация проекта появится после автоматического сканирования источника.
                                </div>
                              ) : null}
                            </div>
                          ) : null}

                          {scanSummary && activeStageIndex !== 1 ? (
                            <div className="scan-summary">
                                <div className="scan-metrics">
                                  <span>
                                    <FileText size={14} />
                                    Страниц: {scanSummary.counts.pages}
                                  </span>
                                  <span>
                                    <Image size={14} />
                                    Ассетов: {scanSummary.counts.assets}
                                  </span>
                                  <span>Скан: {getScanStatusLabel(scanSummary.status)}</span>
                                  {scanSummary.completedAt ? <span>{formatDate(scanSummary.completedAt)}</span> : null}
                                </div>
                                <p className="scan-status-note">{getScanResultMessage(scanSummary)}</p>
                                {scanSummary.projectIndexKey ? (
                                  <details className="scan-help">
                                    <summary>projectIndex.json сохранён в MinIO. Что это?</summary>
                                    <p>
                                      Это JSON-снимок конкретного анализа: список файлов, найденные страницы, ассеты,
                                      заголовки, media-ссылки и технические метаданные. Основная правда всё равно
                                      хранится в Postgres, а этот файл нужен для дебага, отчётов, истории и будущих
                                      модельных шагов.
                                    </p>
                                    <small>{scanSummary.projectIndexKey}</small>
                                  </details>
                                ) : null}
                                {activeStageIndex === 3 ? (
                                  <section className="semantic-panel">
                                    <div className="semantic-heading">
                                      <div>
                                        <strong>Semantic analysis NodeDC</strong>
                                        <small>{getSemanticRunDate(semanticAnalysis)}</small>
                                      </div>
                                      <div className="semantic-heading-actions">
                                        <button
                                          className="tiny-action primary"
                                          disabled={isAnalyzing || isScanning || !scanSummary}
                                          onClick={() => void handleSemanticAnalysis(project)}
                                          type="button"
                                        >
                                          {isAnalyzing ? <Loader2 className="spin" size={14} /> : <Search size={14} />}
                                          {isAnalyzing ? "Анализирую..." : "Запустить анализ"}
                                        </button>
                                        <div className="semantic-export-actions" aria-label="Semantic export">
                                          {(["json", "markdown", "csv"] as const).map((format) => (
                                            <button
                                              className="tiny-action"
                                              disabled={!semanticAnalysis || isAnalyzing || isExportingSemantic}
                                              key={format}
                                              onClick={() => void handleSemanticExport(project, format)}
                                              type="button"
                                            >
                                              {exportingSemanticKey === `${project.id}:${format}` ? (
                                                <Loader2 className="spin" size={14} />
                                              ) : (
                                                <Download size={14} />
                                              )}
                                              {format === "markdown" ? "MD" : format.toUpperCase()}
                                            </button>
                                          ))}
                                        </div>
                                      </div>
                                    </div>
                                    {semanticAnalysis ? (
                                      <>
                                        <div className="semantic-score-grid">
                                          <div className="semantic-score-main">
                                            <span>{semanticAnalysis.summary.coverageScore}%</span>
                                            <strong>dev readiness</strong>
                                            <small>
                                              Lexical:{" "}
                                              {semanticAnalysis.summary.lexicalCoverageScore ?? semanticAnalysis.summary.coverageScore}% ·
                                              Evidence:{" "}
                                              {semanticAnalysis.summary.evidenceScore ?? semanticAnalysis.summary.coverageScore}% ·
                                              Посадочные: {semanticAnalysis.pageScope.indexablePages} · JSON:{" "}
                                              {semanticAnalysis.pageScope.contentSources} · исключено HTML:{" "}
                                              {semanticAnalysis.pageScope.excludedDocuments}
                                            </small>
                                          </div>
                                          <div className="semantic-metric-row">
                                            <span>
                                              Intent:{" "}
                                              {semanticAnalysis.summary.intentCoverageScore ?? semanticAnalysis.summary.coverageScore}%
                                            </span>
                                            <span>
                                              Commerce:{" "}
                                              {semanticAnalysis.summary.commercialReadinessScore ??
                                                semanticAnalysis.summary.coverageScore}%
                                            </span>
                                            <span className="ok">Покрыто: {semanticAnalysis.summary.coveredClusters}</span>
                                            <span>Слабо: {semanticAnalysis.summary.weakClusters}</span>
                                            <span>Пусто: {semanticAnalysis.summary.missingClusters}</span>
                                            <span className={semanticAnalysis.summary.legacyFindings > 0 ? "danger" : "ok"}>
                                              Legacy: {semanticAnalysis.summary.legacyFindings}
                                            </span>
                                            <span className={semanticAnalysis.summary.qualityProblems > 0 ? "danger" : "ok"}>
                                              Проверки: {semanticAnalysis.summary.qualityProblems}/{semanticAnalysis.summary.qualityWarnings}
                                            </span>
                                          </div>
                                        </div>

                                        <div className="semantic-scope-grid">
                                          <div>
                                            <strong>Scope анализа</strong>
                                            <small>
                                              В покрытие попадают только посадочные HTML и смысловые JSON/copy-источники.
                                            </small>
                                          </div>
                                          <span>Посадочные: {semanticAnalysis.pageScope.indexablePages}</span>
                                          <span>Шаблоны: {semanticAnalysis.pageScope.templateBlocks}</span>
                                          <span>Админка: {semanticAnalysis.pageScope.adminPages}</span>
                                          <span>JSON/copy: {semanticAnalysis.pageScope.contentSources}</span>
                                        </div>

                                        <div className="semantic-quality-list">
                                          {semanticAnalysis.qualitySignals.map((signal) => (
                                            <article className={`semantic-quality ${signal.level}`} key={signal.id}>
                                              <span>{getSemanticQualityLabel(signal.level)}</span>
                                              <div>
                                                <strong>{signal.title}</strong>
                                                <small>{signal.message}</small>
                                              </div>
                                            </article>
                                          ))}
                                        </div>

                                        {semanticAnalysis.analysisVerdict ? (
                                          <section className={`semantic-verdict-panel ${semanticAnalysis.analysisVerdict.confidence}`}>
                                            <div className="semantic-verdict-heading">
                                              <div>
                                                <strong>Dev verdict</strong>
                                                <small>Детерминированная оценка до Wordstat, SERP и AI-анализа.</small>
                                              </div>
                                              <div>
                                                <span>
                                                  {getSemanticVerdictConfidenceLabel(semanticAnalysis.analysisVerdict.confidence)}
                                                </span>
                                                <span>
                                                  {getSemanticVerdictObjectivityLabel(
                                                    semanticAnalysis.analysisVerdict.marketObjectivity
                                                  )}
                                                </span>
                                              </div>
                                            </div>
                                            <p>{semanticAnalysis.analysisVerdict.summary}</p>

                                            <div className="semantic-verdict-brief">
                                              {semanticAnalysis.analysisVerdict.risks.slice(0, 3).map((risk) => (
                                                <span className={risk.level} key={risk.id}>{risk.title}</span>
                                              ))}
                                            </div>

                                            <details className="semantic-verdict-details">
                                              <summary>Полный dev-разбор</summary>
                                              <div className="semantic-verdict-detail-body">
                                                <div className="semantic-verdict-columns">
                                                  <section>
                                                    <strong>Что работает</strong>
                                                    {semanticAnalysis.analysisVerdict.whatWorks.map((item) => (
                                                      <small key={item}>{item}</small>
                                                    ))}
                                                  </section>
                                                  <section>
                                                    <strong>Ограничения</strong>
                                                    {semanticAnalysis.analysisVerdict.limits.map((item) => (
                                                      <small key={item}>{item}</small>
                                                    ))}
                                                  </section>
                                                  <section>
                                                    <strong>Внешние сигналы</strong>
                                                    {semanticAnalysis.analysisVerdict.externalSignalsNeeded.map((item) => (
                                                      <small key={item}>{item}</small>
                                                    ))}
                                                  </section>
                                                </div>

                                                <div className="semantic-verdict-risk-list">
                                                  {semanticAnalysis.analysisVerdict.risks.map((risk) => (
                                                    <article className={risk.level} key={risk.id}>
                                                      <span>{getSemanticQualityLabel(risk.level)}</span>
                                                      <div>
                                                        <strong>{risk.title}</strong>
                                                        <small>{risk.message}</small>
                                                      </div>
                                                    </article>
                                                  ))}
                                                </div>

                                                <div className="semantic-verdict-clusters">
                                                  <strong>Оценка кластеров</strong>
                                                  {semanticAnalysis.analysisVerdict.clusterAssessments.slice(0, 6).map((assessment) => (
                                                    <div key={assessment.clusterId}>
                                                      <span>{getSemanticVerdictAssessmentLabel(assessment.assessment)}</span>
                                                      <div>
                                                        <strong>{assessment.title}</strong>
                                                        <small>{assessment.reason}</small>
                                                      </div>
                                                    </div>
                                                  ))}
                                                </div>

                                                <div className="semantic-verdict-next">
                                                  <strong>Следующая валидация</strong>
                                                  <small>{semanticAnalysis.analysisVerdict.nextValidationStep}</small>
                                                </div>
                                              </div>
                                            </details>
                                          </section>
                                        ) : (
                                          <section className="semantic-verdict-panel muted">
                                            <strong>Dev verdict</strong>
                                            <small>Появится после нового semantic analysis.</small>
                                          </section>
                                        )}

                                        <div className="semantic-style-panel">
                                          <div>
                                            <strong>Профиль стиля</strong>
                                            <small>
                                              Язык: {semanticAnalysis.styleProfile.language} · среднее предложение:{" "}
                                              {semanticAnalysis.styleProfile.averageSentenceWords} слов
                                            </small>
                                          </div>
                                          {semanticAnalysis.styleProfile.toneSignals.length > 0 ? (
                                            <div className="semantic-seeds compact">
                                              {semanticAnalysis.styleProfile.toneSignals.map((signal) => (
                                                <span key={signal}>{signal}</span>
                                              ))}
                                            </div>
                                          ) : null}
                                          <div className="semantic-term-cloud">
                                            {semanticAnalysis.styleProfile.topTerms.slice(0, 14).map((term) => (
                                              <span key={term.term}>{term.term} · {term.count}</span>
                                            ))}
                                          </div>
                                          {semanticAnalysis.styleProfile.ctaPhrases.length > 0 ? (
                                            <small>CTA: {semanticAnalysis.styleProfile.ctaPhrases.join(", ")}</small>
                                          ) : null}
                                        </div>

                                        {semanticAnalysis.legacyFindings.length > 0 ? (
                                          <div className="semantic-warning-list">
                                            <strong>Санитарные блокеры</strong>
                                            {semanticAnalysis.legacyFindings.slice(0, 6).map((finding) => (
                                              <div className="semantic-warning-row" key={`${finding.markerId}-${finding.sourceId}`}>
                                                <span>{finding.label}</span>
                                                <p>
                                                  {finding.sourcePath} · {finding.count} упом.
                                                </p>
                                                <small>{finding.action}</small>
                                              </div>
                                            ))}
                                          </div>
                                        ) : (
                                          <div className="semantic-clean">
                                            <CheckCircle2 size={16} />
                                            Legacy-упоминания из санитарного списка не найдены.
                                          </div>
                                        )}

                                        <div className="semantic-cluster-list">
                                          {semanticAnalysis.clusters.slice(0, 8).map((cluster) => (
                                            <button
                                              className={
                                                activeSemanticCluster?.id === cluster.id
                                                  ? `semantic-cluster ${cluster.status} active`
                                                  : `semantic-cluster ${cluster.status}`
                                              }
                                              key={cluster.id}
                                              onClick={() =>
                                                setActiveSemanticClusterIds((currentIds) => ({
                                                  ...currentIds,
                                                  [project.id]: cluster.id
                                                }))
                                              }
                                              type="button"
                                            >
                                              <div>
                                                <span>{getSemanticClusterStatusLabel(cluster.status)}</span>
                                                <em>{getSemanticPriorityLabel(cluster.priority)}</em>
                                              </div>
                                              <strong>{cluster.title}</strong>
                                              <p>{cluster.targetIntent}</p>
                                              <small>
                                                Score {cluster.score}% · hits {cluster.hits}
                                                {cluster.evidence
                                                  ? ` · ${getSemanticEvidenceLevelLabel(cluster.evidence.level)}`
                                                  : ""}
                                                {cluster.matchedTerms.length > 0
                                                  ? ` · ${cluster.matchedTerms.slice(0, 5).join(", ")}`
                                                  : " · терминов пока нет"}
                                              </small>
                                              {cluster.evidenceSnippets[0] ? (
                                                <blockquote>
                                                  {cluster.evidenceSnippets[0].text}
                                                  <small>{cluster.evidenceSnippets[0].sourcePath}</small>
                                                </blockquote>
                                              ) : null}
                                            </button>
                                          ))}
                                        </div>

                                        {activeSemanticCluster ? (
                                          <section className={`semantic-cluster-detail ${activeSemanticCluster.status}`}>
                                            <div className="semantic-detail-heading">
                                              <div>
                                                <span>{getSemanticClusterStatusLabel(activeSemanticCluster.status)}</span>
                                                <strong>{activeSemanticCluster.title}</strong>
                                                <small>{activeSemanticCluster.targetIntent}</small>
                                              </div>
                                              <div>
                                                <em>Score {activeSemanticCluster.score}%</em>
                                                <em>Hits {activeSemanticCluster.hits}</em>
                                                <em>{getSemanticPriorityLabel(activeSemanticCluster.priority)}</em>
                                                {activeSemanticCluster.evidence ? (
                                                  <em>{getSemanticEvidenceLevelLabel(activeSemanticCluster.evidence.level)}</em>
                                                ) : null}
                                              </div>
                                            </div>

                                            <div className="semantic-detail-grid">
                                              <section>
                                                <strong>Matched terms</strong>
                                                <div className="semantic-token-list">
                                                  {activeSemanticCluster.matchedTerms.length > 0 ? (
                                                    activeSemanticCluster.matchedTerms.map((term) => <span key={term}>{term}</span>)
                                                  ) : (
                                                    <small>Совпадений пока нет.</small>
                                                  )}
                                                </div>
                                              </section>
                                              <section>
                                                <strong>Missing terms</strong>
                                                <div className="semantic-token-list muted">
                                                  {activeSemanticCluster.missingTerms.slice(0, 10).map((term) => (
                                                    <span key={term}>{term}</span>
                                                  ))}
                                                </div>
                                              </section>
                                            </div>

                                            {activeSemanticCluster.evidence ? (
                                              <div className="semantic-detail-grid">
                                                <section>
                                                  <strong>Evidence v2</strong>
                                                  <div className="semantic-token-list">
                                                    <span>lexical {activeSemanticCluster.evidence.lexicalScore}%</span>
                                                    <span>intent {activeSemanticCluster.evidence.intentScore}%</span>
                                                    <span>landing hits {activeSemanticCluster.evidence.landingPageHits}</span>
                                                    <span>copy hits {activeSemanticCluster.evidence.contentSourceHits}</span>
                                                  </div>
                                                </section>
                                                <section>
                                                  <strong>Weak signals</strong>
                                                  <div className="semantic-token-list muted">
                                                    {[
                                                      ...activeSemanticCluster.evidence.penalties,
                                                      ...activeSemanticCluster.evidence.missingIntentGroups
                                                    ].length > 0 ? (
                                                      [
                                                        ...activeSemanticCluster.evidence.penalties,
                                                        ...activeSemanticCluster.evidence.missingIntentGroups
                                                      ]
                                                        .slice(0, 8)
                                                        .map((signal) => <span key={signal}>{signal}</span>)
                                                    ) : (
                                                      <small>Критичных слабых сигналов нет.</small>
                                                    )}
                                                  </div>
                                                </section>
                                              </div>
                                            ) : null}

                                            <section className="semantic-evidence-panel">
                                              <strong>Evidence snippets</strong>
                                              {activeSemanticCluster.evidenceSnippets.length > 0 ? (
                                                activeSemanticCluster.evidenceSnippets.map((snippet) => (
                                                  <blockquote key={`${snippet.sourceId}-${snippet.text}`}>
                                                    {snippet.text}
                                                    <small>
                                                      {snippet.sourcePath}
                                                      {snippet.matchedTerms.length > 0
                                                        ? ` · ${snippet.matchedTerms.join(", ")}`
                                                        : ""}
                                                    </small>
                                                  </blockquote>
                                                ))
                                              ) : (
                                                <small>Для этого кластера пока нет текстовых доказательств.</small>
                                              )}
                                            </section>

                                            <div className="semantic-detail-grid">
                                              <section>
                                                <strong>Источники</strong>
                                                <div className="semantic-source-list">
                                                  {activeSemanticCluster.sourceRefs.length > 0 ? (
                                                    activeSemanticCluster.sourceRefs.map((sourceRef) => (
                                                      <div key={`${sourceRef.id}-${sourceRef.sourcePath}`}>
                                                        <span>{getSemanticScopeLabel(sourceRef.scope)}</span>
                                                        <strong>{sourceRef.title}</strong>
                                                        <small>
                                                          {sourceRef.sourcePath} · hits {sourceRef.hits}
                                                        </small>
                                                      </div>
                                                    ))
                                                  ) : (
                                                    <small>Источники не найдены.</small>
                                                  )}
                                                </div>
                                              </section>
                                              <section>
                                                <strong>Целевые страницы</strong>
                                                <div className="semantic-token-list">
                                                  {activeSemanticCluster.targetPages.map((targetPage) => (
                                                    <span key={targetPage}>{targetPage}</span>
                                                  ))}
                                                </div>
                                              </section>
                                            </div>
                                          </section>
                                        ) : null}

                                        {semanticAnalysis.gaps.length > 0 ? (
                                          <div className="semantic-gap-panel">
                                            <strong>Gaps для расширения охвата</strong>
                                            {semanticAnalysis.gaps.slice(0, 5).map((gap) => (
                                              <div className="semantic-gap-row" key={gap.clusterId}>
                                                <span>{getSemanticPriorityLabel(gap.priority)}</span>
                                                <div>
                                                  <strong>{gap.title}</strong>
                                                  <small>
                                                    {getSemanticClusterStatusLabel(gap.status)} · страницы:{" "}
                                                    {gap.targetPages.slice(0, 3).join(", ")}
                                                  </small>
                                                </div>
                                              </div>
                                            ))}
                                          </div>
                                        ) : null}

                                        {(semanticAnalysis.landingPagePlan ?? []).length > 0 ? (
                                          <div className="semantic-page-plan-panel">
                                            <div>
                                              <strong>План посадочных</strong>
                                              <small>Очередь страниц: что создать, что усилить, что отдать на Wordstat/SERP.</small>
                                            </div>
                                            {(semanticAnalysis.landingPagePlan ?? []).slice(0, 6).map((page) => (
                                              <article className={`semantic-page-plan-row ${page.action}`} key={page.path}>
                                                <div>
                                                  <span>{getLandingPagePlanActionLabel(page.action)}</span>
                                                  <strong>{page.path}</strong>
                                                  <small>{getLandingOpportunityPageTypeLabel(page.pageType)}</small>
                                                </div>
                                                <div>
                                                  <strong>
                                                    opportunity {page.opportunityScore}% · readiness {page.readinessScore}%
                                                  </strong>
                                                  <small>
                                                    {getSemanticPriorityLabel(page.priority)} · покрыто {page.coveredClusterCount} ·
                                                    слабо {page.weakClusterCount} · пусто {page.missingClusterCount}
                                                  </small>
                                                  <p>{page.reason}</p>
                                                  <div className="semantic-token-list">
                                                    {page.clusters.slice(0, 4).map((cluster) => (
                                                      <span key={`${page.path}-${cluster.clusterId}`}>{cluster.title}</span>
                                                    ))}
                                                  </div>
                                                  {page.seedPhrases.length > 0 ? (
                                                    <div className="semantic-token-list muted">
                                                      {page.seedPhrases.slice(0, 4).map((phrase) => (
                                                        <span key={`${page.path}-${phrase}`}>{phrase}</span>
                                                      ))}
                                                    </div>
                                                  ) : null}
                                                </div>
                                              </article>
                                            ))}
                                          </div>
                                        ) : null}

                                        {(semanticAnalysis.landingPageBriefs ?? []).length > 0 ? (
                                          <div className="semantic-brief-panel">
                                            <div>
                                              <strong>SEO briefs</strong>
                                              <small>Черновая структура посадочных перед рерайтом, Wordstat и SERP.</small>
                                            </div>
                                            {(semanticAnalysis.landingPageBriefs ?? []).slice(0, 4).map((brief) => {
                                              const quality = brief.qualityContract;

                                              return (
                                                <details className={`semantic-brief-row ${brief.action}`} key={brief.path}>
                                                  <summary>
                                                    <span>{getLandingPagePlanActionLabel(brief.action)}</span>
                                                    <div>
                                                      <strong>{brief.path}</strong>
                                                      <small>{brief.h1}</small>
                                                    </div>
                                                    <span className={`semantic-brief-quality ${quality?.level ?? "legacy"}`}>
                                                      {quality ? `${quality.score}% · ${getBriefQualityLevelLabel(quality.level)}` : "legacy"}
                                                    </span>
                                                  </summary>
                                                  <div className="semantic-brief-body">
                                                    {quality ? (
                                                      <section className="semantic-brief-quality-card">
                                                        <div>
                                                          <strong>Качество брифа</strong>
                                                          <span>{quality.score}%</span>
                                                        </div>
                                                        <small>{quality.summary}</small>
                                                        <div className="semantic-brief-checks">
                                                          {quality.checks.slice(0, 6).map((check) => (
                                                            <span className={check.status} key={`${brief.path}-${check.id}`}>
                                                              {getBriefQualityCheckLabel(check.status)} · {check.title}
                                                            </span>
                                                          ))}
                                                        </div>
                                                        {quality.riskFlags.length > 0 ? (
                                                          <div className="semantic-token-list muted">
                                                            {quality.riskFlags.slice(0, 3).map((risk) => (
                                                              <span className={risk.level} key={`${brief.path}-${risk.id}`}>
                                                                {risk.title}
                                                              </span>
                                                            ))}
                                                          </div>
                                                        ) : null}
                                                        <p>{quality.nextStep}</p>
                                                      </section>
                                                    ) : null}
                                                    <section>
                                                      <strong>Title / meta</strong>
                                                      <small>{brief.title}</small>
                                                      <p>{brief.metaDescription}</p>
                                                    </section>
                                                    <section>
                                                      <strong>Интент и CTA</strong>
                                                      <small>{brief.audience}</small>
                                                      <p>{brief.introAngle}</p>
                                                      <div className="semantic-token-list">
                                                        <span>{brief.primaryCta}</span>
                                                        <span>{brief.secondaryCta}</span>
                                                      </div>
                                                    </section>
                                                    <section>
                                                      <strong>Секции</strong>
                                                      {brief.sections.slice(0, 4).map((section) => (
                                                        <div className="semantic-brief-section" key={`${brief.path}-${section.title}`}>
                                                          <span>{section.title}</span>
                                                          <small>{section.goal}</small>
                                                        </div>
                                                      ))}
                                                    </section>
                                                    <section>
                                                      <strong>Checklist</strong>
                                                      <div className="semantic-brief-checks">
                                                        {brief.readinessChecklist.map((item) => (
                                                          <span className={item.done ? "done" : "pending"} key={`${brief.path}-${item.id}`}>
                                                            {item.title}
                                                          </span>
                                                        ))}
                                                      </div>
                                                    </section>
                                                    <div className="semantic-token-list">
                                                      {brief.mustUseTerms.slice(0, 6).map((term) => (
                                                        <span key={`${brief.path}-${term}`}>{term}</span>
                                                      ))}
                                                    </div>
                                                    <div className="semantic-token-list muted">
                                                      {brief.internalLinks.slice(0, 5).map((link) => (
                                                        <span key={`${brief.path}-${link}`}>{link}</span>
                                                      ))}
                                                    </div>
                                                  </div>
                                                </details>
                                              );
                                            })}
                                          </div>
                                        ) : null}

                                        {(semanticAnalysis.landingOpportunities ?? []).length > 0 ? (
                                          <div className="semantic-landing-panel">
                                            <div>
                                              <strong>Карта посадочных</strong>
                                              <small>Что оставить на главной, что вынести в целевые SEO-страницы.</small>
                                            </div>
                                            {(semanticAnalysis.landingOpportunities ?? []).slice(0, 8).map((opportunity) => (
                                              <article className={`semantic-landing-row ${opportunity.action}`} key={opportunity.clusterId}>
                                                <span>{getSemanticPriorityLabel(opportunity.priority)}</span>
                                                <div>
                                                  <strong>{opportunity.title}</strong>
                                                  <small>
                                                    {getLandingOpportunityActionLabel(opportunity.action)} ·{" "}
                                                    {getSemanticEvidenceLevelLabel(opportunity.evidenceLevel)} · score {opportunity.score}%
                                                  </small>
                                                  <p>{opportunity.reason}</p>
                                                  <div className="semantic-token-list">
                                                    {opportunity.recommendedPages.length > 0 ? (
                                                      opportunity.recommendedPages.slice(0, 4).map((page) => (
                                                        <span key={`${opportunity.clusterId}-${page.path}`}>
                                                          {page.path} · {getLandingOpportunityPageTypeLabel(page.pageType)}
                                                        </span>
                                                      ))
                                                    ) : (
                                                      opportunity.targetPages.slice(0, 3).map((targetPage) => (
                                                        <span key={`${opportunity.clusterId}-${targetPage}`}>{targetPage}</span>
                                                      ))
                                                    )}
                                                  </div>
                                                </div>
                                              </article>
                                            ))}
                                          </div>
                                        ) : null}

                                        <div className="semantic-seeds">
                                          <strong>Seed candidates для Wordstat</strong>
                                          <div>
                                            {semanticAnalysis.seedCandidates.slice(0, 10).map((seed) => (
                                              <span key={`${seed.clusterId}-${seed.phrase}`}>{seed.phrase}</span>
                                            ))}
                                          </div>
                                        </div>

                                        <div className="semantic-action-list">
                                          <strong>Следующие действия</strong>
                                          {semanticAnalysis.nextActions.map((action) => (
                                            <p key={action}>{action}</p>
                                          ))}
                                        </div>
                                      </>
                                    ) : (
                                      <div className="empty-state semantic-empty">
                                        <Search size={18} />
                                        Запусти semantic analysis: модуль прочитает HTML, JSON-контент, посчитает покрытие
                                        NodeDC-онтологии, gaps, legacy-блокеры и стартовые сиды для Wordstat.
                                      </div>
                                    )}
                                  </section>
                                ) : null}
                                {activeStageIndex === 4 ? (
                                  <section className="market-panel">
                                    <div className="market-heading">
                                      <div>
                                        <strong>Market enrichment</strong>
                                        <small>
                                          Платформенный слой для Wordstat/SERP evidence. Пользователь не настраивает MCP и
                                          внешние аккаунты вручную.
                                        </small>
                                      </div>
                                      <button
                                        className="tiny-action primary"
                                        disabled={!semanticAnalysis || isEnrichingMarket}
                                        onClick={() => void handleMarketEnrichment(project)}
                                        type="button"
                                      >
                                        {isEnrichingMarket ? <Loader2 className="spin" size={14} /> : <Search size={14} />}
                                        {isEnrichingMarket
                                          ? "Запускаю..."
                                          : marketEnrichment?.readiness.canCollectWordstat
                                            ? "Собрать Wordstat"
                                            : "Проверить providers"}
                                      </button>
                                    </div>

                                    {semanticAnalysis && marketEnrichment ? (
                                      <>
                                        <div className={`market-state-card ${marketEnrichment.state}`}>
                                          <div>
                                            <span>{getMarketStateLabel(marketEnrichment.state)}</span>
                                            <strong>
                                              Seeds: {marketEnrichment.readiness.seedCount} · Briefs:{" "}
                                              {marketEnrichment.readiness.briefCount}
                                            </strong>
                                            <small>
                                              Provider connected: {marketEnrichment.readiness.configuredProviderCount}/
                                              {marketEnrichment.providers.length} · user setup:{" "}
                                              {marketEnrichment.readiness.userSetupRequired ? "required" : "not required"}
                                            </small>
                                          </div>
                                          <p>
                                            {marketEnrichment.state === "not_configured"
                                              ? "Семантика готова к сбору рынка, но backend-провайдеры ещё не подключены."
                                              : "Внешние evidence будут прикрепляться к seed, brief и page contract отдельно от deterministic-анализа."}
                                          </p>
                                        </div>

                                        <div className="market-wordstat-card">
                                          <div>
                                            <strong>Wordstat job</strong>
                                            <small>
                                              {marketEnrichment.wordstat.latestJob
                                                ? `${marketEnrichment.wordstat.latestJob.status} · ${marketEnrichment.wordstat.latestJob.provider}/${marketEnrichment.wordstat.latestJob.mode} · ${marketEnrichment.wordstat.latestJob.requestedPhraseCount} seeds`
                                                : "job ещё не создавался"}
                                            </small>
                                          </div>
                                          <div>
                                            <span>{formatCount(marketEnrichment.wordstat.summary.collectedSeedCount)} checked</span>
                                            <span>
                                              {formatCount(marketEnrichment.wordstat.summary.exactFrequencySeedCount ?? null)} exact
                                            </span>
                                            <span>
                                              {formatCount(marketEnrichment.wordstat.summary.relatedOnlySeedCount ?? null)} related
                                            </span>
                                            <span>
                                              {formatCount(marketEnrichment.wordstat.summary.noSignalSeedCount ?? null)} empty
                                            </span>
                                            <span>{formatCount(marketEnrichment.wordstat.summary.resultCount)} rows</span>
                                            <span>{formatCount(marketEnrichment.wordstat.summary.totalFrequency)} total</span>
                                          </div>
                                          {marketEnrichment.wordstat.latestJob?.errorMessage ? (
                                            <p>{marketEnrichment.wordstat.latestJob.errorMessage}</p>
                                          ) : null}
                                        </div>

                                        <div className="market-provider-grid">
                                          {marketEnrichment.providers.map((provider) => (
                                            <article className={`market-provider ${provider.status}`} key={provider.id}>
                                              <span>{getMarketProviderStatusLabel(provider.status)}</span>
                                              <div>
                                                <strong>{provider.label}</strong>
                                                <small>{provider.role}</small>
                                                <p>{provider.message}</p>
                                              </div>
                                            </article>
                                          ))}
                                        </div>

                                        <div className="market-columns">
                                          <section>
                                            <div>
                                              <strong>Seed queue</strong>
                                              <small>Фразы готовы к Wordstat, но не собраны без provider.</small>
                                            </div>
                                            <div className="market-seed-list">
                                              {marketEnrichment.seedQueue.slice(0, 12).map((seed) => (
                                                <div key={`${seed.clusterId}-${seed.phrase}`}>
                                                  <span>{getMarketEvidenceStatusLabel(seed.evidenceStatus)}</span>
                                                  <strong>{seed.phrase}</strong>
                                                  <small>
                                                    {seed.frequency !== null
                                                      ? `${formatCount(seed.frequency)} · ${seed.frequencyGroup ?? "group n/a"} · related ${seed.relatedCount}`
                                                      : seed.relatedCount > 0
                                                        ? `${seed.clusterTitle} · related ${seed.relatedCount}`
                                                        : seed.clusterTitle}
                                                  </small>
                                                </div>
                                              ))}
                                            </div>
                                          </section>

                                          <section>
                                            <div>
                                              <strong>Brief evidence</strong>
                                              <small>Куда будут пришиты Wordstat/SERP/competitor evidence.</small>
                                            </div>
                                            <div className="market-brief-list">
                                              {marketEnrichment.briefEvidence.slice(0, 8).map((brief) => (
                                                <div key={brief.path}>
                                                  <span>{getMarketEvidenceStatusLabel(brief.evidenceStatus)}</span>
                                                  <strong>{brief.path}</strong>
                                                  <small>
                                                    quality {brief.qualityScore ?? "n/a"} · {brief.missingEvidence.slice(0, 2).join("; ")}
                                                  </small>
                                                </div>
                                              ))}
                                            </div>
                                          </section>
                                        </div>

                                        {marketEnrichment.wordstat.topResults.length > 0 ? (
                                          <div className="market-next-actions market-wordstat-results">
                                            <strong>Top Wordstat rows</strong>
                                            <div>
                                              {marketEnrichment.wordstat.topResults.slice(0, 10).map((result) => (
                                                <p key={`${result.sourcePhrase ?? "seed"}-${result.phrase}`}>
                                                  <span>{formatCount(result.frequency)}</span>
                                                  {result.phrase}
                                                </p>
                                              ))}
                                            </div>
                                          </div>
                                        ) : null}

                                        <div className="market-next-actions">
                                          <strong>Следующие platform actions</strong>
                                          {marketEnrichment.nextActions.map((action) => (
                                            <p key={action}>{action}</p>
                                          ))}
                                        </div>
                                      </>
                                    ) : semanticAnalysis ? (
                                      <div className="empty-state semantic-empty">
                                        <Search size={18} />
                                        Market contract ещё не загружен. Нажми проверку providers, чтобы получить текущий
                                        статус платформенных интеграций.
                                      </div>
                                    ) : (
                                      <div className="empty-state semantic-empty">
                                        <Search size={18} />
                                        Сначала нужен semantic analysis: он подготовит seed candidates и SEO briefs для
                                        market enrichment.
                                      </div>
                                    )}
                                  </section>
                                ) : null}
                                {scanSummary.pages.length > 0 ? (
                                  <div className="scan-selection">
                                    <div className="scan-selection-bar">
                                      <div>
                                        <strong>
                                          SEO scope: {scanScopeCounts?.selectedIndexable ?? 0}/
                                          {scanScopeCounts?.indexable ?? 0} посадочных
                                        </strong>
                                        <small>
                                          Исключено: шаблоны {scanScopeCounts?.template ?? 0}, админка{" "}
                                          {scanScopeCounts?.admin ?? 0}, технические {scanScopeCounts?.technical ?? 0}
                                          {scanScopeCounts && scanScopeCounts.selectedExcluded > 0
                                            ? ` · выбрано исключённых: ${scanScopeCounts.selectedExcluded}`
                                            : ""}
                                        </small>
                                      </div>
                                      <div className="scan-selection-actions">
                                        <button
                                          disabled={
                                            isSelectingPages ||
                                            ((scanScopeCounts?.selectedIndexable ?? 0) === (scanScopeCounts?.indexable ?? 0) &&
                                              (scanScopeCounts?.selectedExcluded ?? 0) === 0)
                                          }
                                          onClick={() => void handleApplyRecommendedPageScope(project, scanSummary)}
                                          type="button"
                                        >
                                          SEO scope
                                        </button>
                                        <button
                                          disabled={isSelectingPages || selectedPagesCount === 0}
                                          onClick={() =>
                                            void handleProjectPageSelection(
                                              project,
                                              scanSummary.pages.map((page) => page.pageId),
                                              false
                                            )
                                          }
                                          type="button"
                                        >
                                          Снять все
                                        </button>
                                      </div>
                                    </div>
                                    <div className={scanSummary.pages.length > 15 ? "scan-page-tree scrollable" : "scan-page-tree"}>
                                      {pageTreeGroups.map((group) => (
                                        <details
                                          className={`scan-page-tree-group ${group.key}`}
                                          key={group.name}
                                          open={group.defaultOpen}
                                        >
                                          <summary className="scan-page-tree-heading">
                                            <span>
                                              <strong>{group.name}</strong>
                                              <small>{group.description}</small>
                                            </span>
                                            <em>{group.pages.length} шт.</em>
                                          </summary>
                                          <div className="scan-page-list">
                                            {group.pages.map((page) => (
                                              <label
                                                className={`scan-page-option ${page.scope} ${
                                                  page.usedForCoverage ? "used-for-coverage" : "excluded-from-coverage"
                                                }`}
                                                key={`${scanSummary.id}-${page.pageId}`}
                                                title={page.scopeReason}
                                              >
                                                <input
                                                  checked={page.selected}
                                                  disabled={isSelectingPages || isScanning}
                                                  onChange={(event) =>
                                                    void handleProjectPageSelection(project, [page.pageId], event.target.checked)
                                                  }
                                                  type="checkbox"
                                                />
                                                <span>
                                                  <strong>
                                                    {getPageTitle(page)}
                                                    <em>{getScanPageScopeLabel(page.scope)}</em>
                                                  </strong>
                                                  <small>{getPagePathLabel(page)}</small>
                                                  <small>{page.scopeReason}</small>
                                                </span>
                                              </label>
                                            ))}
                                          </div>
                                        </details>
                                      ))}
                                    </div>
                                    <details className="workspace-help">
                                      <summary>Что дальше будет в рабочей зоне страницы?</summary>
                                      <p>
                                        Выбранные страницы пойдут в следующий экран: там появится locked original
                                        snapshot, рабочий draft, meta-поля, секции, видимый текст, media-контекст,
                                        будущие seeds/Wordstat/keyword map и безопасный diff перед Apply.
                                      </p>
                                    </details>
                                    <section className="page-workspace-panel">
                                      <div className="page-workspace-heading">
                                        <div>
                                          <strong>Рабочая зона выбранного scope</strong>
                                          <small>Original read-only, working draft сохраняется в Postgres + MinIO.</small>
                                        </div>
                                        <span>Apply пока не трогаем</span>
                                      </div>
                                      <div className="workspace-page-tabs">
                                        {selectedWorkspaceScopeItems.map((scopeItem) => {
                                          const workspaceKey = getWorkspaceKey(project.id, scopeItem.scopeId);
                                          const isActivePage = activeWorkspaceScopeItem?.scopeId === scopeItem.scopeId;
                                          const isOpening = busyWorkspaceKey === workspaceKey;

                                          return (
                                            <button
                                              className={isActivePage ? "active" : undefined}
                                              disabled={isOpening}
                                              key={`workspace-tab-${scopeItem.scopeId}`}
                                              onClick={() => void handleOpenPageWorkspace(project, scopeItem)}
                                              type="button"
                                            >
                                              {isOpening ? <Loader2 className="spin" size={14} /> : <FileText size={14} />}
                                              <span>{scopeItem.title}</span>
                                            </button>
                                          );
                                        })}
                                      </div>
                                      {activeWorkspace && activeWorkspaceScopeItem && activeWorkspaceKey ? (
                                        <div className="workspace-editor-stack workspace-cockpit">
                                          <div className="workspace-current-page">
                                            <div>
                                              <span>
                                                Активная страница {activeWorkspacePageIndex + 1}/{selectedWorkspaceScopeItems.length}
                                              </span>
                                              <strong>{activeWorkspaceScopeItem.title}</strong>
                                              <small>{activeWorkspaceScopeItem.pathLabel}</small>
                                            </div>
                                          </div>
                                          <section className={`workspace-preview-panel workspace-cockpit-panel ${activeWorkspaceDrawers.length > 0 ? "drawer-open" : ""}`}>
                                            <div className="workspace-cockpit-toolbar">
                                              <div className="workspace-preview-status">
                                                <span>Замечаний на активной странице: {activeWorkspaceIssues.length}</span>
                                                <span>В выбранных страницах: {selectedCoverageIssueCount}</span>
                                                <span>Warning: {activeWorkspaceIssueCounts.warnings} · Info: {activeWorkspaceIssueCounts.info}</span>
                                              </div>
                                              <div className="workspace-current-page-actions">
                                                <button
                                                  className={activeWorkspaceDrawers.includes("pages") ? "active" : undefined}
                                                  onClick={() => toggleWorkspaceDrawer(activeWorkspaceUiKey, "pages")}
                                                  type="button"
                                                >
                                                  <FileText size={14} />
                                                  Страница
                                                  <em>
                                                    {activeWorkspacePageIndex + 1}/{selectedWorkspaceScopeItems.length}
                                                  </em>
                                                  <ChevronDown size={14} />
                                                </button>
                                                <button
                                                  className={activeWorkspaceDrawers.includes("issues") ? "active" : undefined}
                                                  onClick={() => toggleWorkspaceDrawer(activeWorkspaceUiKey, "issues")}
                                                  type="button"
                                                >
                                                  <CircleAlert size={14} />
                                                  Ошибки
                                                  <em>{activeWorkspaceTargets.length}</em>
                                                </button>
                                                <div className="workspace-preview-mode-switch" aria-label="Режим зеркала">
                                                  {WORKSPACE_PREVIEW_MODES.map((mode) => (
                                                    <button
                                                      className={activeWorkspacePreviewMode === mode.id ? "active" : undefined}
                                                      key={mode.id}
                                                      onClick={() =>
                                                        changeWorkspacePreviewMode(
                                                          activeWorkspaceKey,
                                                          mode.id,
                                                          activeWorkspaceTargets,
                                                          selectedWorkspaceScopeItems,
                                                          activeWorkspaceScopeItem
                                                        )
                                                      }
                                                      type="button"
                                                    >
                                                      {mode.label}
                                                    </button>
                                                  ))}
                                                </div>
                                              </div>
                                            </div>
                                            <div className="workspace-preview-shell">
                                              <div className={`workspace-preview-frame ${activeWorkspacePreviewMode === "mobile" ? "mobile-preview" : "desktop-preview"}`}>
                                                <iframe
                                                  onLoad={() =>
                                                    handleWorkspacePreviewLoad(
                                                      activeWorkspaceKey,
                                                      activeWorkspaceTargets,
                                                      selectedWorkspaceScopeItems,
                                                      activeWorkspaceScopeItem
                                                    )
                                                  }
                                                  ref={(element) => {
                                                    if (element) {
                                                      previewFrameRefs.current = {
                                                        [activeWorkspaceKey]: element
                                                      };
                                                    } else {
                                                      delete previewFrameRefs.current[activeWorkspaceKey];
                                                    }
                                                  }}
                                                  sandbox="allow-scripts allow-forms allow-popups"
                                                  src={getPreviewPageUrl(
                                                    project.id,
                                                    activeWorkspacePreviewSourcePath,
                                                    activeWorkspacePreviewFocusSelector,
                                                    activeWorkspacePreviewFocusIndex
                                                  )}
                                                  title={`Предпросмотр ${activeWorkspaceScopeItem.title}`}
                                                />
                                                <div className="workspace-preview-markers">
                                                  {activeWorkspaceMarkers.map((marker) => {
                                                    const target = activeWorkspaceTargets.find((item) => item.id === marker.targetId);

                                                    if (!target) {
                                                      return null;
                                                    }

                                                    return (
                                                      <button
                                                        aria-label={`Открыть поле ${marker.label}`}
                                                        className={marker.targetId === activeWorkspaceTargetId ? "active" : undefined}
                                                        key={`preview-marker-${marker.targetId}`}
                                                        onClick={() => activateWorkspaceTarget(activeWorkspaceKey, target)}
                                                        style={{ left: marker.left, top: marker.top }}
                                                        title={marker.label}
                                                        type="button"
                                                      >
                                                        {marker.number}
                                                      </button>
                                                    );
                                                  })}
                                                </div>
                                              </div>
                                              {activeWorkspaceDrawers.length > 0 ? (
                                                <div className="workspace-drawer-stack">
                                                  {activeWorkspaceDrawers.map((activeWorkspaceDrawer) => {
                                                    const isPagesDrawer = activeWorkspaceDrawer === "pages";

                                                    return (
                                                      <aside className="workspace-drawer open" key={`workspace-drawer-${activeWorkspaceDrawer}`}>
                                                        <div className="workspace-drawer-header">
                                                          <div>
                                                            <span>{isPagesDrawer ? "Страницы и секции" : "Ошибки страницы"}</span>
                                                            <strong>
                                                              {isPagesDrawer
                                                                ? `${activeWorkspacePageIndex + 1}/${selectedWorkspaceScopeItems.length}`
                                                                : `${activeWorkspaceTargets.length} замечаний`}
                                                            </strong>
                                                            <small>
                                                              {isPagesDrawer
                                                                ? "Переключает preview и ошибки рабочей зоны."
                                                                : "Карточки относятся к активной странице."}
                                                            </small>
                                                          </div>
                                                          <button
                                                            aria-label="Закрыть панель"
                                                            className="workspace-drawer-close"
                                                            onClick={() => closeWorkspaceDrawer(activeWorkspaceUiKey, activeWorkspaceDrawer)}
                                                            type="button"
                                                          >
                                                            <X size={16} />
                                                          </button>
                                                        </div>
                                                        {isPagesDrawer ? (
                                                          selectedWorkspaceScopeItems.length > 0 ? (
                                                            <div className="workspace-page-menu-list workspace-drawer-page-list">
                                                              {selectedWorkspaceScopeItems.map((scopeItem, pageIndex) => {
                                                                const pageWorkspaceKey = getWorkspaceKey(project.id, scopeItem.scopeId);
                                                                const isActivePage = activeWorkspaceScopeItem?.scopeId === scopeItem.scopeId;
                                                                const isOpening = busyWorkspaceKey === pageWorkspaceKey;
                                                                const pageIssues = getPageIssues(scanSummary, scopeItem.pageId);

                                                                return (
                                                                  <button
                                                                    className={isActivePage ? "active" : undefined}
                                                                    disabled={isOpening}
                                                                    key={`workspace-page-drawer-${scopeItem.scopeId}`}
                                                                    onClick={() => {
                                                                      void handleOpenPageWorkspace(project, scopeItem, { silent: true });
                                                                    }}
                                                                    type="button"
                                                                  >
                                                                    <span>{pageIndex + 1}</span>
                                                                    <strong>{scopeItem.title}</strong>
                                                                    <small>{scopeItem.pathLabel}</small>
                                                                    <em>{pageIssues.length} замечаний</em>
                                                                  </button>
                                                                );
                                                              })}
                                                            </div>
                                                          ) : (
                                                            <small className="workspace-page-menu-empty">
                                                              В выбранном наборе пока нет рабочих страниц или секций.
                                                            </small>
                                                          )
                                                        ) : (
                                                          <div className="workspace-fields-drawer">
                                                            {activeWorkspaceTargets.length > 0 ? (
                                                              activeWorkspaceTargets.map((target) => {
                                                                const targetSectionText =
                                                                  workspaceSectionTexts[activeWorkspaceKey]?.[target.sectionId] ?? target.workingText;

                                                                return (
                                                                  <div
                                                                    className={
                                                                      activeWorkspaceTarget?.id === target.id
                                                                        ? "section-editor-card workspace-field-card active"
                                                                        : "section-editor-card workspace-field-card"
                                                                    }
                                                                    id={getWorkspaceFieldDomId(activeWorkspaceKey, target.id)}
                                                                    key={`workspace-field-${target.id}`}
                                                                  >
                                                                    <div className="workspace-subheading">
                                                                      <strong>
                                                                        {target.number}. {target.title}
                                                                      </strong>
                                                                      <small>{target.selector ?? "selector пока не задан"}</small>
                                                                    </div>
                                                                    {target.issues.length > 0 ? (
                                                                      <div className="workspace-target-issues">
                                                                        {target.issues.map((issue) => {
                                                                          const explanation = getAuditIssueExplanation(issue.code);

                                                                          return (
                                                                            <article
                                                                              className="workspace-target-issue"
                                                                              key={`target-issue-${target.id}-${issue.id}`}
                                                                            >
                                                                              <span>{getAuditSeverityLabel(issue.severity)}</span>
                                                                              <strong>{issue.message}</strong>
                                                                              <small>{issue.field ? `Поле: ${issue.field}` : "Медиа/страница"}</small>
                                                                              <p>{explanation.action}</p>
                                                                            </article>
                                                                          );
                                                                        })}
                                                                      </div>
                                                                    ) : null}
                                                                    <div className="workspace-field-meta">
                                                                      <span>{getSectionKindLabel(target.kind)}</span>
                                                                      <em>{target.workingText.length} симв.</em>
                                                                    </div>
                                                                    <label>
                                                                      <span>Original · только чтение</span>
                                                                      <pre>{target.originalText || "Пусто"}</pre>
                                                                    </label>
                                                                    <label>
                                                                      <span>Working · редактируется</span>
                                                                      <textarea
                                                                        onChange={(event) =>
                                                                          setWorkspaceSectionTexts((currentTexts) => ({
                                                                            ...currentTexts,
                                                                            [activeWorkspaceKey]: {
                                                                              ...(currentTexts[activeWorkspaceKey] ?? {}),
                                                                              [target.sectionId]: event.target.value
                                                                            }
                                                                          }))
                                                                        }
                                                                        value={targetSectionText}
                                                                      />
                                                                    </label>
                                                                    <div className="workspace-actions">
                                                                      <button
                                                                        className="tiny-action primary"
                                                                        disabled={isWorkspaceBusy}
                                                                        onClick={() =>
                                                                          void handleSavePageWorkspace(
                                                                            project.id,
                                                                            activeWorkspace.pageId,
                                                                            activeWorkspaceKey
                                                                          )
                                                                        }
                                                                        type="button"
                                                                      >
                                                                        {isWorkspaceBusy ? <Loader2 className="spin" size={14} /> : <Save size={14} />}
                                                                        Сохранить draft
                                                                      </button>
                                                                      <button
                                                                        className="tiny-action"
                                                                        disabled={isWorkspaceBusy}
                                                                        onClick={() =>
                                                                          setWorkspaceSectionTexts((currentTexts) => ({
                                                                            ...currentTexts,
                                                                            [activeWorkspaceKey]: {
                                                                              ...(currentTexts[activeWorkspaceKey] ?? {}),
                                                                              [target.sectionId]: target.originalText
                                                                            }
                                                                          }))
                                                                        }
                                                                        type="button"
                                                                      >
                                                                        <RefreshCw size={14} />
                                                                        Сбросить секцию
                                                                      </button>
                                                                    </div>
                                                                  </div>
                                                                );
                                                              })
                                                            ) : (
                                                              <small className="workspace-empty">Для этой страницы пока нет ошибок, связанных с полями.</small>
                                                            )}
                                                          </div>
                                                        )}
                                                      </aside>
                                                    );
                                                  })}
                                                </div>
                                              ) : null}
                                            </div>
                                          </section>
                                          {activeStageIndex > 2 ? (
                                            <details className="workspace-raw-draft">
                                              <summary>Технический draft целиком</summary>
                                              <pre>{activeWorkspaceText}</pre>
                                            </details>
                                          ) : null}
                                        </div>
                                      ) : (
                                        <small className="workspace-empty">
                                          {activeWorkspaceKey && isWorkspaceBusy
                                            ? "Открываю preview страницы и секции..."
                                            : "Выбери страницу выше, чтобы открыть preview и секционный редактор."}
                                        </small>
                                      )}
                                    </section>
                                  </div>
                                ) : (
                                  <small>HTML-страницы пока не найдены.</small>
                                )}
                                {scanSummary.audit ? (
                                  <div className="audit-summary">
                                    <div className="audit-heading">
                                      <strong>{getSelectedAuditResultMessage(selectedPagesCount, selectedAuditCounts)}</strong>
                                      <span>
                                        Блокеры: {selectedAuditCounts.errors} · Предупреждения:{" "}
                                        {selectedAuditCounts.warnings} · Инфо: {selectedAuditCounts.info}
                                      </span>
                                      <small>
                                        Всего по скану: замечаний {scanSummary.audit.counts.total}, предупреждений{" "}
                                        {scanSummary.audit.counts.warnings}, инфо {scanSummary.audit.counts.info}.
                                      </small>
                                    </div>
                                    <div className="audit-toolbar" aria-label="Фильтры ошибок">
                                      {(["all", "error", "warning", "info"] as AuditSeverityFilter[]).map((filter) => (
                                        <button
                                          className={auditSeverityFilter === filter ? "active" : undefined}
                                          disabled={selectedPagesCount === 0}
                                          key={filter}
                                          onClick={() => setAuditSeverityFilter(scanSummary.id, filter)}
                                          type="button"
                                        >
                                          {getAuditSeverityFilterLabel(filter)}
                                          <span>{getAuditFilterCount(selectedAuditAllCounts, filter)}</span>
                                        </button>
                                      ))}
                                    </div>
                                    <details className="audit-help">
                                      <summary>Что значат блокеры, предупреждения и инфо?</summary>
                                      <div className="audit-help-grid">
                                        <p>
                                          <strong>Блокер</strong> — жёсткая техническая проблема, которая позже должна
                                          остановить Apply: например, нельзя безопасно записать файл или конфликтует diff.
                                          Сейчас таких проверок почти нет, потому что мы ещё не меняем сайт.
                                        </p>
                                        <p>
                                          <strong>Предупреждение</strong> — SEO-риск: missing title/description/H1,
                                          отсутствующий ALT, повторяющиеся ALT. Title/description прямо важны для
                                          сниппета и диагностики Яндекса, а H1/heading order, ALT и медиа-поля держим
                                          как смысловую SEO/accessibility-подготовку для страниц, картинок и пользователя.
                                        </p>
                                        <p>
                                          <strong>Инфо</strong> — мягкая подсказка: например, короткий description,
                                          weak filename или видео без poster. Не всё нужно исправлять автоматически.
                                        </p>
                                      </div>
                                    </details>
                                    <details className="media-issue-panel" open>
                                      <summary>
                                        Медиа-ошибки выбранных страниц: {selectedMediaIssues.length}
                                        {auditSeverityFilter !== "all" ? ` · фильтр: ${getAuditSeverityFilterLabel(auditSeverityFilter)}` : ""}
                                      </summary>
                                      {selectedPagesCount === 0 ? (
                                        <small>Выбери страницы выше, и здесь появятся проблемы по изображениям/видео.</small>
                                      ) : selectedMediaIssues.length > 0 ? (
                                        <div className="media-issue-list">
                                          {selectedMediaIssues.map((issue) => (
                                            <div className="media-issue-row" key={`media-${issue.id}`}>
                                              <span>{getAuditSeverityLabel(issue.severity)}</span>
                                              <strong>{issue.message}</strong>
                                              <small>
                                                {issue.sourcePath}
                                                {issue.field ? ` · ${issue.field}` : ""}
                                              </small>
                                            </div>
                                          ))}
                                        </div>
                                      ) : (
                                        <small>По выбранным страницам медиа-ошибок с текущим фильтром нет.</small>
                                      )}
                                    </details>
                                    {scanSummary.audit.issues.length > 0 ? (
                                      <div className="audit-issue-list">
                                        <small className="audit-filter-note">
                                          Ниже показаны ошибки только выбранных страниц, в том же порядке, что и список выше.
                                        </small>
                                        {selectedPagesCount === 0 ? (
                                          <small className="audit-empty-section">
                                            Выбери страницы выше, и здесь появятся ошибки только по ним.
                                          </small>
                                        ) : auditIssueGroups.length > 0 ? (
                                          auditIssueGroups.map((group) => (
                                            <section className="audit-page-group" key={group.key}>
                                              <div className="audit-page-heading">
                                                <div>
                                                  <strong>{group.title}</strong>
                                                  <small>{group.subtitle}</small>
                                                </div>
                                                <span className="audit-page-tag selected">{group.issues.length} шт.</span>
                                              </div>
                                              {group.issues.map((issue) => {
                                                const explanation = getAuditIssueExplanation(issue.code);

                                                return (
                                                  <details className={`audit-issue ${issue.severity}`} key={issue.id}>
                                                    <summary>
                                                      <span>{getAuditSeverityLabel(issue.severity)}</span>
                                                      <strong>{issue.message}</strong>
                                                    </summary>
                                                    <div className="audit-issue-body">
                                                      <small>
                                                        Код: {issue.code} · Область: {getAuditScopeLabel(issue)}
                                                        {issue.sourcePath ? ` · Источник: ${issue.sourcePath}` : ""}
                                                      </small>
                                                      <p>
                                                        <strong>Почему важно:</strong> {explanation.why}
                                                      </p>
                                                      <p>
                                                        <strong>Что делать:</strong> {explanation.action}
                                                      </p>
                                                    </div>
                                                  </details>
                                                );
                                              })}
                                            </section>
                                          ))
                                        ) : (
                                          <small className="audit-empty-section">
                                            По выбранным страницам в базовом аудите ошибок не найдено.
                                          </small>
                                        )}
                                        {auditIssueTotal > AUDIT_ISSUE_PAGE_SIZE ? (
                                          <div className="audit-pagination">
                                            <small>
                                              Показано {visibleAuditIssues.length} ошибок выбранных страниц из {auditIssueTotal}.
                                            </small>
                                            <div>
                                              {auditIssueLimit < auditIssueTotal ? (
                                                <>
                                                  <button
                                                    onClick={() =>
                                                      setAuditIssuesVisibleCount(
                                                        scanSummary.id,
                                                        Math.min(auditIssueLimit + AUDIT_ISSUE_PAGE_SIZE, auditIssueTotal)
                                                      )
                                                    }
                                                    type="button"
                                                  >
                                                    Показать ещё 8
                                                  </button>
                                                  <button
                                                    onClick={() => setAuditIssuesVisibleCount(scanSummary.id, auditIssueTotal)}
                                                    type="button"
                                                  >
                                                    Показать все
                                                  </button>
                                                </>
                                              ) : (
                                                <button
                                                  onClick={() =>
                                                    setAuditIssuesVisibleCount(scanSummary.id, AUDIT_ISSUE_PAGE_SIZE)
                                                  }
                                                  type="button"
                                                >
                                                  Свернуть до 8
                                                </button>
                                              )}
                                            </div>
                                          </div>
                                        ) : null}
                                      </div>
                                    ) : (
                                      <small>Пока всё спокойно: базовый аудит не нашёл явных проблем.</small>
                                    )}
                                  </div>
                                ) : (
                                  <small>Базовый аудит появится после следующего анализа проекта.</small>
                                )}
                              </div>
                            ) : null}
                            {projectActionProjectId === project.id && projectActionError ? (
                              <p className="form-message error project-action-message project-row-action-message">
                                <CircleAlert size={16} />
                                <span>{projectActionError}</span>
                              </p>
                            ) : null}
                            {projectActionProjectId === project.id && projectActionMessage ? (
                              <p className="form-message success project-action-message project-row-action-message">
                                <CheckCircle2 size={16} />
                                <span>{projectActionMessage}</span>
                              </p>
                            ) : null}
                            {pendingDeleteProjectId === project.id ? (
                              <div className="project-delete-confirmation" role="alert">
                                <div>
                                  <strong>Удалить проект «{project.name}»?</strong>
                                  <span>Проект исчезнет из списка, связанные данные импорта будут удалены.</span>
                                </div>
                                <div className="project-delete-actions">
                                  <button
                                    className="tiny-action"
                                    disabled={busyProjectId === project.id}
                                    onClick={cancelProjectDelete}
                                    type="button"
                                  >
                                    Отмена
                                  </button>
                                  <button
                                    className="tiny-action danger confirm-delete-action"
                                    disabled={busyProjectId === project.id}
                                    onClick={() => void handleProjectDelete(project)}
                                    type="button"
                                  >
                                    {busyProjectId === project.id ? <Loader2 className="spin" size={14} /> : <Trash2 size={14} />}
                                    Удалить
                                  </button>
                                </div>
                              </div>
                            ) : null}
                          </div>
                        <div className="project-meta">
                          <span className="project-site-title">{projectSiteTitle}</span>
                          {activeStageIndex === 0 ? (
                            <div className="project-actions">
                              <button
                                aria-label={`Скопировать проект ${project.name}`}
                                className="tiny-action icon-only"
                                disabled={busyProjectId === project.id || isScanning || isSelectingPages}
                                onClick={() => void handleProjectCopy(project)}
                                title="Скопировать проект"
                                type="button"
                              >
                                {busyProjectId === project.id ? <Loader2 className="spin" size={14} /> : <Copy size={14} />}
                              </button>
                              <button
                                aria-label={`Удалить проект ${project.name}`}
                                className="tiny-action danger icon-only"
                                disabled={busyProjectId === project.id || isScanning || isSelectingPages}
                                onClick={() => requestProjectDelete(project)}
                                title="Удалить проект"
                                type="button"
                              >
                                {busyProjectId === project.id ? <Loader2 className="spin" size={14} /> : <Trash2 size={14} />}
                              </button>
                            </div>
                          ) : null}
                        </div>
                      </article>
                    );
                  })}
                </div>
              )}
            </article>
          </div>
        </section>

        {workspaceMode === "dev" && activeStageIndex > 3 ? (
          <section className="stage-placeholder">
            <div className="section-heading">
              <FolderCheck size={20} />
              <div>
                <h3>{activeStage.sectionTitle}</h3>
                <p>{activeStage.description}</p>
              </div>
            </div>
            <div className="empty-state">
              <CircleAlert size={18} />
              Этот этап уже есть в навигации, но рабочий экран подключим после завершения текущей цепочки:
              источник → скан → аудит → рабочая зона → semantic analysis.
            </div>
          </section>
        ) : null}

        <footer className="footer-line">
          <GitBranch size={16} />
          <span>ветка main готова к дальнейшей реализации</span>
        </footer>
      </main>
    </div>
  );
}
