# API Статистики v2 - Документация для Frontend ## Обзор Эндпоинт `/challenge/stats/v2` предоставляет расширенную статистику системы проверки заданий с детальными данными для построения таблиц и прогресс-баров. ## Эндпоинт ``` GET /challenge/stats/v2 ``` ### Аутентификация Не требуется. ### Параметры запроса | Параметр | Тип | Обязательный | Описание | |----------|-----|--------------|----------| | `chainId` | string | Нет | ID цепочки для фильтрации статистики. Если указан, возвращается статистика только по заданиям из этой цепочки | #### Примеры использования Получить полную статистику: ``` GET /challenge/stats/v2 ``` Получить статистику только по одной цепочке: ``` GET /challenge/stats/v2?chainId=507f1f77bcf86cd799439031 ``` ## Структура ответа ```typescript { success: true, body: { // ========== БАЗОВАЯ СТАТИСТИКА (из v1) ========== users: number, // Общее количество пользователей tasks: number, // Общее количество заданий chains: number, // Общее количество цепочек submissions: { total: number, // Всего попыток accepted: number, // Принятых rejected: number, // Отклоненных pending: number, // Ожидающих проверки inProgress: number // В процессе проверки }, averageCheckTimeMs: number, // Среднее время проверки в мс queue: { queueLength: number, waiting: number, inProgress: number, maxConcurrency: number, currentlyProcessing: number }, // ========== НОВЫЕ ДАННЫЕ V2 ========== // Таблица заданий с детальной статистикой tasksTable: Array<{ taskId: string, title: string, totalAttempts: number, // Всего попыток по всем пользователям uniqueUsers: number, // Количество уникальных пользователей acceptedCount: number, // Количество успешных прохождений successRate: number, // Процент успешного прохождения (0-100) averageAttemptsToSuccess: number // Среднее количество попыток до успеха }>, // Активные участники с прогрессом activeParticipants: Array<{ userId: string, nickname: string, totalSubmissions: number, // Всего попыток пользователя completedTasks: number, // Завершенных заданий chainProgress: Array<{ // Прогресс по каждой цепочке chainId: string, chainName: string, totalTasks: number, // Всего заданий в цепочке completedTasks: number, // Завершено заданий progressPercent: number // Процент прохождения (0-100) }> }>, // Детальная информация по цепочкам chainsDetailed: Array<{ chainId: string, name: string, totalTasks: number, tasks: Array<{ // Список заданий в цепочке taskId: string, title: string, description: string }>, participantProgress: Array<{ // Прогресс каждого участника userId: string, nickname: string, taskProgress: Array<{ // Статус по каждому заданию taskId: string, taskTitle: string, status: 'not_started' | 'pending' | 'in_progress' | 'needs_revision' | 'completed' }>, completedCount: number, // Завершено заданий progressPercent: number // Процент прохождения (0-100) }> }> } } ``` ## Пример ответа ```json { "success": true, "body": { "users": 34, "tasks": 22, "chains": 2, "submissions": { "total": 39, "accepted": 16, "rejected": 23, "pending": 0, "inProgress": 0 }, "averageCheckTimeMs": 2043, "queue": { "queueLength": 0, "waiting": 0, "inProgress": 0, "maxConcurrency": 1, "currentlyProcessing": 0 }, "tasksTable": [ { "taskId": "507f1f77bcf86cd799439011", "title": "Создание REST API", "totalAttempts": 45, "uniqueUsers": 12, "acceptedCount": 8, "successRate": 67, "averageAttemptsToSuccess": 2.3 }, { "taskId": "507f1f77bcf86cd799439012", "title": "Работа с базой данных", "totalAttempts": 38, "uniqueUsers": 10, "acceptedCount": 6, "successRate": 60, "averageAttemptsToSuccess": 3.1 } ], "activeParticipants": [ { "userId": "507f1f77bcf86cd799439021", "nickname": "student1", "totalSubmissions": 15, "completedTasks": 5, "chainProgress": [ { "chainId": "507f1f77bcf86cd799439031", "chainName": "Основы Backend", "totalTasks": 10, "completedTasks": 5, "progressPercent": 50 }, { "chainId": "507f1f77bcf86cd799439032", "chainName": "Frontend разработка", "totalTasks": 8, "completedTasks": 0, "progressPercent": 0 } ] } ], "chainsDetailed": [ { "chainId": "507f1f77bcf86cd799439031", "name": "Основы Backend", "totalTasks": 10, "tasks": [ { "taskId": "507f1f77bcf86cd799439011", "title": "Создание REST API", "description": "Создайте простой REST API с использованием Express.js" }, { "taskId": "507f1f77bcf86cd799439012", "title": "Работа с базой данных", "description": "Интегрируйте MongoDB в ваше приложение" } ], "participantProgress": [ { "userId": "507f1f77bcf86cd799439021", "nickname": "student1", "taskProgress": [ { "taskId": "507f1f77bcf86cd799439011", "taskTitle": "Создание REST API", "status": "completed" }, { "taskId": "507f1f77bcf86cd799439012", "taskTitle": "Работа с базой данных", "status": "needs_revision" } ], "completedCount": 1, "progressPercent": 10 } ] } ] } } ``` ## Фильтрация по цепочке При передаче параметра `chainId`: 1. **tasksTable** - содержит только задания из указанной цепочки 2. **activeParticipants** - включает только участников, которые делали попытки по заданиям этой цепочки. В `chainProgress` будет информация только об указанной цепочке 3. **chainsDetailed** - содержит информацию только об указанной цепочке ### Пример фильтрованного ответа ```bash curl http://localhost:3000/challenge/stats/v2?chainId=507f1f77bcf86cd799439031 ``` ```json { "success": true, "body": { "users": 34, "tasks": 22, "chains": 2, "submissions": { "total": 39, "accepted": 16, "rejected": 23, "pending": 0, "inProgress": 0 }, "averageCheckTimeMs": 2043, "queue": { ... }, "tasksTable": [ // Только задания из цепочки 507f1f77bcf86cd799439031 { "taskId": "507f1f77bcf86cd799439011", "title": "Создание REST API", "totalAttempts": 45, "uniqueUsers": 12, "acceptedCount": 8, "successRate": 67, "averageAttemptsToSuccess": 2.3 } ], "activeParticipants": [ // Только участники с попытками в этой цепочке { "userId": "507f1f77bcf86cd799439021", "nickname": "student1", "totalSubmissions": 10, "completedTasks": 3, "chainProgress": [ // Только одна цепочка { "chainId": "507f1f77bcf86cd799439031", "chainName": "Основы Backend", "totalTasks": 10, "completedTasks": 3, "progressPercent": 30 } ] } ], "chainsDetailed": [ // Только указанная цепочка { "chainId": "507f1f77bcf86cd799439031", "name": "Основы Backend", "totalTasks": 10, "tasks": [...], "participantProgress": [...] } ] } } ``` ## Использование в UI компонентах ### 1. Таблица заданий Используйте `tasksTable` для отображения статистики по каждому заданию: ```tsx // React пример function TasksTable({ tasksTable }) { return ( {tasksTable.map(task => ( ))}
Название задания Попыток Уникальных пользователей Успешно завершено % успеха Среднее попыток до успеха
{task.title} {task.totalAttempts} {task.uniqueUsers} {task.acceptedCount} {task.successRate}% {task.averageAttemptsToSuccess.toFixed(1)}
); } ``` ### 2. Прогресс-бары участников Используйте `activeParticipants` для отображения прогресса каждого участника: ```tsx // React пример function ParticipantProgress({ activeParticipants }) { return (
{activeParticipants.map(participant => (

{participant.nickname}

Завершено заданий: {participant.completedTasks}

Всего попыток: {participant.totalSubmissions}

{participant.chainProgress.map(chain => (
{chain.chainName} {chain.completedTasks}/{chain.totalTasks}
{chain.progressPercent}%
))}
))}
); } ``` ### 3. Детальный прогресс по цепочкам Используйте `chainsDetailed` для отображения детального прогресса всех участников в рамках каждой цепочки: ```tsx // React пример function ChainDetailedView({ chainsDetailed }) { return (
{chainsDetailed.map(chain => (

{chain.name}

Заданий в цепочке: {chain.totalTasks}

{/* Список заданий */}

Задания:

{chain.tasks.map(task => (

{task.title}

{task.description}

))}
{/* Прогресс участников */}

Прогресс участников:

{chain.tasks.map(task => ( ))} {chain.participantProgress.map(participant => ( {participant.taskProgress.map(taskProg => ( ))} ))}
Участник{task.title}Прогресс
{participant.nickname} {participant.progressPercent}%
))}
); } // Компонент для отображения статуса function StatusBadge({ status }) { const statusConfig = { 'not_started': { label: 'Не начато', color: 'gray' }, 'pending': { label: 'Ожидает', color: 'yellow' }, 'in_progress': { label: 'В процессе', color: 'blue' }, 'needs_revision': { label: 'Доработка', color: 'orange' }, 'completed': { label: 'Завершено', color: 'green' } }; const config = statusConfig[status]; return ( {config.label} ); } ``` ## Рекомендации по использованию ### Производительность - Эндпоинт выполняет множество агрегаций, поэтому может работать медленно при большом количестве данных - Рекомендуется кэшировать результат на клиенте на 30-60 секунд - Используйте loading индикаторы во время загрузки данных ```tsx // Пример с кэшированием function useStatsV2(chainId?: string) { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const lastFetch = useRef(0); const fetchStats = async (force = false) => { const now = Date.now(); // Кэш на 60 секунд if (!force && data && (now - lastFetch.current) < 60000) { return data; } setLoading(true); try { const url = chainId ? `/challenge/stats/v2?chainId=${chainId}` : '/challenge/stats/v2'; const response = await fetch(url); const result = await response.json(); if (result.success) { setData(result.body); lastFetch.current = now; } } catch (err) { setError(err); } finally { setLoading(false); } }; useEffect(() => { fetchStats(); }, [chainId]); // Перезагружаем при смене цепочки return { data, loading, error, refetch: () => fetchStats(true) }; } // Использование function ChainStatistics() { const [selectedChainId, setSelectedChainId] = useState(); const { data, loading } = useStatsV2(selectedChainId); if (loading) return
Загрузка...
; return (
); } ``` ### Фильтрация и сортировка Все массивы данных можно фильтровать и сортировать на клиенте: ```tsx // Сортировка таблицы заданий по проценту успеха const sortedTasks = [...tasksTable].sort((a, b) => b.successRate - a.successRate); // Фильтрация активных участников с прогрессом > 50% const activeStudents = activeParticipants.filter(p => p.chainProgress.some(c => c.progressPercent > 50) ); // Поиск участника по имени const searchParticipant = (query) => activeParticipants.filter(p => p.nickname.toLowerCase().includes(query.toLowerCase()) ); ``` ### Визуализация данных Для построения графиков и диаграмм используйте библиотеки типа: - **Chart.js** / **Recharts** - для графиков прогресса - **AG Grid** / **TanStack Table** - для таблиц с сортировкой - **React Progress Bar** - для прогресс-баров ```tsx // Пример с Chart.js import { Bar } from 'react-chartjs-2'; function TasksSuccessChart({ tasksTable }) { const data = { labels: tasksTable.map(t => t.title), datasets: [{ label: 'Процент успеха', data: tasksTable.map(t => t.successRate), backgroundColor: 'rgba(75, 192, 192, 0.6)', }] }; return ; } ``` ## Когда использовать фильтрацию по цепочке ### Используйте `chainId` когда: 1. **Страница отдельной цепочки** - пользователь просматривает детали конкретной цепочки ```tsx function ChainDetailsPage({ chainId }) { const { data } = useStatsV2(chainId); return ; } ``` 2. **Улучшение производительности** - когда нужны данные только по одной цепочке, фильтрация на сервере работает быстрее 3. **Фокус на конкретной программе** - преподаватель хочет видеть прогресс по конкретному курсу/модулю ### НЕ используйте `chainId` когда: 1. **Общий дашборд** - нужна статистика по всем цепочкам 2. **Сравнение цепочек** - нужно показать метрики по всем цепочкам одновременно 3. **Общая аналитика** - нужны агрегированные данные по всей системе ## Различия между v1 и v2 | Параметр | v1 (/stats) | v2 (/stats/v2) | v2 с chainId | |----------|-------------|----------------|--------------| | Базовая статистика | ✅ | ✅ | ✅ | | Таблица заданий | ❌ | ✅ (все) | ✅ (фильтр) | | Прогресс участников | ❌ | ✅ (все) | ✅ (фильтр) | | Детальный прогресс по цепочкам | ❌ | ✅ (все) | ✅ (одна) | | Статистика по попыткам | Общая | Детальная | Детальная (фильтр) | | Скорость работы | Быстро | Медленнее | Средняя | ## Обработка ошибок ```tsx async function fetchStatsV2(chainId?: string) { try { const url = chainId ? `/challenge/stats/v2?chainId=${chainId}` : '/challenge/stats/v2'; const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); if (!data.success) { throw new Error(data.error?.message || 'Unknown error'); } return data.body; } catch (error) { console.error('Failed to fetch stats v2:', error); // Специальная обработка для неверного chainId if (error.message.includes('Chain not found')) { showNotification('Цепочка не найдена', 'error'); // Перенаправить на страницу всех цепочек window.location.href = '/chains'; } else { showNotification('Не удалось загрузить статистику', 'error'); } } } ``` ### Типичные ошибки | Ошибка | Причина | Решение | |--------|---------|---------| | `Chain not found` | Передан несуществующий `chainId` | Проверить ID цепочки, показать ошибку пользователю | | `500 Internal Server Error` | Проблема на сервере | Показать общее сообщение об ошибке, повторить запрос | | Timeout | Слишком много данных | Использовать фильтрацию по `chainId` для уменьшения объема данных | ## Дополнительные возможности ### Экспорт данных Данные можно экспортировать в CSV или Excel для анализа: ```typescript function exportToCSV(tasksTable) { const headers = ['Название', 'Попыток', 'Пользователей', 'Успешно', '% успеха', 'Средние попытки']; const rows = tasksTable.map(task => [ task.title, task.totalAttempts, task.uniqueUsers, task.acceptedCount, task.successRate, task.averageAttemptsToSuccess ]); const csv = [headers, ...rows] .map(row => row.join(',')) .join('\n'); const blob = new Blob([csv], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'tasks-statistics.csv'; a.click(); } ``` ## Заключение Эндпоинт `/stats/v2` предоставляет все необходимые данные для построения информативных дашбордов с таблицами заданий и прогресс-барами участников. Комбинируйте различные части данных для создания удобных UI компонентов. Для дополнительной информации см. также: - [CHALLENGE_FRONTEND_GUIDE.md](./CHALLENGE_FRONTEND_GUIDE.md) - общее руководство по работе с Challenge API - [CHALLENGE_REACT_EXAMPLE.md](./CHALLENGE_REACT_EXAMPLE.md) - примеры React компонентов