Fix upload auto-refresh, health data refresh, and HR zone recalculation
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 6s
Build and push images / build-worker (push) Successful in 5s
Build and push images / build-frontend (push) Successful in 10s

- 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:
2026-06-06 23:13:44 +01:00
parent b5fd17a597
commit 95f704cb54
6 changed files with 205 additions and 11 deletions
+43 -4
View File
@@ -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>