Skip to content

HR Interview Mode — RECRUITER role, candidate CRM, pipeline kanban, live-coding#13

Open
SomeoneUnlicensed wants to merge 1 commit into
mainfrom
feature/hr-interview-mode
Open

HR Interview Mode — RECRUITER role, candidate CRM, pipeline kanban, live-coding#13
SomeoneUnlicensed wants to merge 1 commit into
mainfrom
feature/hr-interview-mode

Conversation

@SomeoneUnlicensed

Copy link
Copy Markdown
Owner

HR Interview Mode

Полноценный модуль для технических собеседований прямо на платформе ЛитКот.

Что сделано

Prisma Schema:

  • Роль RECRUITER
  • Модели: Candidate, InterviewSession, InterviewChallenge, InterviewEvent, InterviewNote
  • Pipeline: SCREENING → TECH_INTERVIEW → SYSTEM_DESIGN → FINAL_INTERVIEW → OFFER → REJECTED

Админка /dashboard/hr:

  • Обзор — статистика
  • Кандидаты — список + детальная карточка с переключением этапа
  • Воронка — канбан drag-and-drop (6 этапов)
  • Задачи — банк активных челленджей
  • Интервью — создание (выбор кандидата + задачи + время), детали, live-просмотр кода

Комната кандидата /interview/[token]:

  • Таймер, переключение задач, редактор кода, запуск тестов
  • Автосохранение каждые 3с через API
  • Принудительная отправка по таймауту

Live-просмотр:

  • Polling каждые 2с для рекрутера
  • Лог событий (запуски, сохранения, отправки)
  • Заметки рекрутера по сессии

Техдолг (на потом)

  • Заменить polling на WebSocket
  • Monaco Editor вместо textarea
  • Hidden test cases для интервьюера
  • Email-уведомления

…ban, live coding view, interview room

- Добавлена роль RECRUITER в Prisma enum
- Модели: Candidate, InterviewSession, InterviewChallenge, InterviewEvent, InterviewNote
- Админ-панель HR: дашборд, candidates (list+detail+pipeline actions), pipeline (kanban drag-and-drop), task bank, interviews (create+detail+live)
- Комната кандидата /interview/[token] с таймером, редактором, запуском тестов, автосохранением
- Live-просмотр кода для рекрутера с polling (поэтапная замена на WS)
@SomeoneUnlicensed

Copy link
Copy Markdown
Owner Author

/copilot review

@SomeoneUnlicensed

Copy link
Copy Markdown
Owner Author

🦅 Кондор-Ревьюер — автоматическая проверка кода

Рейтинг: CHANGES_REQUESTED

Что проверено:

  • 🔒 Безопасность: Проверка авторизации есть, но отсутствует валидация входных данных в server actions (кроме createCandidate). Есть потенциальная проблема с stage as any — это отключает проверку типов.
  • ⚙️ Логика: В целом логика верная. Но есть несколько моментов: отсутствие обработки ошибок в server actions (кроме createCandidate), неоптимальный polling в live-view.tsx, дублирование stageColors.
  • 🎨 Стиль/Качество: Код читаемый, но есть дублирование (stageColors), неиспользуемые импорты, и потенциально опасное приведение типов.
  • 🧪 Тесты: Тесты отсутствуют.

