Add trend-range gating, vehicle filter, sync cancel, moving time, and UI fixes
- Grey out trend ranges beyond available health history - Reject implausibly fast (vehicle) activities on upload with feedback - Add cancel button + cooperative cancellation for Garmin sync - Show daily steps prominently on the dashboard - Clear errors for malformed/empty upload ZIPs - Snap-target dot when drawing a segment on the map - Time-axis fallback for stationary/HIIT HR timelines; hide map when no GPS - Parse and display moving time (timer) vs elapsed; backfill task - Restyle SegmentsPanel like RouteLeaderboard; Laps/Routes/Segments on one row Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,19 @@ from app.models.user import User, GarminConnectConfig
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _redis_client():
|
||||
import redis as redis_lib
|
||||
return redis_lib.Redis.from_url(settings.redis_url)
|
||||
|
||||
|
||||
def sync_task_key(user_id: int) -> str:
|
||||
return f"garmin_sync_task:{user_id}"
|
||||
|
||||
|
||||
def sync_cancel_key(user_id: int) -> str:
|
||||
return f"garmin_sync_cancel:{user_id}"
|
||||
|
||||
|
||||
class GarminConfigIn(BaseModel):
|
||||
email: str
|
||||
password: Optional[str] = None # plaintext; encrypted before storage. None = keep existing.
|
||||
@@ -183,4 +196,47 @@ async def trigger_sync(
|
||||
|
||||
from app.workers.tasks import sync_garmin_connect_user
|
||||
task = sync_garmin_connect_user.delay(current_user.id)
|
||||
|
||||
# Track the active task id and clear any stale cancel flag so the new sync runs.
|
||||
try:
|
||||
r = _redis_client()
|
||||
r.delete(sync_cancel_key(current_user.id))
|
||||
r.set(sync_task_key(current_user.id), task.id, ex=3600)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {"task_id": task.id, "status": "queued"}
|
||||
|
||||
|
||||
@router.post("/cancel")
|
||||
async def cancel_sync(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Request cancellation of the user's in-progress Garmin sync. The running task
|
||||
checks this flag between items and aborts cooperatively."""
|
||||
from app.workers.tasks import celery_app
|
||||
|
||||
try:
|
||||
r = _redis_client()
|
||||
r.set(sync_cancel_key(current_user.id), "1", ex=3600)
|
||||
task_id = r.get(sync_task_key(current_user.id))
|
||||
if task_id:
|
||||
tid = task_id.decode() if isinstance(task_id, (bytes, bytearray)) else task_id
|
||||
# terminate=False: don't kill a running worker mid-transaction; the
|
||||
# cooperative flag handles an already-running task, and this revoke
|
||||
# prevents a still-queued one from starting.
|
||||
celery_app.control.revoke(tid, terminate=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Reflect intent immediately so the UI updates before the worker writes "Cancelled".
|
||||
result = await db.execute(
|
||||
select(GarminConnectConfig).where(GarminConnectConfig.user_id == current_user.id)
|
||||
)
|
||||
cfg = result.scalar_one_or_none()
|
||||
if cfg:
|
||||
cfg.last_sync_status = "Cancelling…"
|
||||
await db.commit()
|
||||
|
||||
return {"status": "cancelling"}
|
||||
|
||||
Reference in New Issue
Block a user