diff --git a/docs/stats-v2-api.md b/docs/stats-v2-api.md
new file mode 100644
index 0000000..03e7b54
--- /dev/null
+++ b/docs/stats-v2-api.md
@@ -0,0 +1,693 @@
+# 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 => (
+ {task.title}
+ ))}
+ Прогресс
+
+
+
+ {chain.participantProgress.map(participant => (
+
+ {participant.nickname}
+ {participant.taskProgress.map(taskProg => (
+
+
+
+ ))}
+ {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 (
+
+
setSelectedChainId(e.target.value || undefined)}
+ >
+ Все цепочки
+ {data?.chainsDetailed.map(chain => (
+
+ {chain.name}
+
+ ))}
+
+
+
+
+
+ );
+}
+```
+
+### Фильтрация и сортировка
+
+Все массивы данных можно фильтровать и сортировать на клиенте:
+
+```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 компонентов
+
diff --git a/locales/en.json b/locales/en.json
index 31cd851..c5c1bb5 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -178,6 +178,7 @@
"challenge.admin.submissions.details.close": "Close",
"challenge.admin.layout.title": "Challenge Admin",
"challenge.admin.layout.nav.dashboard": "Dashboard",
+ "challenge.admin.layout.nav.detailed.stats": "Detailed Statistics",
"challenge.admin.layout.nav.tasks": "Tasks",
"challenge.admin.layout.nav.chains": "Chains",
"challenge.admin.layout.nav.users": "Users",
@@ -188,5 +189,46 @@
"challenge.admin.common.error.default": "An error occurred while loading data",
"challenge.admin.common.retry": "Try again",
"challenge.admin.common.confirm": "Confirm",
- "challenge.admin.common.close": "Close"
+ "challenge.admin.common.close": "Close",
+ "challenge.admin.detailed.stats.title": "Detailed Statistics",
+ "challenge.admin.detailed.stats.loading": "Loading detailed statistics...",
+ "challenge.admin.detailed.stats.load.error": "Failed to load detailed statistics",
+ "challenge.admin.detailed.stats.auto.refresh": "Auto-refreshes every 5 seconds",
+ "challenge.admin.detailed.stats.select.chain": "Select a chain to view detailed statistics",
+ "challenge.admin.detailed.stats.no.chains": "No chains available",
+ "challenge.admin.detailed.stats.chain.card.click": "Click to view detailed statistics for this chain",
+ "challenge.admin.detailed.stats.chain.card.tasks": "tasks",
+ "challenge.admin.detailed.stats.back.to.chains": "Back to chain selection",
+ "challenge.admin.detailed.stats.overview.title": "Overview",
+ "challenge.admin.detailed.stats.overview.users": "Users",
+ "challenge.admin.detailed.stats.overview.tasks": "Tasks",
+ "challenge.admin.detailed.stats.overview.chains": "Chains",
+ "challenge.admin.detailed.stats.overview.total.attempts": "Total attempts",
+ "challenge.admin.detailed.stats.overview.successful": "Successful",
+ "challenge.admin.detailed.stats.overview.in.progress.pending": "In progress / Pending",
+ "challenge.admin.detailed.stats.overview.avg.check.time": "Average check time",
+ "challenge.admin.detailed.stats.tasks.table.title": "Task Statistics",
+ "challenge.admin.detailed.stats.tasks.table.empty": "No data to display",
+ "challenge.admin.detailed.stats.tasks.table.task.name": "Task name",
+ "challenge.admin.detailed.stats.tasks.table.attempts": "Attempts",
+ "challenge.admin.detailed.stats.tasks.table.users": "Users",
+ "challenge.admin.detailed.stats.tasks.table.completed": "Completed",
+ "challenge.admin.detailed.stats.tasks.table.success.rate": "Success %",
+ "challenge.admin.detailed.stats.tasks.table.avg.attempts": "Avg attempts",
+ "challenge.admin.detailed.stats.participants.title": "Active Participants",
+ "challenge.admin.detailed.stats.participants.empty": "No active participants",
+ "challenge.admin.detailed.stats.participants.completed": "Completed:",
+ "challenge.admin.detailed.stats.participants.attempts": "Attempts:",
+ "challenge.admin.detailed.stats.participants.no.progress": "No chain progress",
+ "challenge.admin.detailed.stats.chains.title": "Detailed Chain Progress",
+ "challenge.admin.detailed.stats.chains.empty": "No chain data",
+ "challenge.admin.detailed.stats.chains.total.tasks": "Total tasks:",
+ "challenge.admin.detailed.stats.chains.participant": "Participant",
+ "challenge.admin.detailed.stats.chains.progress": "Progress",
+ "challenge.admin.detailed.stats.chains.no.participants": "No participants in this chain",
+ "challenge.admin.detailed.stats.status.not.started": "Not started",
+ "challenge.admin.detailed.stats.status.pending": "Pending",
+ "challenge.admin.detailed.stats.status.in.progress": "In progress",
+ "challenge.admin.detailed.stats.status.needs.revision": "Needs revision",
+ "challenge.admin.detailed.stats.status.completed": "Completed"
}
\ No newline at end of file
diff --git a/locales/ru.json b/locales/ru.json
index b2cbe4e..9592f78 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -177,6 +177,7 @@
"challenge.admin.submissions.details.close": "Закрыть",
"challenge.admin.layout.title": "Challenge Admin",
"challenge.admin.layout.nav.dashboard": "Dashboard",
+ "challenge.admin.layout.nav.detailed.stats": "Детальная статистика",
"challenge.admin.layout.nav.tasks": "Задания",
"challenge.admin.layout.nav.chains": "Цепочки",
"challenge.admin.layout.nav.users": "Пользователи",
@@ -187,5 +188,46 @@
"challenge.admin.common.error.default": "Произошла ошибка при загрузке данных",
"challenge.admin.common.retry": "Попробовать снова",
"challenge.admin.common.confirm": "Подтвердить",
- "challenge.admin.common.close": "Закрыть"
+ "challenge.admin.common.close": "Закрыть",
+ "challenge.admin.detailed.stats.title": "Детальная статистика",
+ "challenge.admin.detailed.stats.loading": "Загрузка детальной статистики...",
+ "challenge.admin.detailed.stats.load.error": "Не удалось загрузить детальную статистику",
+ "challenge.admin.detailed.stats.auto.refresh": "Обновляется автоматически каждые 5 секунд",
+ "challenge.admin.detailed.stats.select.chain": "Выберите цепочку для просмотра детальной статистики",
+ "challenge.admin.detailed.stats.no.chains": "Нет доступных цепочек",
+ "challenge.admin.detailed.stats.chain.card.click": "Нажмите для просмотра детальной статистики по этой цепочке",
+ "challenge.admin.detailed.stats.chain.card.tasks": "заданий",
+ "challenge.admin.detailed.stats.back.to.chains": "Назад к выбору цепочки",
+ "challenge.admin.detailed.stats.overview.title": "Общая статистика",
+ "challenge.admin.detailed.stats.overview.users": "Пользователей",
+ "challenge.admin.detailed.stats.overview.tasks": "Заданий",
+ "challenge.admin.detailed.stats.overview.chains": "Цепочек",
+ "challenge.admin.detailed.stats.overview.total.attempts": "Всего попыток",
+ "challenge.admin.detailed.stats.overview.successful": "Успешных",
+ "challenge.admin.detailed.stats.overview.in.progress.pending": "В процессе / Ожидают",
+ "challenge.admin.detailed.stats.overview.avg.check.time": "Среднее время проверки",
+ "challenge.admin.detailed.stats.tasks.table.title": "Статистика по заданиям",
+ "challenge.admin.detailed.stats.tasks.table.empty": "Нет данных для отображения",
+ "challenge.admin.detailed.stats.tasks.table.task.name": "Название задания",
+ "challenge.admin.detailed.stats.tasks.table.attempts": "Попыток",
+ "challenge.admin.detailed.stats.tasks.table.users": "Пользователей",
+ "challenge.admin.detailed.stats.tasks.table.completed": "Завершено",
+ "challenge.admin.detailed.stats.tasks.table.success.rate": "% успеха",
+ "challenge.admin.detailed.stats.tasks.table.avg.attempts": "Средние попытки",
+ "challenge.admin.detailed.stats.participants.title": "Активные участники",
+ "challenge.admin.detailed.stats.participants.empty": "Нет активных участников",
+ "challenge.admin.detailed.stats.participants.completed": "Завершено:",
+ "challenge.admin.detailed.stats.participants.attempts": "Попыток:",
+ "challenge.admin.detailed.stats.participants.no.progress": "Нет прогресса по цепочкам",
+ "challenge.admin.detailed.stats.chains.title": "Детальный прогресс по цепочкам",
+ "challenge.admin.detailed.stats.chains.empty": "Нет данных по цепочкам",
+ "challenge.admin.detailed.stats.chains.total.tasks": "Всего заданий:",
+ "challenge.admin.detailed.stats.chains.participant": "Участник",
+ "challenge.admin.detailed.stats.chains.progress": "Прогресс",
+ "challenge.admin.detailed.stats.chains.no.participants": "Нет участников в этой цепочке",
+ "challenge.admin.detailed.stats.status.not.started": "Не начато",
+ "challenge.admin.detailed.stats.status.pending": "Ожидает",
+ "challenge.admin.detailed.stats.status.in.progress": "В процессе",
+ "challenge.admin.detailed.stats.status.needs.revision": "Доработка",
+ "challenge.admin.detailed.stats.status.completed": "Завершено"
}
\ No newline at end of file
diff --git a/src/__data__/api/api.ts b/src/__data__/api/api.ts
index 6790690..364f830 100644
--- a/src/__data__/api/api.ts
+++ b/src/__data__/api/api.ts
@@ -8,6 +8,7 @@ import type {
ChallengeUser,
ChallengeSubmission,
SystemStats,
+ SystemStatsV2,
UserStats,
CreateTaskRequest,
UpdateTaskRequest,
@@ -125,6 +126,14 @@ export const api = createApi({
transformResponse: (response: { body: SystemStats }) => response.body,
providesTags: ['Stats'],
}),
+ getSystemStatsV2: builder.query({
+ query: (chainId) => ({
+ url: '/challenge/stats/v2',
+ params: chainId ? { chainId } : undefined,
+ }),
+ transformResponse: (response: { body: SystemStatsV2 }) => response.body,
+ providesTags: ['Stats'],
+ }),
getUserStats: builder.query({
query: (userId) => `/challenge/user/${userId}/stats`,
transformResponse: (response: { body: UserStats }) => response.body,
@@ -161,6 +170,7 @@ export const {
useDeleteChainMutation,
useGetUsersQuery,
useGetSystemStatsQuery,
+ useGetSystemStatsV2Query,
useGetUserStatsQuery,
useGetUserSubmissionsQuery,
useGetAllSubmissionsQuery,
diff --git a/src/__data__/urls.ts b/src/__data__/urls.ts
index 982870a..d0df8a5 100644
--- a/src/__data__/urls.ts
+++ b/src/__data__/urls.ts
@@ -12,6 +12,11 @@ export const URLs = {
// Dashboard
dashboard: makeUrl(''),
+ // Detailed Stats
+ detailedStats: makeUrl('/detailed-stats'),
+ detailedStatsChain: (chainId: string) => makeUrl(`/detailed-stats/${chainId}`),
+ detailedStatsChainPath: makeUrl('/detailed-stats/:chainId'),
+
// Tasks
tasks: makeUrl('/tasks'),
taskNew: makeUrl('/tasks/new'),
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
index 3e5c355..1d68b38 100644
--- a/src/components/Layout.tsx
+++ b/src/components/Layout.tsx
@@ -1,7 +1,7 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Link, useLocation, useNavigate } from 'react-router-dom'
-import { Box, Container, Flex, HStack, VStack, Button, Text } from '@chakra-ui/react'
+import { Box, Container, Flex, HStack, Button, Text } from '@chakra-ui/react'
import { useAppSelector } from '../__data__/store'
import { URLs } from '../__data__/urls'
import { keycloak } from '../__data__/kc'
@@ -34,6 +34,7 @@ export const Layout: React.FC = ({ children }) => {
const navItems = [
{ label: t('challenge.admin.layout.nav.dashboard'), path: URLs.dashboard },
+ { label: t('challenge.admin.layout.nav.detailed.stats'), path: URLs.detailedStats },
{ label: t('challenge.admin.layout.nav.tasks'), path: URLs.tasks },
{ label: t('challenge.admin.layout.nav.chains'), path: URLs.chains },
{ label: t('challenge.admin.layout.nav.users'), path: URLs.users },
@@ -106,16 +107,15 @@ export const Layout: React.FC = ({ children }) => {
{navItems.map((item) => (
-
- {item.label}
-
+
+
+ {item.label}
+
+
))}
diff --git a/src/dashboard.tsx b/src/dashboard.tsx
index 2be7f2a..351b0a5 100644
--- a/src/dashboard.tsx
+++ b/src/dashboard.tsx
@@ -3,6 +3,7 @@ import { Route, Routes } from 'react-router-dom'
import { Layout } from './components/Layout'
import { DashboardPage } from './pages/dashboard/DashboardPage'
+import { DetailedStatsPage } from './pages/detailed-stats/DetailedStatsPage'
import { TasksListPage } from './pages/tasks/TasksListPage'
import { TaskFormPage } from './pages/tasks/TaskFormPage'
import { ChainsListPage } from './pages/chains/ChainsListPage'
@@ -30,6 +31,24 @@ export const Dashboard = () => {
}
/>
+ {/* Detailed Stats */}
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
{/* Tasks */}
string): StatusConfig => {
+ const configs: Record = {
+ not_started: { label: t('challenge.admin.detailed.stats.status.not.started'), color: 'gray' },
+ pending: { label: t('challenge.admin.detailed.stats.status.pending'), color: 'yellow' },
+ in_progress: { label: t('challenge.admin.detailed.stats.status.in.progress'), color: 'blue' },
+ needs_revision: { label: t('challenge.admin.detailed.stats.status.needs.revision'), color: 'orange' },
+ completed: { label: t('challenge.admin.detailed.stats.status.completed'), color: 'green' },
+ }
+ return configs[status]
+}
+
+export const ChainDetailedView: React.FC = ({ chains }) => {
+ const { t } = useTranslation()
+
+ if (chains.length === 0) {
+ return (
+
+
+ {t('challenge.admin.detailed.stats.chains.title')}
+
+
+ {t('challenge.admin.detailed.stats.chains.empty')}
+
+
+ )
+ }
+
+ const chain = chains[0] // Теперь всегда одна цепочка из API
+
+ return (
+
+
+ {t('challenge.admin.detailed.stats.chains.title')}
+
+
+
+
+ {chain.name}
+
+
+ {t('challenge.admin.detailed.stats.chains.total.tasks')} {chain.totalTasks}
+
+
+
+ {chain.participantProgress.length > 0 ? (
+
+
+
+
+
+ {t('challenge.admin.detailed.stats.chains.participant')}
+
+ {chain.tasks.map((task) => (
+
+
+ {task.title}
+
+
+ ))}
+
+ {t('challenge.admin.detailed.stats.chains.progress')}
+
+
+
+
+ {chain.participantProgress.map((participant) => (
+
+
+ {participant.nickname}
+
+ {participant.taskProgress.map((taskProg) => {
+ const config = getStatusConfig(taskProg.status, t)
+ return (
+
+
+ {config.label}
+
+
+ )
+ })}
+
+ = 80
+ ? 'green'
+ : participant.progressPercent >= 50
+ ? 'yellow'
+ : 'red'
+ }
+ >
+ {participant.progressPercent}%
+
+
+ {participant.completedCount}/{chain.totalTasks}
+
+
+
+ ))}
+
+
+
+ ) : (
+
+ {t('challenge.admin.detailed.stats.chains.no.participants')}
+
+ )}
+
+ )
+}
+
diff --git a/src/pages/detailed-stats/DetailedStatsPage.tsx b/src/pages/detailed-stats/DetailedStatsPage.tsx
new file mode 100644
index 0000000..ad22d68
--- /dev/null
+++ b/src/pages/detailed-stats/DetailedStatsPage.tsx
@@ -0,0 +1,240 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import { useParams, Link } from 'react-router-dom'
+import { Box, Heading, VStack, Text, HStack, Badge, SimpleGrid } from '@chakra-ui/react'
+import { useGetChainsQuery, useGetSystemStatsV2Query } from '../../__data__/api/api'
+import { LoadingSpinner } from '../../components/LoadingSpinner'
+import { ErrorAlert } from '../../components/ErrorAlert'
+import { URLs } from '../../__data__/urls'
+import { TasksStatisticsTable } from './TasksStatisticsTable'
+import { ParticipantsProgress } from './ParticipantsProgress'
+import { ChainDetailedView } from './ChainDetailedView'
+
+export const DetailedStatsPage: React.FC = () => {
+ const { t } = useTranslation()
+ const { chainId } = useParams<{ chainId?: string }>()
+
+ // Получаем список цепочек
+ const { data: chains, isLoading: isChainsLoading, error: chainsError } = useGetChainsQuery()
+
+ // Получаем детальную статистику по выбранной цепочке (только если chainId есть)
+ const { data: stats, isLoading: isStatsLoading, error: statsError, refetch } = useGetSystemStatsV2Query(
+ chainId,
+ {
+ pollingInterval: 5000, // Обновление каждые 5 секунд для реального времени
+ skip: !chainId, // Не делаем запрос пока не выбрана цепочка
+ }
+ )
+
+ const isLoading = isChainsLoading || (chainId && isStatsLoading)
+ const error = chainsError || statsError
+
+ if (isLoading) {
+ return
+ }
+
+ if (error) {
+ return (
+
+ )
+ }
+
+ // Если chainId не указан - показываем карточки для выбора
+ if (!chainId) {
+ return (
+
+
+ {t('challenge.admin.detailed.stats.title')}
+
+ {t('challenge.admin.detailed.stats.select.chain')}
+
+
+
+ {chains && chains.length > 0 ? (
+
+ {chains.map((chain) => (
+
+
+
+
+ {chain.name}
+
+
+
+ {chain.tasks.length} {t('challenge.admin.detailed.stats.chain.card.tasks')}
+
+
+
+ {t('challenge.admin.detailed.stats.chain.card.click')}
+
+
+
+
+ ))}
+
+ ) : (
+
+ {t('challenge.admin.detailed.stats.no.chains')}
+
+ )}
+
+ )
+ }
+
+ // Если chainId указан но данных еще нет - ждем загрузки
+ if (!stats) {
+ return null
+ }
+
+ const acceptanceRate = stats.submissions.total > 0
+ ? ((stats.submissions.accepted / stats.submissions.total) * 100).toFixed(1)
+ : '0'
+
+ const selectedChain = chains?.find(c => c.id === chainId)
+
+ return (
+
+
+
+
+
+
+
+ ← {t('challenge.admin.detailed.stats.back.to.chains')}
+
+
+
+
+ {selectedChain?.name || t('challenge.admin.detailed.stats.title')}
+
+
+ {t('challenge.admin.detailed.stats.auto.refresh')}
+
+
+
+
+
+ {/* Quick Stats Overview */}
+
+
+ {t('challenge.admin.detailed.stats.overview.title')}
+
+
+
+
+ {t('challenge.admin.detailed.stats.overview.users')}
+
+
+ {stats.users}
+
+
+
+
+
+ {t('challenge.admin.detailed.stats.overview.tasks')}
+
+
+ {stats.tasks}
+
+
+
+
+
+ {t('challenge.admin.detailed.stats.overview.chains')}
+
+
+ {stats.chains}
+
+
+
+
+
+ {t('challenge.admin.detailed.stats.overview.total.attempts')}
+
+
+ {stats.submissions.total}
+
+
+
+
+
+ {t('challenge.admin.detailed.stats.overview.successful')}
+
+
+
+ {stats.submissions.accepted}
+
+
+ {acceptanceRate}%
+
+
+
+
+
+
+ {t('challenge.admin.detailed.stats.overview.in.progress.pending')}
+
+
+
+ {stats.submissions.inProgress}
+
+
+ {stats.submissions.pending}
+
+
+
+
+
+
+ {t('challenge.admin.detailed.stats.overview.avg.check.time')}
+
+
+ {(stats.averageCheckTimeMs / 1000).toFixed(1)}с
+
+
+
+
+
+ {/* Main Content - Three Sections */}
+ {stats && (
+
+ {/* 1. Tasks Statistics Table */}
+
+
+ {/* 2. Active Participants Progress */}
+
+
+ {/* 3. Chain Detailed View */}
+
+
+ )}
+
+ )
+}
+
diff --git a/src/pages/detailed-stats/ParticipantsProgress.tsx b/src/pages/detailed-stats/ParticipantsProgress.tsx
new file mode 100644
index 0000000..d6d452f
--- /dev/null
+++ b/src/pages/detailed-stats/ParticipantsProgress.tsx
@@ -0,0 +1,110 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import { Box, Heading, Grid, Text, VStack, HStack, Progress } from '@chakra-ui/react'
+import type { ActiveParticipant } from '../../types/challenge'
+
+interface ParticipantsProgressProps {
+ participants: ActiveParticipant[]
+}
+
+export const ParticipantsProgress: React.FC = ({ participants }) => {
+ const { t } = useTranslation()
+
+ const getProgressColor = (percent: number) => {
+ if (percent >= 80) return 'green'
+ if (percent >= 50) return 'yellow'
+ return 'red'
+ }
+
+ if (participants.length === 0) {
+ return (
+
+
+ {t('challenge.admin.detailed.stats.participants.title')}
+
+
+ {t('challenge.admin.detailed.stats.participants.empty')}
+
+
+ )
+ }
+
+ return (
+
+
+ {t('challenge.admin.detailed.stats.participants.title')}
+
+
+ {participants.map((participant) => (
+
+
+ {/* Participant Header */}
+
+
+ {participant.nickname}
+
+
+
+ {t('challenge.admin.detailed.stats.participants.completed')}
+ {participant.completedTasks}
+
+
+
+ {t('challenge.admin.detailed.stats.participants.attempts')}
+ {participant.totalSubmissions}
+
+
+
+
+
+ {/* Chain Progress */}
+ {participant.chainProgress.length > 0 ? (
+
+ {participant.chainProgress.map((chain) => (
+
+
+
+ {chain.chainName}
+
+
+ {chain.completedTasks}/{chain.totalTasks}
+
+
+
+
+
+
+
+
+ {chain.progressPercent}%
+
+
+ ))}
+
+ ) : (
+
+ {t('challenge.admin.detailed.stats.participants.no.progress')}
+
+ )}
+
+
+ ))}
+
+
+ )
+}
+
diff --git a/src/pages/detailed-stats/TasksStatisticsTable.tsx b/src/pages/detailed-stats/TasksStatisticsTable.tsx
new file mode 100644
index 0000000..2b450b4
--- /dev/null
+++ b/src/pages/detailed-stats/TasksStatisticsTable.tsx
@@ -0,0 +1,148 @@
+import React, { useState, useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
+import { Box, Heading, Table, Badge } from '@chakra-ui/react'
+import type { TaskTableItem } from '../../types/challenge'
+
+interface TasksStatisticsTableProps {
+ tasks: TaskTableItem[]
+}
+
+type SortKey = keyof TaskTableItem | null
+type SortDirection = 'asc' | 'desc'
+
+export const TasksStatisticsTable: React.FC = ({ tasks }) => {
+ const { t } = useTranslation()
+ const [sortKey, setSortKey] = useState('successRate')
+ const [sortDirection, setSortDirection] = useState('desc')
+
+ const handleSort = (key: SortKey) => {
+ if (sortKey === key) {
+ setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc')
+ } else {
+ setSortKey(key)
+ setSortDirection('desc')
+ }
+ }
+
+ const sortedTasks = useMemo(() => {
+ if (!sortKey) return tasks
+
+ return [...tasks].sort((a, b) => {
+ const aVal = a[sortKey]
+ const bVal = b[sortKey]
+
+ if (typeof aVal === 'string' && typeof bVal === 'string') {
+ return sortDirection === 'asc'
+ ? aVal.localeCompare(bVal)
+ : bVal.localeCompare(aVal)
+ }
+
+ if (typeof aVal === 'number' && typeof bVal === 'number') {
+ return sortDirection === 'asc' ? aVal - bVal : bVal - aVal
+ }
+
+ return 0
+ })
+ }, [tasks, sortKey, sortDirection])
+
+ const getSuccessRateColor = (rate: number) => {
+ if (rate >= 80) return 'green'
+ if (rate >= 50) return 'yellow'
+ return 'red'
+ }
+
+ if (tasks.length === 0) {
+ return (
+
+
+ {t('challenge.admin.detailed.stats.tasks.table.title')}
+
+
+ {t('challenge.admin.detailed.stats.tasks.table.empty')}
+
+
+ )
+ }
+
+ return (
+
+
+ {t('challenge.admin.detailed.stats.tasks.table.title')}
+
+
+
+
+
+ handleSort('title')}
+ _hover={{ bg: 'gray.50' }}
+ >
+ {t('challenge.admin.detailed.stats.tasks.table.task.name')} {sortKey === 'title' && (sortDirection === 'asc' ? '↑' : '↓')}
+
+ handleSort('totalAttempts')}
+ _hover={{ bg: 'gray.50' }}
+ textAlign="right"
+ >
+ {t('challenge.admin.detailed.stats.tasks.table.attempts')} {sortKey === 'totalAttempts' && (sortDirection === 'asc' ? '↑' : '↓')}
+
+ handleSort('uniqueUsers')}
+ _hover={{ bg: 'gray.50' }}
+ textAlign="right"
+ >
+ {t('challenge.admin.detailed.stats.tasks.table.users')} {sortKey === 'uniqueUsers' && (sortDirection === 'asc' ? '↑' : '↓')}
+
+ handleSort('acceptedCount')}
+ _hover={{ bg: 'gray.50' }}
+ textAlign="right"
+ >
+ {t('challenge.admin.detailed.stats.tasks.table.completed')} {sortKey === 'acceptedCount' && (sortDirection === 'asc' ? '↑' : '↓')}
+
+ handleSort('successRate')}
+ _hover={{ bg: 'gray.50' }}
+ textAlign="right"
+ >
+ {t('challenge.admin.detailed.stats.tasks.table.success.rate')} {sortKey === 'successRate' && (sortDirection === 'asc' ? '↑' : '↓')}
+
+ handleSort('averageAttemptsToSuccess')}
+ _hover={{ bg: 'gray.50' }}
+ textAlign="right"
+ >
+ {t('challenge.admin.detailed.stats.tasks.table.avg.attempts')} {sortKey === 'averageAttemptsToSuccess' && (sortDirection === 'asc' ? '↑' : '↓')}
+
+
+
+
+ {sortedTasks.map((task) => (
+
+ {task.title}
+ {task.totalAttempts}
+ {task.uniqueUsers}
+ {task.acceptedCount}
+
+
+ {task.successRate}%
+
+
+
+ {task.averageAttemptsToSuccess.toFixed(1)}
+
+
+ ))}
+
+
+
+
+ )
+}
+
diff --git a/src/types/challenge.ts b/src/types/challenge.ts
index 2e43b9d..8116726 100644
--- a/src/types/challenge.ts
+++ b/src/types/challenge.ts
@@ -113,7 +113,7 @@ export interface SystemStats {
// API Request/Response types
export interface APIResponse {
- error: any
+ error: unknown
data: T
}
@@ -139,3 +139,87 @@ export interface UpdateChainRequest {
taskIds?: string[]
}
+// ========== Stats v2 Types ==========
+
+export type TaskProgressStatus = 'not_started' | 'pending' | 'in_progress' | 'needs_revision' | 'completed'
+
+export interface TaskTableItem {
+ taskId: string
+ title: string
+ totalAttempts: number
+ uniqueUsers: number
+ acceptedCount: number
+ successRate: number
+ averageAttemptsToSuccess: number
+}
+
+export interface ChainProgress {
+ chainId: string
+ chainName: string
+ totalTasks: number
+ completedTasks: number
+ progressPercent: number
+}
+
+export interface ActiveParticipant {
+ userId: string
+ nickname: string
+ totalSubmissions: number
+ completedTasks: number
+ chainProgress: ChainProgress[]
+}
+
+export interface TaskProgress {
+ taskId: string
+ taskTitle: string
+ status: TaskProgressStatus
+}
+
+export interface ParticipantProgress {
+ userId: string
+ nickname: string
+ taskProgress: TaskProgress[]
+ completedCount: number
+ progressPercent: number
+}
+
+export interface ChainTask {
+ taskId: string
+ title: string
+ description: string
+}
+
+export interface ChainDetailed {
+ chainId: string
+ name: string
+ totalTasks: number
+ tasks: ChainTask[]
+ participantProgress: ParticipantProgress[]
+}
+
+export interface SystemStatsV2 {
+ // Базовая статистика из 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: TaskTableItem[]
+ activeParticipants: ActiveParticipant[]
+ chainsDetailed: ChainDetailed[]
+}
+
diff --git a/stubs/api/README.md b/stubs/api/README.md
index 21ee61a..128203b 100644
--- a/stubs/api/README.md
+++ b/stubs/api/README.md
@@ -11,7 +11,8 @@ stubs/api/
│ ├── chains.json # Цепочки (3 шт.)
│ ├── users.json # Пользователи (8 шт.)
│ ├── submissions.json # Попытки (8 шт.)
-│ └── stats.json # Системная статистика
+│ ├── stats.json # Системная статистика (v1)
+│ └── stats-v2.json # Детальная статистика (v2, 20 заданий)
├── index.js # API роуты
└── README.md # Эта документация
```
@@ -36,7 +37,8 @@ stubs/api/
- `GET /api/challenge/users` - список всех пользователей
### Statistics (Статистика)
-- `GET /api/challenge/stats` - общая системная статистика
+- `GET /api/challenge/stats` - общая системная статистика (v1)
+- `GET /api/challenge/stats/v2` - детальная статистика с таблицами и прогрессом (v2)
- `GET /api/challenge/user/:userId/stats` - статистика пользователя (генерируется динамически)
### Submissions (Попытки)
@@ -150,6 +152,17 @@ GET /api/challenge/user/user001/stats
Ответ будет содержать динамически вычисленную статистику на основе всех попыток пользователя.
+### Получить детальную статистику (v2)
+```bash
+GET /api/challenge/stats/v2
+```
+
+Ответ будет содержать:
+- Базовую статистику (users, tasks, chains, submissions, queue)
+- Таблицу заданий с детальной статистикой (20 заданий с попытками, успешностью, средними показателями)
+- 6 активных участников с прогрессом по цепочкам
+- Детальную матрицу прогресса по каждой из 2 цепочек (Backend разработка - 10 заданий, Frontend разработка - 10 заданий)
+
## ⚙️ Настройка задержки
По умолчанию все запросы имеют задержку 300ms для имитации сетевых запросов. Изменить можно в `index.js`:
diff --git a/stubs/api/data/stats-v2.json b/stubs/api/data/stats-v2.json
new file mode 100644
index 0000000..6b1d8ae
--- /dev/null
+++ b/stubs/api/data/stats-v2.json
@@ -0,0 +1,592 @@
+{
+ "users": 8,
+ "tasks": 20,
+ "chains": 2,
+ "submissions": {
+ "total": 95,
+ "accepted": 38,
+ "rejected": 42,
+ "pending": 8,
+ "inProgress": 7
+ },
+ "averageCheckTimeMs": 2143,
+ "queue": {
+ "queueLength": 15,
+ "waiting": 8,
+ "inProgress": 7,
+ "maxConcurrency": 10,
+ "currentlyProcessing": 7
+ },
+ "tasksTable": [
+ {
+ "taskId": "task_1",
+ "title": "Создание REST API",
+ "totalAttempts": 12,
+ "uniqueUsers": 6,
+ "acceptedCount": 5,
+ "successRate": 83,
+ "averageAttemptsToSuccess": 2.4
+ },
+ {
+ "taskId": "task_2",
+ "title": "Работа с базой данных MongoDB",
+ "totalAttempts": 10,
+ "uniqueUsers": 5,
+ "acceptedCount": 3,
+ "successRate": 60,
+ "averageAttemptsToSuccess": 3.3
+ },
+ {
+ "taskId": "task_3",
+ "title": "JWT аутентификация",
+ "totalAttempts": 8,
+ "uniqueUsers": 4,
+ "acceptedCount": 2,
+ "successRate": 50,
+ "averageAttemptsToSuccess": 4.0
+ },
+ {
+ "taskId": "task_4",
+ "title": "Middleware для Express",
+ "totalAttempts": 6,
+ "uniqueUsers": 4,
+ "acceptedCount": 3,
+ "successRate": 75,
+ "averageAttemptsToSuccess": 2.0
+ },
+ {
+ "taskId": "task_5",
+ "title": "WebSocket сервер",
+ "totalAttempts": 5,
+ "uniqueUsers": 3,
+ "acceptedCount": 1,
+ "successRate": 33,
+ "averageAttemptsToSuccess": 5.0
+ },
+ {
+ "taskId": "task_6",
+ "title": "Кэширование с Redis",
+ "totalAttempts": 7,
+ "uniqueUsers": 4,
+ "acceptedCount": 2,
+ "successRate": 57,
+ "averageAttemptsToSuccess": 3.5
+ },
+ {
+ "taskId": "task_7",
+ "title": "GraphQL Schema",
+ "totalAttempts": 4,
+ "uniqueUsers": 3,
+ "acceptedCount": 2,
+ "successRate": 67,
+ "averageAttemptsToSuccess": 2.0
+ },
+ {
+ "taskId": "task_8",
+ "title": "Docker контейнеризация",
+ "totalAttempts": 3,
+ "uniqueUsers": 2,
+ "acceptedCount": 1,
+ "successRate": 50,
+ "averageAttemptsToSuccess": 3.0
+ },
+ {
+ "taskId": "task_9",
+ "title": "CI/CD Pipeline",
+ "totalAttempts": 2,
+ "uniqueUsers": 2,
+ "acceptedCount": 1,
+ "successRate": 50,
+ "averageAttemptsToSuccess": 2.0
+ },
+ {
+ "taskId": "task_10",
+ "title": "Микросервисная архитектура",
+ "totalAttempts": 2,
+ "uniqueUsers": 1,
+ "acceptedCount": 0,
+ "successRate": 0,
+ "averageAttemptsToSuccess": 0
+ },
+ {
+ "taskId": "task_11",
+ "title": "React компоненты",
+ "totalAttempts": 11,
+ "uniqueUsers": 6,
+ "acceptedCount": 6,
+ "successRate": 100,
+ "averageAttemptsToSuccess": 1.8
+ },
+ {
+ "taskId": "task_12",
+ "title": "React Hooks",
+ "totalAttempts": 9,
+ "uniqueUsers": 5,
+ "acceptedCount": 4,
+ "successRate": 80,
+ "averageAttemptsToSuccess": 2.3
+ },
+ {
+ "taskId": "task_13",
+ "title": "Redux State Management",
+ "totalAttempts": 7,
+ "uniqueUsers": 4,
+ "acceptedCount": 2,
+ "successRate": 57,
+ "averageAttemptsToSuccess": 3.5
+ },
+ {
+ "taskId": "task_14",
+ "title": "React Router",
+ "totalAttempts": 6,
+ "uniqueUsers": 4,
+ "acceptedCount": 3,
+ "successRate": 75,
+ "averageAttemptsToSuccess": 2.0
+ },
+ {
+ "taskId": "task_15",
+ "title": "Form валидация",
+ "totalAttempts": 5,
+ "uniqueUsers": 3,
+ "acceptedCount": 2,
+ "successRate": 67,
+ "averageAttemptsToSuccess": 2.5
+ },
+ {
+ "taskId": "task_16",
+ "title": "API интеграция",
+ "totalAttempts": 8,
+ "uniqueUsers": 5,
+ "acceptedCount": 3,
+ "successRate": 60,
+ "averageAttemptsToSuccess": 2.7
+ },
+ {
+ "taskId": "task_17",
+ "title": "CSS-in-JS стилизация",
+ "totalAttempts": 4,
+ "uniqueUsers": 3,
+ "acceptedCount": 2,
+ "successRate": 67,
+ "averageAttemptsToSuccess": 2.0
+ },
+ {
+ "taskId": "task_18",
+ "title": "Оптимизация производительности",
+ "totalAttempts": 3,
+ "uniqueUsers": 2,
+ "acceptedCount": 1,
+ "successRate": 50,
+ "averageAttemptsToSuccess": 3.0
+ },
+ {
+ "taskId": "task_19",
+ "title": "Unit тесты с Jest",
+ "totalAttempts": 2,
+ "uniqueUsers": 2,
+ "acceptedCount": 1,
+ "successRate": 50,
+ "averageAttemptsToSuccess": 2.0
+ },
+ {
+ "taskId": "task_20",
+ "title": "E2E тесты с Playwright",
+ "totalAttempts": 1,
+ "uniqueUsers": 1,
+ "acceptedCount": 0,
+ "successRate": 0,
+ "averageAttemptsToSuccess": 0
+ }
+ ],
+ "activeParticipants": [
+ {
+ "userId": "user_1",
+ "nickname": "alex_dev",
+ "totalSubmissions": 18,
+ "completedTasks": 12,
+ "chainProgress": [
+ {
+ "chainId": "chain_1",
+ "chainName": "Backend разработка",
+ "totalTasks": 10,
+ "completedTasks": 6,
+ "progressPercent": 60
+ },
+ {
+ "chainId": "chain_2",
+ "chainName": "Frontend разработка",
+ "totalTasks": 10,
+ "completedTasks": 6,
+ "progressPercent": 60
+ }
+ ]
+ },
+ {
+ "userId": "user_2",
+ "nickname": "maria_coder",
+ "totalSubmissions": 15,
+ "completedTasks": 9,
+ "chainProgress": [
+ {
+ "chainId": "chain_1",
+ "chainName": "Backend разработка",
+ "totalTasks": 10,
+ "completedTasks": 5,
+ "progressPercent": 50
+ },
+ {
+ "chainId": "chain_2",
+ "chainName": "Frontend разработка",
+ "totalTasks": 10,
+ "completedTasks": 4,
+ "progressPercent": 40
+ }
+ ]
+ },
+ {
+ "userId": "user_3",
+ "nickname": "ivan_programmer",
+ "totalSubmissions": 10,
+ "completedTasks": 5,
+ "chainProgress": [
+ {
+ "chainId": "chain_1",
+ "chainName": "Backend разработка",
+ "totalTasks": 10,
+ "completedTasks": 3,
+ "progressPercent": 30
+ },
+ {
+ "chainId": "chain_2",
+ "chainName": "Frontend разработка",
+ "totalTasks": 10,
+ "completedTasks": 2,
+ "progressPercent": 20
+ }
+ ]
+ },
+ {
+ "userId": "user_4",
+ "nickname": "kate_fullstack",
+ "totalSubmissions": 22,
+ "completedTasks": 15,
+ "chainProgress": [
+ {
+ "chainId": "chain_1",
+ "chainName": "Backend разработка",
+ "totalTasks": 10,
+ "completedTasks": 8,
+ "progressPercent": 80
+ },
+ {
+ "chainId": "chain_2",
+ "chainName": "Frontend разработка",
+ "totalTasks": 10,
+ "completedTasks": 7,
+ "progressPercent": 70
+ }
+ ]
+ },
+ {
+ "userId": "user_5",
+ "nickname": "dmitry_backend",
+ "totalSubmissions": 12,
+ "completedTasks": 6,
+ "chainProgress": [
+ {
+ "chainId": "chain_1",
+ "chainName": "Backend разработка",
+ "totalTasks": 10,
+ "completedTasks": 5,
+ "progressPercent": 50
+ },
+ {
+ "chainId": "chain_2",
+ "chainName": "Frontend разработка",
+ "totalTasks": 10,
+ "completedTasks": 1,
+ "progressPercent": 10
+ }
+ ]
+ },
+ {
+ "userId": "user_6",
+ "nickname": "anna_react",
+ "totalSubmissions": 14,
+ "completedTasks": 7,
+ "chainProgress": [
+ {
+ "chainId": "chain_1",
+ "chainName": "Backend разработка",
+ "totalTasks": 10,
+ "completedTasks": 1,
+ "progressPercent": 10
+ },
+ {
+ "chainId": "chain_2",
+ "chainName": "Frontend разработка",
+ "totalTasks": 10,
+ "completedTasks": 6,
+ "progressPercent": 60
+ }
+ ]
+ }
+ ],
+ "chainsDetailed": [
+ {
+ "chainId": "chain_1",
+ "name": "Backend разработка",
+ "totalTasks": 10,
+ "tasks": [
+ { "taskId": "task_1", "title": "Создание REST API", "description": "Создайте REST API с Express.js" },
+ { "taskId": "task_2", "title": "Работа с базой данных MongoDB", "description": "Интегрируйте MongoDB" },
+ { "taskId": "task_3", "title": "JWT аутентификация", "description": "Реализуйте JWT-аутентификацию" },
+ { "taskId": "task_4", "title": "Middleware для Express", "description": "Создайте кастомные middleware" },
+ { "taskId": "task_5", "title": "WebSocket сервер", "description": "Реализуйте WebSocket для real-time" },
+ { "taskId": "task_6", "title": "Кэширование с Redis", "description": "Внедрите Redis для кэширования" },
+ { "taskId": "task_7", "title": "GraphQL Schema", "description": "Создайте GraphQL API" },
+ { "taskId": "task_8", "title": "Docker контейнеризация", "description": "Контейнеризируйте приложение" },
+ { "taskId": "task_9", "title": "CI/CD Pipeline", "description": "Настройте CI/CD" },
+ { "taskId": "task_10", "title": "Микросервисная архитектура", "description": "Разбейте на микросервисы" }
+ ],
+ "participantProgress": [
+ {
+ "userId": "user_1",
+ "nickname": "alex_dev",
+ "taskProgress": [
+ { "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" },
+ { "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "completed" },
+ { "taskId": "task_3", "taskTitle": "JWT аутентификация", "status": "completed" },
+ { "taskId": "task_4", "taskTitle": "Middleware для Express", "status": "completed" },
+ { "taskId": "task_5", "taskTitle": "WebSocket сервер", "status": "completed" },
+ { "taskId": "task_6", "taskTitle": "Кэширование с Redis", "status": "completed" },
+ { "taskId": "task_7", "taskTitle": "GraphQL Schema", "status": "in_progress" },
+ { "taskId": "task_8", "taskTitle": "Docker контейнеризация", "status": "not_started" },
+ { "taskId": "task_9", "taskTitle": "CI/CD Pipeline", "status": "not_started" },
+ { "taskId": "task_10", "taskTitle": "Микросервисная архитектура", "status": "not_started" }
+ ],
+ "completedCount": 6,
+ "progressPercent": 60
+ },
+ {
+ "userId": "user_2",
+ "nickname": "maria_coder",
+ "taskProgress": [
+ { "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" },
+ { "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "completed" },
+ { "taskId": "task_3", "taskTitle": "JWT аутентификация", "status": "completed" },
+ { "taskId": "task_4", "taskTitle": "Middleware для Express", "status": "completed" },
+ { "taskId": "task_5", "taskTitle": "WebSocket сервер", "status": "completed" },
+ { "taskId": "task_6", "taskTitle": "Кэширование с Redis", "status": "needs_revision" },
+ { "taskId": "task_7", "taskTitle": "GraphQL Schema", "status": "pending" },
+ { "taskId": "task_8", "taskTitle": "Docker контейнеризация", "status": "not_started" },
+ { "taskId": "task_9", "taskTitle": "CI/CD Pipeline", "status": "not_started" },
+ { "taskId": "task_10", "taskTitle": "Микросервисная архитектура", "status": "not_started" }
+ ],
+ "completedCount": 5,
+ "progressPercent": 50
+ },
+ {
+ "userId": "user_3",
+ "nickname": "ivan_programmer",
+ "taskProgress": [
+ { "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" },
+ { "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "completed" },
+ { "taskId": "task_3", "taskTitle": "JWT аутентификация", "status": "completed" },
+ { "taskId": "task_4", "taskTitle": "Middleware для Express", "status": "needs_revision" },
+ { "taskId": "task_5", "taskTitle": "WebSocket сервер", "status": "not_started" },
+ { "taskId": "task_6", "taskTitle": "Кэширование с Redis", "status": "not_started" },
+ { "taskId": "task_7", "taskTitle": "GraphQL Schema", "status": "not_started" },
+ { "taskId": "task_8", "taskTitle": "Docker контейнеризация", "status": "not_started" },
+ { "taskId": "task_9", "taskTitle": "CI/CD Pipeline", "status": "not_started" },
+ { "taskId": "task_10", "taskTitle": "Микросервисная архитектура", "status": "not_started" }
+ ],
+ "completedCount": 3,
+ "progressPercent": 30
+ },
+ {
+ "userId": "user_4",
+ "nickname": "kate_fullstack",
+ "taskProgress": [
+ { "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" },
+ { "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "completed" },
+ { "taskId": "task_3", "taskTitle": "JWT аутентификация", "status": "completed" },
+ { "taskId": "task_4", "taskTitle": "Middleware для Express", "status": "completed" },
+ { "taskId": "task_5", "taskTitle": "WebSocket сервер", "status": "completed" },
+ { "taskId": "task_6", "taskTitle": "Кэширование с Redis", "status": "completed" },
+ { "taskId": "task_7", "taskTitle": "GraphQL Schema", "status": "completed" },
+ { "taskId": "task_8", "taskTitle": "Docker контейнеризация", "status": "completed" },
+ { "taskId": "task_9", "taskTitle": "CI/CD Pipeline", "status": "in_progress" },
+ { "taskId": "task_10", "taskTitle": "Микросервисная архитектура", "status": "not_started" }
+ ],
+ "completedCount": 8,
+ "progressPercent": 80
+ },
+ {
+ "userId": "user_5",
+ "nickname": "dmitry_backend",
+ "taskProgress": [
+ { "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" },
+ { "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "completed" },
+ { "taskId": "task_3", "taskTitle": "JWT аутентификация", "status": "completed" },
+ { "taskId": "task_4", "taskTitle": "Middleware для Express", "status": "completed" },
+ { "taskId": "task_5", "taskTitle": "WebSocket сервер", "status": "completed" },
+ { "taskId": "task_6", "taskTitle": "Кэширование с Redis", "status": "in_progress" },
+ { "taskId": "task_7", "taskTitle": "GraphQL Schema", "status": "not_started" },
+ { "taskId": "task_8", "taskTitle": "Docker контейнеризация", "status": "not_started" },
+ { "taskId": "task_9", "taskTitle": "CI/CD Pipeline", "status": "not_started" },
+ { "taskId": "task_10", "taskTitle": "Микросервисная архитектура", "status": "not_started" }
+ ],
+ "completedCount": 5,
+ "progressPercent": 50
+ },
+ {
+ "userId": "user_6",
+ "nickname": "anna_react",
+ "taskProgress": [
+ { "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" },
+ { "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "pending" },
+ { "taskId": "task_3", "taskTitle": "JWT аутентификация", "status": "not_started" },
+ { "taskId": "task_4", "taskTitle": "Middleware для Express", "status": "not_started" },
+ { "taskId": "task_5", "taskTitle": "WebSocket сервер", "status": "not_started" },
+ { "taskId": "task_6", "taskTitle": "Кэширование с Redis", "status": "not_started" },
+ { "taskId": "task_7", "taskTitle": "GraphQL Schema", "status": "not_started" },
+ { "taskId": "task_8", "taskTitle": "Docker контейнеризация", "status": "not_started" },
+ { "taskId": "task_9", "taskTitle": "CI/CD Pipeline", "status": "not_started" },
+ { "taskId": "task_10", "taskTitle": "Микросервисная архитектура", "status": "not_started" }
+ ],
+ "completedCount": 1,
+ "progressPercent": 10
+ }
+ ]
+ },
+ {
+ "chainId": "chain_2",
+ "name": "Frontend разработка",
+ "totalTasks": 10,
+ "tasks": [
+ { "taskId": "task_11", "title": "React компоненты", "description": "Создайте переиспользуемые компоненты" },
+ { "taskId": "task_12", "title": "React Hooks", "description": "Используйте хуки" },
+ { "taskId": "task_13", "title": "Redux State Management", "description": "Управление состоянием" },
+ { "taskId": "task_14", "title": "React Router", "description": "Маршрутизация" },
+ { "taskId": "task_15", "title": "Form валидация", "description": "Валидация форм" },
+ { "taskId": "task_16", "title": "API интеграция", "description": "Интеграция с API" },
+ { "taskId": "task_17", "title": "CSS-in-JS стилизация", "description": "Стилизация" },
+ { "taskId": "task_18", "title": "Оптимизация производительности", "description": "Оптимизация React" },
+ { "taskId": "task_19", "title": "Unit тесты с Jest", "description": "Юнит тестирование" },
+ { "taskId": "task_20", "title": "E2E тесты с Playwright", "description": "End-to-end тестирование" }
+ ],
+ "participantProgress": [
+ {
+ "userId": "user_1",
+ "nickname": "alex_dev",
+ "taskProgress": [
+ { "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" },
+ { "taskId": "task_12", "taskTitle": "React Hooks", "status": "completed" },
+ { "taskId": "task_13", "taskTitle": "Redux State Management", "status": "completed" },
+ { "taskId": "task_14", "taskTitle": "React Router", "status": "completed" },
+ { "taskId": "task_15", "taskTitle": "Form валидация", "status": "completed" },
+ { "taskId": "task_16", "taskTitle": "API интеграция", "status": "completed" },
+ { "taskId": "task_17", "taskTitle": "CSS-in-JS стилизация", "status": "in_progress" },
+ { "taskId": "task_18", "taskTitle": "Оптимизация производительности", "status": "not_started" },
+ { "taskId": "task_19", "taskTitle": "Unit тесты с Jest", "status": "not_started" },
+ { "taskId": "task_20", "taskTitle": "E2E тесты с Playwright", "status": "not_started" }
+ ],
+ "completedCount": 6,
+ "progressPercent": 60
+ },
+ {
+ "userId": "user_2",
+ "nickname": "maria_coder",
+ "taskProgress": [
+ { "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" },
+ { "taskId": "task_12", "taskTitle": "React Hooks", "status": "completed" },
+ { "taskId": "task_13", "taskTitle": "Redux State Management", "status": "completed" },
+ { "taskId": "task_14", "taskTitle": "React Router", "status": "completed" },
+ { "taskId": "task_15", "taskTitle": "Form валидация", "status": "needs_revision" },
+ { "taskId": "task_16", "taskTitle": "API интеграция", "status": "pending" },
+ { "taskId": "task_17", "taskTitle": "CSS-in-JS стилизация", "status": "not_started" },
+ { "taskId": "task_18", "taskTitle": "Оптимизация производительности", "status": "not_started" },
+ { "taskId": "task_19", "taskTitle": "Unit тесты с Jest", "status": "not_started" },
+ { "taskId": "task_20", "taskTitle": "E2E тесты с Playwright", "status": "not_started" }
+ ],
+ "completedCount": 4,
+ "progressPercent": 40
+ },
+ {
+ "userId": "user_3",
+ "nickname": "ivan_programmer",
+ "taskProgress": [
+ { "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" },
+ { "taskId": "task_12", "taskTitle": "React Hooks", "status": "completed" },
+ { "taskId": "task_13", "taskTitle": "Redux State Management", "status": "in_progress" },
+ { "taskId": "task_14", "taskTitle": "React Router", "status": "not_started" },
+ { "taskId": "task_15", "taskTitle": "Form валидация", "status": "not_started" },
+ { "taskId": "task_16", "taskTitle": "API интеграция", "status": "not_started" },
+ { "taskId": "task_17", "taskTitle": "CSS-in-JS стилизация", "status": "not_started" },
+ { "taskId": "task_18", "taskTitle": "Оптимизация производительности", "status": "not_started" },
+ { "taskId": "task_19", "taskTitle": "Unit тесты с Jest", "status": "not_started" },
+ { "taskId": "task_20", "taskTitle": "E2E тесты с Playwright", "status": "not_started" }
+ ],
+ "completedCount": 2,
+ "progressPercent": 20
+ },
+ {
+ "userId": "user_4",
+ "nickname": "kate_fullstack",
+ "taskProgress": [
+ { "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" },
+ { "taskId": "task_12", "taskTitle": "React Hooks", "status": "completed" },
+ { "taskId": "task_13", "taskTitle": "Redux State Management", "status": "completed" },
+ { "taskId": "task_14", "taskTitle": "React Router", "status": "completed" },
+ { "taskId": "task_15", "taskTitle": "Form валидация", "status": "completed" },
+ { "taskId": "task_16", "taskTitle": "API интеграция", "status": "completed" },
+ { "taskId": "task_17", "taskTitle": "CSS-in-JS стилизация", "status": "completed" },
+ { "taskId": "task_18", "taskTitle": "Оптимизация производительности", "status": "in_progress" },
+ { "taskId": "task_19", "taskTitle": "Unit тесты с Jest", "status": "not_started" },
+ { "taskId": "task_20", "taskTitle": "E2E тесты с Playwright", "status": "not_started" }
+ ],
+ "completedCount": 7,
+ "progressPercent": 70
+ },
+ {
+ "userId": "user_5",
+ "nickname": "dmitry_backend",
+ "taskProgress": [
+ { "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" },
+ { "taskId": "task_12", "taskTitle": "React Hooks", "status": "pending" },
+ { "taskId": "task_13", "taskTitle": "Redux State Management", "status": "not_started" },
+ { "taskId": "task_14", "taskTitle": "React Router", "status": "not_started" },
+ { "taskId": "task_15", "taskTitle": "Form валидация", "status": "not_started" },
+ { "taskId": "task_16", "taskTitle": "API интеграция", "status": "not_started" },
+ { "taskId": "task_17", "taskTitle": "CSS-in-JS стилизация", "status": "not_started" },
+ { "taskId": "task_18", "taskTitle": "Оптимизация производительности", "status": "not_started" },
+ { "taskId": "task_19", "taskTitle": "Unit тесты с Jest", "status": "not_started" },
+ { "taskId": "task_20", "taskTitle": "E2E тесты с Playwright", "status": "not_started" }
+ ],
+ "completedCount": 1,
+ "progressPercent": 10
+ },
+ {
+ "userId": "user_6",
+ "nickname": "anna_react",
+ "taskProgress": [
+ { "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" },
+ { "taskId": "task_12", "taskTitle": "React Hooks", "status": "completed" },
+ { "taskId": "task_13", "taskTitle": "Redux State Management", "status": "completed" },
+ { "taskId": "task_14", "taskTitle": "React Router", "status": "completed" },
+ { "taskId": "task_15", "taskTitle": "Form валидация", "status": "completed" },
+ { "taskId": "task_16", "taskTitle": "API интеграция", "status": "completed" },
+ { "taskId": "task_17", "taskTitle": "CSS-in-JS стилизация", "status": "in_progress" },
+ { "taskId": "task_18", "taskTitle": "Оптимизация производительности", "status": "not_started" },
+ { "taskId": "task_19", "taskTitle": "Unit тесты с Jest", "status": "not_started" },
+ { "taskId": "task_20", "taskTitle": "E2E тесты с Playwright", "status": "not_started" }
+ ],
+ "completedCount": 6,
+ "progressPercent": 60
+ }
+ ]
+ }
+ ]
+}
diff --git a/stubs/api/data/stats.json b/stubs/api/data/stats.json
index 41702c4..a5425b7 100644
--- a/stubs/api/data/stats.json
+++ b/stubs/api/data/stats.json
@@ -1,21 +1,21 @@
{
"users": 8,
- "tasks": 5,
- "chains": 3,
+ "tasks": 20,
+ "chains": 2,
"submissions": {
- "total": 8,
- "accepted": 5,
- "rejected": 3,
- "pending": 0,
- "inProgress": 0
+ "total": 95,
+ "accepted": 38,
+ "rejected": 42,
+ "pending": 8,
+ "inProgress": 7
},
- "averageCheckTimeMs": 3275,
+ "averageCheckTimeMs": 2143,
"queue": {
- "queueLength": 0,
- "waiting": 0,
- "inProgress": 0,
- "maxConcurrency": 5,
- "currentlyProcessing": 0
+ "queueLength": 15,
+ "waiting": 8,
+ "inProgress": 7,
+ "maxConcurrency": 10,
+ "currentlyProcessing": 7
}
}
diff --git a/stubs/api/index.js b/stubs/api/index.js
index 9a5ac54..90e0ab2 100644
--- a/stubs/api/index.js
+++ b/stubs/api/index.js
@@ -29,6 +29,7 @@ let chainsCache = null;
let usersCache = null;
let submissionsCache = null;
let statsCache = null;
+let statsV2Cache = null;
const getTasks = () => {
if (!tasksCache) tasksCache = loadJSON('tasks.json');
@@ -55,6 +56,11 @@ const getStats = () => {
return statsCache;
};
+const getStatsV2 = () => {
+ if (!statsV2Cache) statsV2Cache = loadJSON('stats-v2.json');
+ return statsV2Cache;
+};
+
router.use(timer());
// ============= TASKS =============
@@ -282,6 +288,46 @@ router.get('/challenge/stats', (req, res) => {
respond(res, stats);
});
+// GET /api/challenge/stats/v2
+router.get('/challenge/stats/v2', (req, res) => {
+ const statsV2 = getStatsV2();
+ const chainId = req.query.chainId;
+
+ // Если chainId не передан, возвращаем все данные
+ if (!chainId) {
+ respond(res, statsV2);
+ return;
+ }
+
+ // Фильтруем данные по выбранной цепочке
+ const filteredChain = statsV2.chainsDetailed.find(c => c.chainId === chainId);
+
+ if (!filteredChain) {
+ return respondError(res, 'Chain not found', 404);
+ }
+
+ // Фильтруем tasksTable - только задания из этой цепочки
+ const chainTaskIds = new Set(filteredChain.tasks.map(t => t.taskId));
+ const filteredTasksTable = statsV2.tasksTable.filter(t => chainTaskIds.has(t.taskId));
+
+ // Фильтруем activeParticipants - только участники с попытками в этой цепочке
+ const participantIds = new Set(filteredChain.participantProgress.map(p => p.userId));
+ const filteredParticipants = statsV2.activeParticipants
+ .filter(p => participantIds.has(p.userId))
+ .map(p => ({
+ ...p,
+ chainProgress: p.chainProgress.filter(cp => cp.chainId === chainId)
+ }));
+
+ // Возвращаем отфильтрованные данные
+ respond(res, {
+ ...statsV2,
+ tasksTable: filteredTasksTable,
+ activeParticipants: filteredParticipants,
+ chainsDetailed: [filteredChain]
+ });
+});
+
// GET /api/challenge/user/:userId/stats
router.get('/challenge/user/:userId/stats', (req, res) => {
const users = getUsers();