Fix upload auto-refresh, health data refresh, and HR zone recalculation
- UploadPage now polls task status every 2s and invalidates activity, health-summary, and health-metrics queries on completion so new activities and health data appear without a hard refresh - Garmin and Strava export endpoints now return a task_id for polling - Updating max HR in Profile triggers a background Celery task to recalculate hr_zones for all existing activities; profile page shows a confirmation note when this is queued - Add CLAUDE.md with repo architecture and dev commands Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,38 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import { useState, useCallback, useEffect, useRef } from 'react'
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import api from '../utils/api'
|
||||
|
||||
function UploadZone({ title, description, accept, endpoint, icon }) {
|
||||
const [tasks, setTasks] = useState([])
|
||||
const queryClient = useQueryClient()
|
||||
const intervalsRef = useRef({})
|
||||
|
||||
const pollTask = useCallback((taskId) => {
|
||||
if (intervalsRef.current[taskId]) return
|
||||
const intervalId = setInterval(async () => {
|
||||
try {
|
||||
const { data } = await api.get(`/upload/task/${taskId}`)
|
||||
if (data.status === 'SUCCESS' || data.status === 'FAILURE') {
|
||||
clearInterval(intervalsRef.current[taskId])
|
||||
delete intervalsRef.current[taskId]
|
||||
setTasks(ts => ts.map(t =>
|
||||
t.task_id === taskId ? { ...t, status: data.status === 'SUCCESS' ? 'done' : 'failed' } : t
|
||||
))
|
||||
if (data.status === 'SUCCESS') {
|
||||
queryClient.invalidateQueries({ queryKey: ['activities'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['health-summary'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['health-metrics'] })
|
||||
}
|
||||
}
|
||||
} catch { /* ignore transient poll errors */ }
|
||||
}, 2000)
|
||||
intervalsRef.current[taskId] = intervalId
|
||||
}, [queryClient])
|
||||
|
||||
useEffect(() => {
|
||||
return () => { Object.values(intervalsRef.current).forEach(clearInterval) }
|
||||
}, [])
|
||||
|
||||
const upload = useMutation({
|
||||
mutationFn: async (file) => {
|
||||
@@ -16,7 +44,11 @@ function UploadZone({ title, description, accept, endpoint, icon }) {
|
||||
return { file: file.name, ...data }
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
setTasks(t => [...t, { ...data, status: 'queued' }])
|
||||
const task = { ...data, status: data.task_id ? 'processing' : 'queued' }
|
||||
setTasks(t => [...t, task])
|
||||
if (data.task_id) {
|
||||
pollTask(data.task_id)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -30,6 +62,13 @@ function UploadZone({ title, description, accept, endpoint, icon }) {
|
||||
multiple: true,
|
||||
})
|
||||
|
||||
function StatusBadge({ status }) {
|
||||
if (status === 'processing') return <span className="ml-2 text-blue-400 animate-pulse">⏳ Processing</span>
|
||||
if (status === 'done') return <span className="ml-2 text-green-400">✓ Done</span>
|
||||
if (status === 'failed') return <span className="ml-2 text-red-400">✗ Failed</span>
|
||||
return <span className="ml-2 text-green-400">✓ Queued</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-gray-900 rounded-xl border border-gray-800 p-5">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
@@ -73,7 +112,7 @@ function UploadZone({ title, description, accept, endpoint, icon }) {
|
||||
{task.activity_tasks !== undefined && (
|
||||
<span className="text-gray-500 ml-2">{task.activity_tasks} activities queued</span>
|
||||
)}
|
||||
<span className="ml-2 text-green-400">✓ Queued</span>
|
||||
<StatusBadge status={task.status} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user