diff --git a/claude.md b/claude.md
new file mode 100644
index 0000000..d9e362e
--- /dev/null
+++ b/claude.md
@@ -0,0 +1,420 @@
+# Правила работы с Challenge Platform
+
+Этот документ содержит важные правила и паттерны для работы с кодовой базой проекта Challenge Platform.
+
+## 🎯 Chakra UI - Специфика проекта
+
+### TypeScript и Chakra UI Props
+
+Проект использует Chakra UI v2/v3 с особенностями типизации. **ВАЖНО:**
+
+#### ✅ Правильное подавление ошибок TypeScript:
+
+```tsx
+// ❌ НЕПРАВИЛЬНО - комментарий на отдельной строке НЕ работает:
+{/* @ts-expect-error Chakra UI v2 uses isDisabled */}
+
+
+// ✅ ПРАВИЛЬНО - комментарий перед элементом + все props на одной строке:
+{/* @ts-expect-error Chakra UI v2 uses isDisabled */}
+
+
+// ✅ ПРАВИЛЬНО - inline комментарий после prop:
+
+```
+
+#### Частые props требующие подавления:
+
+- `isDisabled` (Button, Input)
+- `isLoading` (Button)
+- `isInvalid` (Input)
+- `spacing` (VStack, HStack)
+
+### VStack и HStack
+
+```tsx
+// ✅ ПРАВИЛЬНО:
+{/* @ts-expect-error Chakra UI v2 uses spacing */}
+
+ {/* content */}
+
+```
+
+## 💾 LocalStorage - Ключи и паттерны
+
+### Константы для ключей
+
+**ВСЕГДА** используйте константы для ключей localStorage:
+
+```tsx
+// ✅ ПРАВИЛЬНО:
+const WORKPLACE_NUMBER_KEY = 'challengeWorkplaceNumber'
+const USER_ID_KEY = 'challengeUserId'
+const USER_NICKNAME_KEY = 'challengeNickname'
+const SELECTED_CHAIN_KEY = 'challengeSelectedChainId'
+const SELECTED_TASK_KEY = 'challengeSelectedTaskId'
+
+// Префикс 'challenge' для всех ключей проекта
+```
+
+### Синхронизация localStorage и Context
+
+Если добавляете новое поле в Context, которое должно сохраняться:
+
+1. **Добавьте константу** для ключа
+2. **Инициализируйте state** из localStorage:
+ ```tsx
+ const [field, setField] = useState(() =>
+ isBrowser() ? window.localStorage.getItem(FIELD_KEY) : null,
+ )
+ ```
+3. **Сохраняйте в localStorage** при изменении (login)
+4. **Очищайте из localStorage** при выходе (logout)
+5. **Добавьте в интерфейс** ChallengeContextValue
+6. **Добавьте в useMemo** (value и dependencies)
+
+## 🔄 Context - Паттерн обновления
+
+### Добавление нового поля в ChallengeContext
+
+Пример: добавление `workplaceNumber`
+
+**Шаг 1:** Интерфейс
+```tsx
+interface ChallengeContextValue {
+ // ... существующие поля
+ workplaceNumber: string | null // ← добавить
+}
+```
+
+**Шаг 2:** Константа
+```tsx
+const WORKPLACE_NUMBER_KEY = 'challengeWorkplaceNumber'
+```
+
+**Шаг 3:** State с инициализацией
+```tsx
+const [workplaceNumber, setWorkplaceNumber] = useState(() =>
+ isBrowser() ? window.localStorage.getItem(WORKPLACE_NUMBER_KEY) : null,
+)
+```
+
+**Шаг 4:** Обновление login
+```tsx
+const login = useCallback(
+ async (nicknameValue: string, workplaceNumberValue: string) => {
+ // ... существующий код
+ setWorkplaceNumber(workplaceNumberValue)
+
+ if (isBrowser()) {
+ // ... существующие setItem
+ window.localStorage.setItem(WORKPLACE_NUMBER_KEY, workplaceNumberValue)
+ }
+ },
+ [authUser, refreshStatsById],
+)
+```
+
+**Шаг 5:** Обновление logout
+```tsx
+const logout = useCallback(() => {
+ // ... существующие setNull
+ setWorkplaceNumber(null)
+
+ if (isBrowser()) {
+ // ... существующие removeItem
+ window.localStorage.removeItem(WORKPLACE_NUMBER_KEY)
+ }
+}, [])
+```
+
+**Шаг 6:** Добавить в value
+```tsx
+const value = useMemo(
+ () => ({
+ // ... существующие поля
+ workplaceNumber, // ← добавить
+ }),
+ [
+ // ... существующие зависимости
+ workplaceNumber, // ← добавить в dependencies
+ ],
+)
+```
+
+## 📝 Форматирование кода
+
+### Отступы
+- **2 пробела** для отступов
+- Никаких табов
+- JSX атрибуты выравниваются по отступам родителя + 2 пробела
+
+### Комментарии на русском
+```tsx
+// ✅ ПРАВИЛЬНО:
+// Проверяем, есть ли сохранённый номер рабочего места
+useEffect(() => {
+ const savedWorkplace = localStorage.getItem(WORKPLACE_NUMBER_KEY)
+ // ...
+}, [])
+```
+
+## 🔐 Паттерн двухшаговой формы
+
+Если нужна форма с несколькими шагами:
+
+```tsx
+export const MultiStepForm = () => {
+ const [step, setStep] = useState<'step1' | 'step2'>('step1')
+
+ // Проверка сохранённых данных
+ useEffect(() => {
+ const savedData = localStorage.getItem(SAVED_DATA_KEY)
+ if (savedData) {
+ setData(savedData)
+ setStep('step2') // Пропускаем первый шаг
+ }
+ }, [])
+
+ const handleStep1Submit = (e: React.FormEvent) => {
+ e.preventDefault()
+ // Сохраняем СРАЗУ после валидации
+ localStorage.setItem(SAVED_DATA_KEY, trimmedData)
+ setStep('step2')
+ }
+
+ // Условный рендеринг
+ if (step === 'step1') {
+ return
+ }
+
+ return
+}
+```
+
+### ⚠️ Важные моменты:
+1. Сохраняйте данные **сразу** после валидации, не ждите финального submit
+2. Проверяйте localStorage при монтировании компонента
+3. Пропускайте шаги, если данные уже есть
+4. Давайте возможность вернуться и изменить данные
+
+## 🎨 Header - Паттерн отображения информации
+
+Порядок отображения в Header:
+```
+Место №{number} • {nickname} • {taskProgress}
+```
+
+Важные элементы отмечаем `fontWeight="medium"`:
+```tsx
+
+ Место №{workplaceNumber}
+
+```
+
+## 🐛 Частые ошибки и решения
+
+### ❌ Ошибка: "Property 'spacing' does not exist"
+**Решение:** Добавить `@ts-expect-error` комментарий перед VStack/HStack
+
+### ❌ Ошибка: "Unused '@ts-expect-error' directive"
+**Причина:** Комментарий расположен неправильно
+**Решение:** Поместить комментарий **непосредственно** перед элементом с ошибкой, и все props элемента должны быть на одной строке или с inline комментариями
+
+### ❌ Ошибка: Данные не сохраняются между сессиями
+**Проверить:**
+1. Используется ли `localStorage.setItem`?
+2. Инициализируется ли state из localStorage?
+3. Очищается ли в logout?
+4. Синхронизированы ли ключи (одинаковые константы)?
+
+### ❌ Ошибка: Context не обновляется
+**Проверить:**
+1. Добавлено ли новое поле в `useMemo` value?
+2. Добавлено ли новое поле в dependencies массив `useMemo`?
+3. Добавлено ли в интерфейс `ChallengeContextValue`?
+
+## 📚 Структура проекта
+
+```
+src/
+├── components/ # Компоненты
+│ ├── LoginForm.tsx # Форма входа (может быть многошаговой)
+│ ├── Header.tsx # Шапка с информацией о пользователе
+│ └── ...
+├── context/ # React Context
+│ └── ChallengeContext.tsx # Главный контекст приложения
+├── pages/ # Страницы
+│ └── main/
+└── ...
+```
+
+## ✅ Чеклист перед коммитом
+
+- [ ] Нет linter errors (проверить `read_lints`)
+- [ ] TypeScript ошибки подавлены правильно
+- [ ] LocalStorage ключи используют константы
+- [ ] Новые поля Context добавлены во все нужные места
+- [ ] Комментарии на русском языке
+- [ ] Отступы - 2 пробела
+- [ ] Данные сохраняются и восстанавливаются из localStorage
+
+## 🎯 Динамическая очередь проверки с прогресс-баром
+
+### Симуляция очереди в API (stubs/api/index.js)
+
+Для лучшего UX очередь проверки показывает прогресс через прогресс-бар.
+
+**Паттерн реализации:**
+
+```javascript
+// Таймауты: 100ms для быстрой работы в dev
+const timer = (time = 100) => (req, res, next) => setTimeout(next, time)
+
+// Храним состояние для каждого queueId
+const queueStates = {}
+
+router.get('/challenge/check-status/:queueId', (req, res) => {
+ const queueId = req.params.queueId
+
+ // Инициализируем с позицией 3
+ if (!queueStates[queueId]) {
+ queueStates[queueId] = {
+ position: 3,
+ initialPosition: 3, // Для расчёта прогресса
+ pollCount: 0
+ }
+ }
+
+ const state = queueStates[queueId]
+ state.pollCount++
+
+ // Каждый запрос уменьшаем позицию (быстро)
+ if (state.pollCount >= 1 && state.position > 0) {
+ state.position--
+ state.pollCount = 0
+ }
+
+ // Когда позиция 0 → возвращаем результат проверки
+ if (state.position === 0 && state.pollCount >= 1) {
+ delete queueStates[queueId] // Очищаем
+ return res.json({ status: 'completed', submission: {...} })
+ }
+
+ // Возвращаем текущую позицию + начальную для прогресса
+ return res.json({
+ status: state.position > 0 ? 'waiting' : 'in_progress',
+ position: state.position,
+ initialPosition: state.initialPosition // ← Важно!
+ })
+})
+```
+
+### UI с прогресс-баром
+
+**Новый подход:** Вместо отображения номера в очереди показываем прогресс-бар.
+
+**Расчёт прогресса:**
+
+```tsx
+const checkingProgress = (() => {
+ if (!queueStatus) return 0
+
+ const initial = queueStatus.initialPosition || 3
+ const current = queueStatus.position || 0
+
+ if (queueStatus.status === 'in_progress') return 90 // Почти готово
+
+ // От 0% до 80% по мере движения в очереди
+ const progress = ((initial - current) / initial) * 80
+ return Math.max(10, progress) // Минимум 10% чтобы было видно
+})()
+```
+
+**Кастомный прогресс-бар с анимацией:**
+
+```tsx
+
+
+ {/* Анимированный блик */}
+
+
+
+```
+
+**Текст под прогресс-баром:**
+```tsx
+
+ {checkingProgress < 50 ? 'Ожидание в очереди' :
+ checkingProgress < 90 ? 'Начинаем проверку' :
+ 'Почти готово'}
+
+```
+
+### ⚠️ Важно
+
+- **Таймауты в dev:** 100ms для быстрой работы. На prod бэкенд будет быстрее
+- **Прогресс:** 0-80% = движение в очереди, 80-90% = начало проверки, 90%+ = финализация
+- **Не показываем номер** в очереди - только прогресс-бар
+- **Текст:** простой "Проверяем решение..." вместо деталей
+- **Анимация shimmer:** добавляет ощущение процесса
+- Полинг: интервал 2 секунды (`PollingManager`)
+- **КРИТИЧНО:** При смене задания сбрасывайте все локальные состояния
+
+### Сброс состояния при смене задания
+
+**ОБЯЗАТЕЛЬНО** добавляйте useEffect для сброса локальных состояний:
+
+```tsx
+// Сбрасываем состояние при смене задания
+useEffect(() => {
+ setLastResult(null)
+ setPrevPosition(null)
+ setPositionChanged(false)
+ // ... любые другие локальные состояния
+}, [task.id]) // ← зависимость от task.id
+```
+
+**Почему это важно:**
+- Без сброса плашки успеха/ошибки от предыдущего задания остаются видимыми
+- Пользователь видит некорректную информацию
+- Состояние очереди "протекает" между заданиями
+
+## 🎓 Философия проекта
+
+1. **Удобство пользователя превыше всего** - сохраняем данные, не переспрашиваем
+2. **Типобезопасность** - используем TypeScript, подавляем ошибки осознанно
+3. **Консистентность** - следуем установленным паттернам
+4. **Персистентность** - важные данные (номер места, выбор цепочки) сохраняются в localStorage
+5. **Обратная связь** - пользователь всегда видит что происходит (позиция в очереди, прогресс проверки)
+
+---
+
+**Версия:** 1.1
+**Дата:** 13.12.2025
+**Автор:** Claude (на основе опыта работы с проектом)
+
diff --git a/src/__data__/types.ts b/src/__data__/types.ts
index 879cc15..c887635 100644
--- a/src/__data__/types.ts
+++ b/src/__data__/types.ts
@@ -48,6 +48,7 @@ export interface QueueStatus {
submission?: ChallengeSubmission
error?: string
position?: number
+ initialPosition?: number
}
export interface TaskAttempt {
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 61c0ccc..6757c06 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -9,7 +9,7 @@ interface HeaderProps {
}
export const Header = ({ chainName, taskProgress }: HeaderProps) => {
- const { nickname, logout } = useChallenge()
+ const { nickname, workplaceNumber, logout } = useChallenge()
if (!nickname) return null
@@ -21,6 +21,14 @@ export const Header = ({ chainName, taskProgress }: HeaderProps) => {
{chainName || 'Challenge Platform'}
+ {workplaceNumber && (
+ <>
+
+ Место №{workplaceNumber}
+
+ •
+ >
+ )}
{nickname}
diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx
index 548c3e0..63a13a2 100644
--- a/src/components/LoginForm.tsx
+++ b/src/components/LoginForm.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react'
+import React, { useEffect, useState } from 'react'
import {
Box,
Button,
@@ -10,12 +10,41 @@ import {
import { useChallenge } from '../context/ChallengeContext'
+const WORKPLACE_NUMBER_KEY = 'challengeWorkplaceNumber'
+
export const LoginForm = () => {
+ const [step, setStep] = useState<'workplace' | 'fio'>('workplace')
+ const [workplaceNumber, setWorkplaceNumber] = useState('')
const [fullName, setFullName] = useState('')
const [error, setError] = useState('')
const { login, isAuthLoading } = useChallenge()
- const handleSubmit = async (e: React.FormEvent) => {
+ // Проверяем, есть ли сохранённый номер рабочего места
+ useEffect(() => {
+ const savedWorkplace = localStorage.getItem(WORKPLACE_NUMBER_KEY)
+ if (savedWorkplace) {
+ setWorkplaceNumber(savedWorkplace)
+ setStep('fio')
+ }
+ }, [])
+
+ const handleWorkplaceSubmit = (e: React.FormEvent) => {
+ e.preventDefault()
+
+ const trimmedWorkplace = workplaceNumber.trim()
+ if (!trimmedWorkplace) {
+ setError('Пожалуйста, введите номер рабочего места')
+ return
+ }
+
+ // Сохраняем номер рабочего места сразу
+ localStorage.setItem(WORKPLACE_NUMBER_KEY, trimmedWorkplace)
+ setWorkplaceNumber(trimmedWorkplace)
+ setError('')
+ setStep('fio')
+ }
+
+ const handleFioSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const trimmedName = fullName.trim()
@@ -31,13 +60,76 @@ export const LoginForm = () => {
try {
setError('')
- await login(trimmedName)
+ await login(trimmedName, workplaceNumber.trim())
} catch (err) {
setError('Произошла ошибка при входе. Попробуйте снова.')
console.error('Login error:', err)
}
}
+ if (step === 'workplace') {
+ return (
+
+
+ {/* @ts-expect-error Chakra UI v2 uses spacing */}
+
+
+
+ Challenge Platform
+
+
+ Добро пожаловать! Введите номер рабочего места
+
+
+
+
+
+
+
+ )
+ }
+
return (
{
maxW="480px"
w="full"
>
+ {/* @ts-expect-error Chakra UI v2 uses spacing */}
Challenge Platform
-
- Добро пожаловать! Введите ваше ФИО для начала работы
+
+ Рабочее место: №{workplaceNumber}
+
+
+ (номер сохранён)
+
+
+ Введите ваше ФИО для начала работы
-
@@ -96,4 +203,3 @@ export const LoginForm = () => {
)
}
-
diff --git a/src/components/personal/TaskWorkspace.tsx b/src/components/personal/TaskWorkspace.tsx
index d046ed3..fcea615 100644
--- a/src/components/personal/TaskWorkspace.tsx
+++ b/src/components/personal/TaskWorkspace.tsx
@@ -3,7 +3,6 @@ import {
Box,
Button,
HStack,
- Spinner,
Text,
Textarea,
VStack,
@@ -33,6 +32,26 @@ export const TaskWorkspace = ({ task, onTaskComplete }: TaskWorkspaceProps) => {
const isAccepted = finalSubmission?.status === 'accepted'
const needsRevision = finalSubmission?.status === 'needs_revision'
+ // Вычисляем прогресс проверки (0-100%)
+ const checkingProgress = (() => {
+ if (!queueStatus) return 0
+
+ const initial = queueStatus.initialPosition || 3
+ const current = queueStatus.position || 0
+
+ if (queueStatus.status === 'in_progress') return 90 // Почти готово
+ if (current === 0) return 90
+
+ // От 0% до 80% по мере движения в очереди
+ const progress = ((initial - current) / initial) * 80
+ return Math.max(10, progress) // Минимум 10% чтобы было видно
+ })()
+
+ // Сбрасываем состояние при смене задания
+ useEffect(() => {
+ setLastResult(null)
+ }, [task.id])
+
// Обновляем сохраненный результат только когда получаем новый
useEffect(() => {
if (finalSubmission) {
@@ -230,14 +249,59 @@ export const TaskWorkspace = ({ task, onTaskComplete }: TaskWorkspaceProps) => {
{/* Статус проверки и результат - фиксированное место */}
{queueStatus && !finalSubmission ? (
-
-
-
-
- {queueStatus.status === 'waiting' ? 'Ожидание в очереди...' : 'Проверяем решение...'}
- {typeof queueStatus.position === 'number' && queueStatus.position > 0 && ` (позиция: ${queueStatus.position})`}
-
-
+
+
+
+
+ Проверяем решение...
+
+
+
+
+ {/* Кастомный прогресс-бар */}
+
+
+ {/* Анимированные полоски */}
+
+
+
+
+ {checkingProgress < 50 ? 'Ожидание в очереди' : checkingProgress < 90 ? 'Начинаем проверку' : 'Почти готово'}
+
+
+
) : showAccepted ? (
diff --git a/src/context/ChallengeContext.tsx b/src/context/ChallengeContext.tsx
index 0c3a4a6..1b257a4 100644
--- a/src/context/ChallengeContext.tsx
+++ b/src/context/ChallengeContext.tsx
@@ -56,13 +56,14 @@ class ChallengeCache {
interface ChallengeContextValue {
userId: string | null
nickname: string | null
+ workplaceNumber: string | null
stats: UserStats | null
personalDashboard: PersonalDashboard | null
chains: ChallengeChain[]
isAuthenticated: boolean
isAuthLoading: boolean
isStatsLoading: boolean
- login: (nickname: string) => Promise
+ login: (nickname: string, workplaceNumber: string) => Promise
logout: () => void
refreshStats: () => Promise
eventEmitter: ChallengeEventEmitter
@@ -78,13 +79,14 @@ const ChallengeContext = createContext(undefi
const USER_ID_KEY = 'challengeUserId'
const USER_NICKNAME_KEY = 'challengeNickname'
+const WORKPLACE_NUMBER_KEY = 'challengeWorkplaceNumber'
export const ChallengeProvider = ({ children }: PropsWithChildren) => {
const cacheRef = useRef(new ChallengeCache())
const metricsCollector = useMemo(() => new MetricsCollector(), [])
const behaviorTracker = useMemo(() => new BehaviorTracker(), [])
const eventEmitter = useMemo(() => new ChallengeEventEmitter(), [])
- const pollingManager = useMemo(() => new PollingManager(), [])
+ const pollingManager = useMemo(() => new PollingManager({ initialDelay: 800, maxDelay: 5000, multiplier: 1.15 }), [])
const [userId, setUserId] = useState(() =>
isBrowser() ? window.localStorage.getItem(USER_ID_KEY) : null,
@@ -92,6 +94,9 @@ export const ChallengeProvider = ({ children }: PropsWithChildren) => {
const [nickname, setNickname] = useState(() =>
isBrowser() ? window.localStorage.getItem(USER_NICKNAME_KEY) : null,
)
+ const [workplaceNumber, setWorkplaceNumber] = useState(() =>
+ isBrowser() ? window.localStorage.getItem(WORKPLACE_NUMBER_KEY) : null,
+ )
const [stats, setStats] = useState(null)
const [personalDashboard, setPersonalDashboard] = useState(null)
const [chains, setChains] = useState(() => {
@@ -151,14 +156,16 @@ export const ChallengeProvider = ({ children }: PropsWithChildren) => {
}, [refreshStats, userId])
const login = useCallback(
- async (nicknameValue: string) => {
+ async (nicknameValue: string, workplaceNumberValue: string) => {
const response = await authUser({ nickname: nicknameValue }).unwrap()
setUserId(response.userId)
setNickname(nicknameValue)
+ setWorkplaceNumber(workplaceNumberValue)
if (isBrowser()) {
window.localStorage.setItem(USER_ID_KEY, response.userId)
window.localStorage.setItem(USER_NICKNAME_KEY, nicknameValue)
+ window.localStorage.setItem(WORKPLACE_NUMBER_KEY, workplaceNumberValue)
}
cacheRef.current.clear('chains')
@@ -170,6 +177,7 @@ export const ChallengeProvider = ({ children }: PropsWithChildren) => {
const logout = useCallback(() => {
setUserId(null)
setNickname(null)
+ setWorkplaceNumber(null)
setStats(null)
setPersonalDashboard(null)
cacheRef.current.clear()
@@ -177,6 +185,7 @@ export const ChallengeProvider = ({ children }: PropsWithChildren) => {
if (isBrowser()) {
window.localStorage.removeItem(USER_ID_KEY)
window.localStorage.removeItem(USER_NICKNAME_KEY)
+ window.localStorage.removeItem(WORKPLACE_NUMBER_KEY)
window.localStorage.removeItem('challengeSelectedChainId')
window.localStorage.removeItem('challengeSelectedTaskId')
}
@@ -188,6 +197,7 @@ export const ChallengeProvider = ({ children }: PropsWithChildren) => {
() => ({
userId,
nickname,
+ workplaceNumber,
stats,
personalDashboard,
chains,
@@ -217,6 +227,7 @@ export const ChallengeProvider = ({ children }: PropsWithChildren) => {
logout,
metricsCollector,
nickname,
+ workplaceNumber,
personalDashboard,
pollingManager,
refreshStats,
diff --git a/src/pages/main/main.tsx b/src/pages/main/main.tsx
index 5f125b9..eb60aaf 100644
--- a/src/pages/main/main.tsx
+++ b/src/pages/main/main.tsx
@@ -78,14 +78,19 @@ export const MainPage = () => {
const unsubscribe = eventEmitter.on('submission_completed', (event) => {
const submission = (event.data as { submission?: { status: string; attemptNumber: number } })?.submission
const accepted = submission?.status === 'accepted'
- const title = accepted ? 'Задание принято' : 'Задание требует доработки'
+
+ if (!accepted) {
+ return
+ }
+
+ const title = 'Задание принято'
const description = submission ? `Попытка №${submission.attemptNumber}` : undefined
if (notificationTimeoutRef.current) {
window.clearTimeout(notificationTimeoutRef.current)
}
- setNotification({ status: accepted ? 'success' : 'warning', title, description })
+ // setNotification({ status: 'success', title, description })
notificationTimeoutRef.current = window.setTimeout(() => setNotification(null), 4000)
})
diff --git a/stubs/api/index.js b/stubs/api/index.js
index f259838..b03aa43 100644
--- a/stubs/api/index.js
+++ b/stubs/api/index.js
@@ -2,7 +2,7 @@ const fs = require('fs')
const path = require('path')
const router = require('express').Router()
-const timer = (time = 300) => (req, res, next) => setTimeout(next, time)
+const timer = (time = 100) => (req, res, next) => setTimeout(next, time)
const dataDir = path.join(__dirname, 'data')
@@ -57,18 +57,97 @@ router.get('/challenge/task/:id', (req, res) => {
router.post('/challenge/submit', (req, res) => {
const response = readJson('submit.json')
+ const queueId = response.body?.queueId
+
+ if (queueId) {
+ queueBehaviors[queueId] = queueBehaviors[queueId] ?? { nextFailure: false, attemptNumber: 0 }
+ queueStates[queueId] = {
+ position: 3,
+ initialPosition: 3,
+ pollCount: 0,
+ startTime: Date.now(),
+ }
+ }
+
res.json(response)
})
+// Храним состояние очереди для каждого queueId
+const queueStates = {}
+const queueBehaviors = {}
+
router.get('/challenge/check-status/:queueId', (req, res) => {
- const data = readJson('queue-status.json')
- const statuses = data.body || data
- const status = statuses[req.params.queueId]
-
- if (!status) {
- return sendNotFound(res, `Статус очереди ${req.params.queueId} не найден`)
+ const queueId = req.params.queueId
+
+ // Инициализируем состояние очереди, если его нет
+ if (!queueStates[queueId]) {
+ queueStates[queueId] = {
+ position: 3,
+ initialPosition: 3,
+ pollCount: 0,
+ startTime: Date.now()
+ }
}
+
+ const state = queueStates[queueId]
+ state.pollCount++
+
+ // Симулируем движение в очереди
+ // Каждый запрос уменьшаем позицию (быстрее)
+ if (state.pollCount >= 1 && state.position > 0) {
+ state.position--
+ state.pollCount = 0
+ }
+
+ // Если позиция 0, переходим к проверке
+ if (state.position === 0 && state.pollCount >= 1) {
+ const behavior = queueBehaviors[queueId] ?? { nextFailure: false, attemptNumber: 0 }
+ const attemptNumber = behavior.attemptNumber + 1
+ behavior.attemptNumber = attemptNumber
+ const shouldFail = behavior.nextFailure
+ behavior.nextFailure = !shouldFail
+ const baseSubmission = {
+ _id: `submission-${queueId}-${attemptNumber}`,
+ id: `submission-${queueId}-${attemptNumber}`,
+ user: 'user-frontend-001',
+ task: 'task-html-intro',
+ result: 'Hello
',
+ queueId,
+ submittedAt: new Date(state.startTime).toISOString(),
+ checkedAt: new Date().toISOString(),
+ attemptNumber,
+ }
+
+ const submission = shouldFail
+ ? {
+ ...baseSubmission,
+ status: 'needs_revision',
+ feedback: 'Добавьте описание внутри и поясните, зачем нужен заголовок.',
+ }
+ : {
+ ...baseSubmission,
+ status: 'accepted',
+ feedback: 'Отличная работа! Теперь можно двигаться дальше.',
+ }
+
+ delete queueStates[queueId]
+
+ return res.json({
+ success: true,
+ body: {
+ status: 'completed',
+ position: 0,
+ submission,
+ },
+ })
+ }
+
+ // Возвращаем текущее состояние очереди с начальной позицией для расчёта прогресса
+ const status = state.position > 0
+ ? { status: 'waiting', position: state.position, initialPosition: state.initialPosition }
+ : { status: 'in_progress', position: 0, initialPosition: state.initialPosition }
+
return res.json({ success: true, body: status })
})