feat(komoot): improve import flow with throttling, cancellation, and UI guidance

Signed-off-by: Marcus Fihlon <marcus@fihlon.swiss>
This commit is contained in:
Marcus Fihlon 2026-04-28 15:24:15 +02:00
parent 0387ca01e3
commit f7f919f0b1
Signed by: McPringle
GPG key ID: C6B7F469EE363E1F
4 changed files with 124 additions and 9 deletions

View file

@ -19,15 +19,20 @@
<div class="alert alert-secondary">
<div class="fw-semibold mb-1">Important</div>
<div class="small mb-2">
<div class="small mb-1">
Your Komoot credentials are only used for this request and are not stored in FitPub.
</div>
<div class="small mb-0">
Komoot does not provide a public API for this flow. This import currently depends on the
same web API endpoints used by the Komoot website and may stop working if Komoot changes them.
The import currently runs in this browser tab. If you leave or reload the page, remaining activities will not continue importing automatically.
</div>
</div>
<ul class="small text-muted ps-3 mb-4">
<li>Import starts with the oldest new activities, so progress begins at the bottom of the list.</li>
<li>FitPub adds short delays between Komoot requests during loading and import to reduce rate limiting.</li>
<li>This integration depends on Komoot web endpoints and may stop working if Komoot changes them.</li>
</ul>
<div id="errorAlert" class="alert alert-danger d-none" role="alert"></div>
<div class="card shadow-sm">
@ -81,6 +86,9 @@
Importing...
</span>
</button>
<button type="button" class="btn btn-outline-danger d-none" id="cancelImportBtn">
<i class="bi bi-stop-circle"></i> Stop After Current Activity
</button>
</div>
</form>
</div>
@ -130,10 +138,13 @@
const importFirstBtn = document.getElementById('importFirstBtn');
const importFirstText = document.getElementById('importFirstText');
const importFirstSpinner = document.getElementById('importFirstSpinner');
const cancelImportBtn = document.getElementById('cancelImportBtn');
let currentActivities = [];
let importCancellationRequested = false;
let importInProgress = false;
function updateImportButtonState() {
importFirstBtn.disabled = currentActivities.length === 0;
importFirstBtn.disabled = importInProgress || currentActivities.length === 0;
}
function setLoading(loading) {
@ -143,9 +154,13 @@
}
function setImportLoading(loading) {
importFirstBtn.disabled = loading;
importInProgress = loading;
loadActivitiesBtn.disabled = loading;
updateImportButtonState();
importFirstText.classList.toggle('d-none', loading);
importFirstSpinner.classList.toggle('d-none', !loading);
cancelImportBtn.classList.toggle('d-none', !loading);
cancelImportBtn.disabled = !loading;
}
function showError(message) {
@ -222,6 +237,10 @@
}
function renderImportStatus(activity) {
if (activity.uiImportStatus === 'queued') {
return '<i class="bi bi-hourglass-split text-warning" title="Queued for import" aria-label="Queued for import"></i>';
}
if (activity.uiImportStatus === 'importing') {
return '<span class="spinner-border spinner-border-sm text-primary" role="status" aria-label="Importing"></span>';
}
@ -289,6 +308,15 @@
return Boolean(payload.startDate) !== Boolean(payload.endDate);
}
function resetQueuedActivities() {
for (const activity of currentActivities) {
if (activity.uiImportStatus === 'queued') {
activity.uiImportStatus = null;
activity.uiImportError = null;
}
}
}
form.addEventListener('submit', async function(event) {
event.preventDefault();
clearError();
@ -356,6 +384,7 @@
showError('Load Komoot activities before starting the import.');
return;
}
importCancellationRequested = false;
setImportLoading(true);
try {
@ -368,8 +397,15 @@
return;
}
for (const activity of activitiesToImport) {
activity.uiImportStatus = 'queued';
activity.uiImportError = null;
}
renderActivities(currentActivities);
let importedCount = 0;
let failedCount = 0;
let cancelled = false;
for (const activity of activitiesToImport) {
activity.uiImportStatus = 'importing';
@ -407,9 +443,20 @@
}
renderActivities(currentActivities);
if (importCancellationRequested) {
cancelled = true;
resetQueuedActivities();
renderActivities(currentActivities);
break;
}
}
showStatus(`Imported ${importedCount} Komoot activit${importedCount === 1 ? 'y' : 'ies'}${failedCount > 0 ? `, ${failedCount} failed` : ''}.`);
if (cancelled) {
showStatus(`Import stopped after the current activity. Imported ${importedCount} Komoot activit${importedCount === 1 ? 'y' : 'ies'}${failedCount > 0 ? `, ${failedCount} failed` : ''}.`);
} else {
showStatus(`Imported ${importedCount} Komoot activit${importedCount === 1 ? 'y' : 'ies'}${failedCount > 0 ? `, ${failedCount} failed` : ''}.`);
}
} catch (error) {
let message = error instanceof Error ? error.message : 'Failed to import Komoot activities.';
@ -420,9 +467,16 @@
showError(message);
} finally {
setImportLoading(false);
importCancellationRequested = false;
}
});
cancelImportBtn.addEventListener('click', function() {
importCancellationRequested = true;
cancelImportBtn.disabled = true;
showStatus('Import will stop after the current activity finishes.');
});
updateImportButtonState();
});
</script>