From 4475d156c8dcd0019e4893951cb20e229e2cfcb3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 09:35:01 +0000 Subject: [PATCH] sync: import activities via AJAX with a per-row progress bar 'Alle sichtbaren importieren' used to POST a plain form, which synchronously imported every visible activity server-side and only then redirected via a meta refresh -- the browser just sat on a blank reload for however long the whole batch took, with no per-activity feedback. Each activity row now carries data-athlete-id/data-activity-id plus a progress bar and status line. Both the per-row 'Analysieren' button and the 'Alle sichtbaren importieren' button now drive a small JS loop that POSTs to the existing /sync/{athlete}/activities/{activity}/import endpoint per activity with Accept: application/json, animates that row's progress bar while the request is in flight, and shows the resulting crossing count (or an error) inline -- without a full page reload. The endpoint itself now branches on the Accept header: JSON for the AJAX path, the previous meta-refresh HTML for plain form submits (so it still works without JavaScript). process_activity and process_activity_with_client now return the crossing count so it can be reported back to the row. --- src/main.rs | 220 ++++++++++++++++++++++++++++++++++++++++++++----- src/webhook.rs | 23 ++++-- 2 files changed, 216 insertions(+), 27 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3ff3c8c..639de19 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,9 +11,10 @@ mod webhook; use anyhow::{Context, Result, anyhow}; use axum::{ - Router, + Json, Router, extract::{Path, Query, State}, - response::Html, + http::{HeaderMap, header::ACCEPT}, + response::{Html, IntoResponse}, routing::{get, post}, }; use chrono::{DateTime, Duration, Utc}; @@ -381,6 +382,44 @@ input[type="datetime-local"] { background: #111827; color: #f9fafb; } +.row-progress { + display: none; + height: 6px; + width: 220px; + max-width: 100%; + margin-top: .5rem; + border-radius: 4px; + background: #374151; + overflow: hidden; +} +.row-progress.active { + display: block; +} +.row-progress-bar { + height: 100%; + width: 30%; + border-radius: 4px; + background: #2563eb; + animation: row-progress-indeterminate 1.1s ease-in-out infinite; +} +@keyframes row-progress-indeterminate { + 0% { margin-left: -30%; } + 100% { margin-left: 100%; } +} +.row-status { + margin-top: .3rem; + min-height: 1.1em; + font-size: .9rem; +} +.row-status.ok { + color: #4ade80; +} +.row-status.error { + color: #f87171; +} +.activity-row.done { + opacity: .6; +} @@ -451,7 +490,8 @@ input[type="datetime-local"] { if !visible.is_empty() { html.push_str(&format!( r#"
-
"#); + html.push_str(&format!( + r#"
"#, + html::escape(&athlete_id), + html::escape(&activity.id), + )); html.push_str(&format!( r#"
@@ -494,6 +538,8 @@ input[type="datetime-local"] {
{} · {} · {}
+
+
"#, html::escape(&activity.name), html::escape(&activity.activity_type), @@ -502,7 +548,8 @@ input[type="datetime-local"] { )); html.push_str(&format!( - r#"