5.0.9: Ожидание бэкенда при завершении теста
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m5s

This commit is contained in:
poignatov
2026-02-09 16:07:36 +03:00
parent 72da547b80
commit 29bd50acab
3 changed files with 121 additions and 83 deletions

View File

@@ -1 +1 @@
5.0.8 5.0.9

View File

@@ -1,6 +1,6 @@
{ {
"name": "play-life-web", "name": "play-life-web",
"version": "5.0.8", "version": "5.0.9",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react' import React, { useState, useEffect, useRef, useCallback } from 'react'
import { useAuth } from './auth/AuthContext' import { useAuth } from './auth/AuthContext'
import LoadingError from './LoadingError' import LoadingError from './LoadingError'
import './TestWords.css' import './TestWords.css'
@@ -26,11 +26,14 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
const [error, setError] = useState('') const [error, setError] = useState('')
const [showPreview, setShowPreview] = useState(false) // Показывать ли экран предпросмотра const [showPreview, setShowPreview] = useState(false) // Показывать ли экран предпросмотра
const [showResults, setShowResults] = useState(false) // Показывать ли экран результатов const [showResults, setShowResults] = useState(false) // Показывать ли экран результатов
const [isClosing, setIsClosing] = useState(false) // Ожидание завершения сохранения при нажатии «Закончить»
const isFinishingRef = useRef(false) const isFinishingRef = useRef(false)
const wordStatsRef = useRef({}) const wordStatsRef = useRef({})
const processingRef = useRef(false) const processingRef = useRef(false)
const cardsShownRef = useRef(0) // Синхронный счётчик для избежания race condition const cardsShownRef = useRef(0) // Синхронный счётчик для избежания race condition
const saveProgressPromiseRef = useRef(null) // Промис сохранения прогресса (null = ещё не запущено)
const savePayloadRef = useRef(null) // Данные для сохранения при нажатии «Закончить» (если сохранение ещё не запускали)
// Функция равномерного распределения слов в пуле с гарантией максимального расстояния между одинаковыми словами // Функция равномерного распределения слов в пуле с гарантией максимального расстояния между одинаковыми словами
// excludeFirstWordId - ID слова, которое не должно быть первым в пуле (только что показанная карточка) // excludeFirstWordId - ID слова, которое не должно быть первым в пуле (только что показанная карточка)
@@ -221,6 +224,8 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
setShowResults(false) // Сбрасываем экран результатов при загрузке нового теста setShowResults(false) // Сбрасываем экран результатов при загрузке нового теста
isFinishingRef.current = false isFinishingRef.current = false
processingRef.current = false processingRef.current = false
saveProgressPromiseRef.current = null
savePayloadRef.current = null
setLoading(true) setLoading(true)
const loadWords = async () => { const loadWords = async () => {
@@ -307,21 +312,12 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
}) })
} }
// Завершение теста // Завершение теста: показываем результаты сразу, сохранение на бэкенде идёт в фоне (промис в ref для ожидания в handleFinish)
const finishTest = async () => { const finishTest = () => {
if (isFinishingRef.current) return if (isFinishingRef.current) return
isFinishingRef.current = true isFinishingRef.current = true
// Сразу показываем экран результатов, чтобы предотвратить показ новых карточек
setShowResults(true)
// Отправляем статистику на бэкенд
try {
// Получаем актуальные данные из состояния
const currentStats = wordStatsRef.current const currentStats = wordStatsRef.current
// Отправляем все слова, которые были в тесте, с их текущими значениями
// Бэкенд сам обновит только измененные поля
const updates = words.map(word => { const updates = words.map(word => {
const stats = currentStats[word.id] || { const stats = currentStats[word.id] || {
success: word.success || 0, success: word.success || 0,
@@ -339,7 +335,9 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
}) })
if (updates.length === 0) { if (updates.length === 0) {
console.log('No words to send - empty test') saveProgressPromiseRef.current = Promise.resolve()
savePayloadRef.current = null
setShowResults(true)
return return
} }
@@ -348,12 +346,10 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
requestBody.config_id = configId requestBody.config_id = configId
} }
console.log('Sending test progress to backend:', { savePayloadRef.current = { requestBody, configId, taskId }
wordsCount: updates.length,
configId: configId,
requestBody
})
const doSave = async () => {
try {
const response = await authFetch(`${API_URL}/test/progress`, { const response = await authFetch(`${API_URL}/test/progress`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -365,10 +361,8 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
throw new Error(`Server responded with status ${response.status}: ${errorText}`) throw new Error(`Server responded with status ${response.status}: ${errorText}`)
} }
const responseData = await response.json().catch(() => ({})) await response.json().catch(() => ({}))
console.log('Test progress saved successfully:', responseData)
// Если есть taskId, выполняем задачу
if (taskId) { if (taskId) {
try { try {
const completeResponse = await authFetch(`${API_URL}/tasks/${taskId}/complete`, { const completeResponse = await authFetch(`${API_URL}/tasks/${taskId}/complete`, {
@@ -377,9 +371,7 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
body: JSON.stringify({}), body: JSON.stringify({}),
}) })
if (completeResponse.ok) { if (!completeResponse.ok) {
console.log('Task completed successfully')
} else {
console.error('Failed to complete task:', await completeResponse.text()) console.error('Failed to complete task:', await completeResponse.text())
} }
} catch (taskErr) { } catch (taskErr) {
@@ -388,10 +380,14 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
} }
} catch (err) { } catch (err) {
console.error('Failed to save progress:', err) console.error('Failed to save progress:', err)
// Можно показать уведомление пользователю, но не блокируем показ результатов
} }
} }
const promise = doSave()
saveProgressPromiseRef.current = promise
setShowResults(true)
}
// Берём карточку из пула (getAndDelete) и показываем её // Берём карточку из пула (getAndDelete) и показываем её
const showNextCard = () => { const showNextCard = () => {
// Проверяем, не завершился ли тест // Проверяем, не завершился ли тест
@@ -566,8 +562,46 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
showNextCard() showNextCard()
} }
const handleFinish = () => { const runSaveFromPayload = useCallback(async (payload) => {
const { requestBody, taskId: payloadTaskId } = payload
const progressRes = await authFetch(`${API_URL}/test/progress`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody),
})
if (!progressRes.ok) {
const text = await progressRes.text()
throw new Error(`Progress save failed: ${progressRes.status} ${text}`)
}
if (payloadTaskId) {
const completeRes = await authFetch(`${API_URL}/tasks/${payloadTaskId}/complete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
})
if (!completeRes.ok) {
const text = await completeRes.text()
throw new Error(`Task complete failed: ${completeRes.status} ${text}`)
}
}
}, [authFetch])
const handleFinish = async () => {
if (isClosing) return
setIsClosing(true)
try {
let promise = saveProgressPromiseRef.current
if (promise == null && savePayloadRef.current) {
promise = runSaveFromPayload(savePayloadRef.current)
saveProgressPromiseRef.current = promise
}
await (promise ?? Promise.resolve())
onNavigate?.('tasks') onNavigate?.('tasks')
} catch (err) {
console.error('Failed to save before close:', err)
} finally {
setIsClosing(false)
}
} }
const getRandomSide = (word) => { const getRandomSide = (word) => {
@@ -630,8 +664,12 @@ function TestWords({ onNavigate, wordCount: initialWordCount, configId: initialC
})} })}
</div> </div>
<div className="results-actions"> <div className="results-actions">
<button className="test-finish-button" onClick={handleFinish}> <button
Закончить className="test-finish-button"
onClick={handleFinish}
disabled={isClosing}
>
{isClosing ? 'Завершение…' : 'Закончить'}
</button> </button>
</div> </div>
</div> </div>