Найденные проблемы:

  1. updateCandidateStage (строка 12): stage as any — опасное приведение типов.

    • Файл: apps/admin/src/app/dashboard/hr/candidates/[id]/update-stage.action.ts
    • Проблема: pipelineStage в Prisma, скорее всего, имеет тип enum. Приведение stage as any полностью отключает проверку типов TypeScript. Это может привести к сохранению невалидных значений в БД.
    • Решение: Использовать корректный тип. Например, если в Prisma это enum PipelineStage, то нужно импортировать его и использовать: data: { pipelineStage: stage as PipelineStage }. Или, как минимум, добавить валидацию через Zod.
  2. Отсутствие обработки ошибок в server actions (кроме createCandidate).

    • Файлы: update-stage.action.ts, save-note.action.ts
    • Проблема: Если prisma.candidate.update или prisma.interviewNote.create выбросят исключение (например, из-за нарушения уникальности или проблем с БД), то action упадёт с 500 ошибкой. Пользователь увидит только "Ошибка" в тосте, без конкретной информации.
    • Решение: Обернуть логику в try...catch и возвращать объект с success: false и сообщением об ошибке. Например:
      try {
        await prisma.candidate.update({...});
        return { success: true };
      } catch (error) {
        console.error("Failed to update candidate stage:", error);
        return { success: false, error: "Не удалось обновить этап" };
      }
  3. Дублирование stageColors в page.tsx и columns.tsx.

    • Файлы: apps/admin/src/app/dashboard/hr/candidates/[id]/page.tsx (строка 10-17), apps/admin/src/app/dashboard/hr/candidates/columns.tsx (строка 17-24)
    • Проблема: Один и тот же объект определён дважды. Это нарушает принцип DRY (Don't Repeat Yourself). При добавлении нового этапа нужно будет менять в двух местах.
    • Решение: Вынести stageColors в общий конфигурационный файл (например, apps/admin/src/app/dashboard/hr/candidates/stage-config.ts) и импортировать его в оба файла.
  4. Неоптимальный polling в live-view.tsx.

    • Файл: apps/admin/src/app/dashboard/hr/interviews/[id]/live/live-view.tsx
    • Проблема: Polling каждые 2 секунды — это нормально для MVP, но может создать лишнюю нагрузку на сервер. Кроме того, нет обработки ошибок при fetch (пустой catch {}).
    • Решение: Рассмотреть использование WebSockets или Server-Sent Events (SSE) для real-time обновлений. Если polling остаётся, то добавить обработку ошибок в catch (например, увеличить интервал при ошибке) и очищать интервал при размонтировании компонента (сейчас это делается, но можно улучшить).
  5. Неиспользуемый импорт cn и buttonVariants в InterviewDetailPage.

    • Файл: apps/admin/src/app/dashboard/hr/interviews/[id]/page.tsx (строка 9-10)
    • Проблема: Импортированы cn и buttonVariants, но они не используются в коде. Это засоряет код и может сбивать с толку.
    • Решение: Удалить неиспользуемые импорты.

Предлагаемые фиксы:

  1. Исправить updateCandidateStage:

    • Импортировать PipelineStage из @prisma/client (или @repo/db).
    • Изменить data: { pipelineStage: stage as any } на data: { pipelineStage: stage as PipelineStage }.
    • Добавить валидацию через Zod: z.enum(['SCREENING', 'TECH_INTERVIEW', ...]).
  2. Добавить обработку ошибок в server actions:

    • Обернуть логику в try...catch.
    • Возвращать { success: false, error: "Сообщение об ошибке" } в случае неудачи.
    • В клиентском коде (actions.tsx) обрабатывать result.error и показывать его в тосте.
  3. Вынести stageColors в общий файл:

    • Создать apps/admin/src/app/dashboard/hr/candidates/stage-config.ts.
    • Экспортировать stageColors и, возможно, массив stages.
    • Импортировать в page.tsx и columns.tsx.
  4. Улучшить polling:

    • Добавить обработку ошибок в catch блока poll функции.
    • Рассмотреть возможность увеличения интервала polling при ошибках.
    • (Опционально) Изучить возможность использования WebSockets.
  5. Удалить неиспользуемые импорты:

    • Удалить import { cn } from '@repo/ui/cn'; и import { buttonVariants } from '@repo/ui/components/button'; из InterviewDetailPage.

Дополнительные рекомендации:

  • Добавить больше тестов (unit и e2e) для критических путей (создание кандидата, изменение этапа, live-coding).
  • Рассмотреть возможность добавления пагинации на страницу списка кандидатов, если их может быть много.
  • В live-view.tsx можно добавить индикатор загрузки при первом запросе данных.
  • Убедиться, что NEXT_PUBLIC_APP_URL корректно настроен в production.

Итог: PR в целом хороший, функциональность реализована правильно. Однако есть несколько критических замечаний (безопасность типов, обработка ошибок) и несколько улучшений (дублирование, неиспользуемые импорты). Рекомендую исправить их перед мержем.


🤖 Кондор-Ревьюер | Rating: CHANGES_REQUESTED | Полная история ревью: /memory_reviewer

@SomeoneUnlicensed

Copy link
Copy Markdown
Owner Author

🦅 Кондор-Ревьюер — автоматическая проверка кода

Рейтинг: CHANGES_REQUESTED

Что проверено:

  • 🔒 Безопасность: Проверка авторизации через assertAdminOrRecruiter присутствует во всех server actions. Нет защиты от CSRF (Server Actions защищены по умолчанию). Нет валидации входных данных в updateCandidateStage и saveNote.
  • ⚙️ Логика: В целом логика корректна. Есть потенциальные проблемы с типизацией, обработкой ошибок и производительностью.
  • 🎨 Стиль/Качество: Код читаемый, но есть дублирование (stageColors), неиспользуемые импорты, потенциально опасные конструкции.
  • 🧪 Тесты: Тесты отсутствуют.

Найденные проблемы:

  1. apps/admin/src/app/dashboard/hr/candidates/[id]/update-stage.action.ts (строка 12): Использование stage as any для приведения типа string к полю pipelineStage. Это отключает проверку типов TypeScript и может привести к ошибкам во время выполнения, если будет передан невалидный этап. Необходимо использовать Zod-схему для валидации или явное приведение к допустимому типу.

  2. apps/admin/src/app/dashboard/hr/candidates/[id]/actions.tsx (строка 14): В handleStageChange нет обработки ошибок. Если updateCandidateStage выбросит исключение (например, из-за проблем с БД или авторизацией), то toast с ошибкой не покажется, и пользователь не узнает о проблеме. Нужно обернуть вызов в try...catch.

  3. apps/admin/src/app/dashboard/hr/interviews/[id]/live/live-view.tsx (строка 37): Использование any[] для типа events. Это снижает типобезопасность. Необходимо определить и использовать конкретный тип для событий.

  4. apps/admin/src/app/dashboard/hr/interviews/[id]/live/live-view.tsx (строка 42): Polling каждые 2 секунды без обработки ошибок. Если API вернет ошибку, fetch выбросит исключение, которое будет поймано пустым catch {}, и пользователь не увидит индикации проблемы. Нужно добавить обработку ошибок и, возможно, индикатор состояния.

  5. apps/admin/src/app/dashboard/hr/interviews/[id]/live/page.tsx (строка 28): Использование JSON.parse(JSON.stringify(interview)) для передачи данных из серверного компонента в клиентский. Это хак, который может привести к потере типов и проблемам с сериализацией (например, Date). Лучше использовать structuredClone или передавать данные через пропсы, предварительно сериализовав их.

  6. apps/admin/src/app/dashboard/hr/candidates/columns.tsx (строка 5): Неиспользуемый импорт Button из @repo/ui/components/button. Его следует удалить.

  7. apps/admin/src/app/dashboard/hr/candidates/[id]/page.tsx (строка 5): Неиспользуемый импорт Button из @repo/ui/components/button. Его следует удалить.

  8. apps/admin/src/app/dashboard/hr/candidates/[id]/page.tsx (строка 6): Неиспользуемый импорт Link из next/link. Его следует удалить.

  9. apps/admin/src/app/dashboard/hr/candidates/[id]/page.tsx (строка 7): Неиспользуемый импорт cn из @repo/ui/cn. Его следует удалить.

  10. apps/admin/src/app/dashboard/hr/interviews/[id]/page.tsx (строка 7): Неиспользуемый импорт cn из @repo/ui/cn. Его следует удалить.

Предлагаемые фиксы:

  1. update-stage.action.ts:

    // Вместо stage as any
    const validStages = ['SCREENING', 'TECH_INTERVIEW', 'SYSTEM_DESIGN', 'FINAL_INTERVIEW', 'OFFER', 'REJECTED'] as const;
    type Stage = typeof validStages[number];
    
    export async function updateCandidateStage(candidateId: string, stage: string) {
      const session = await auth();
      assertAdminOrRecruiter(session);
    
      if (!validStages.includes(stage as Stage)) {
        return { success: false, error: 'Invalid stage' };
      }
    
      await prisma.candidate.update({
        where: { id: candidateId },
        data: { pipelineStage: stage as Stage },
      });
    
      revalidatePath('/dashboard/hr');
      return { success: true };
    }
  2. actions.tsx:

    const handleStageChange = (stage: string) => {
      startTransition(async () => {
        try {
          const result = await updateCandidateStage(candidateId, stage);
          if (result.success) {
            toast({ title: 'Этап обновлён' });
          } else {
            toast({ title: result.error || 'Ошибка', variant: 'destructive' });
          }
        } catch (error) {
          toast({ title: 'Ошибка при обновлении этапа', variant: 'destructive' });
        }
      });
    };
  3. live-view.tsx:

    // Определить тип для событий
    type InterviewEvent = {
      timestamp: string;
      type: string;
      // ... другие поля
    };
    
    // Вместо any[]
    const [events, setEvents] = useState<InterviewEvent[]>([]);
  4. live-view.tsx:

    const poll = async () => {
      try {
        const res = await fetch(`/api/interview/poll?id=${interview.id}`);
        if (!res.ok) {
          throw new Error(`HTTP error! status: ${res.status}`);
        }
        const data = await res.json();
        if (data.code) setCode(data.code);
        if (data.events) setEvents(data.events.slice(-100));
      } catch (error) {
        console.error('Polling error:', error);
        // Можно показать toast или индикатор ошибки
      }
    };
  5. live/page.tsx:

    // Вместо JSON.parse(JSON.stringify(interview))
    const serializedInterview = {
      ...interview,
      createdAt: interview.createdAt.toISOString(),
      // ... сериализовать другие поля Date, если есть
    };
    return <LiveCodingView interview={serializedInterview} />;
  6. columns.tsx: Удалить строку import { Button } from '@repo/ui/components/button';.

  7. candidates/[id]/page.tsx: Удалить строки import { Button } from '@repo/ui/components/button';, import Link from 'next/link';, import { cn } from '@repo/ui/cn';.

  8. interviews/[id]/page.tsx: Удалить строку import { cn } from '@repo/ui/cn';.

Дополнительные рекомендации:

  • Рассмотреть возможность вынесения stageColors в общий конфигурационный файл, чтобы избежать дублирования.
  • Добавить больше unit-тестов для server actions и компонентов.
  • Для polling в live-view.tsx рассмотреть использование WebSockets или Server-Sent Events для более эффективной и своевременной передачи данных.

Итог: PR вносит значительный функционал, но содержит несколько критических проблем, связанных с типобезопасностью, обработкой ошибок и неиспользуемым кодом. Рекомендуется исправить указанные проблемы перед мержем.


🤖 Кондор-Ревьюер | Rating: CHANGES_REQUESTED | Полная история ревью: /memory_reviewer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant