Skip to content

Instantly share code, notes, and snippets.

@ndeloof
Created May 20, 2026 14:39
Show Gist options
  • Select an option

  • Save ndeloof/bb5a331a0ae58f627bd998904427603d to your computer and use it in GitHub Desktop.

Select an option

Save ndeloof/bb5a331a0ae58f627bd998904427603d to your computer and use it in GitHub Desktop.
Garmin workout cleanup
(async () => {
const DRY_RUN = true;
const PAGE_WAIT = 800;
const DELETE_DELAY = 300;
const MAX_PAGES = 50;
const sleep = ms => new Promise(r => setTimeout(r, ms));
const csrf = document.querySelector('meta[name="csrf-token"]')?.content;
if (!csrf) throw new Error('CSRF token introuvable dans <meta name="csrf-token">');
function readWorkoutsFromTbody(tbody) {
const k = Object.keys(tbody).find(x => x.startsWith('__reactFiber'));
if (!k) return null;
let fiber = tbody[k];
for (let i = 0; i < 30 && fiber; i++) {
const p = fiber.memoizedProps;
if (p && Array.isArray(p.workouts) && p.workouts.length && p.workouts[0].workoutId) {
return p.workouts;
}
fiber = fiber.return;
}
return null;
}
function findWorkoutsTbody() {
for (const tb of document.querySelectorAll('tbody')) {
if (readWorkoutsFromTbody(tb)) return tb;
}
return null;
}
function readPageState() {
const wrap = document.querySelector('.Pagination_wrapper__mYZ9R');
if (!wrap) return { current: 1, total: 1 };
const m = wrap.innerText.match(/(\d+)\s*\D+\s*(\d+)/);
return m ? { current: +m[1], total: +m[2] } : { current: 1, total: 1 };
}
function clickNext() {
const svg = document.querySelector('.Pagination_wrapper__mYZ9R svg[aria-label="Suivant"]');
const btn = svg && svg.closest('button');
if (!btn || btn.disabled) return false;
btn.click();
return true;
}
async function collectAll() {
const seen = new Map();
let pages = 0;
while (pages < MAX_PAGES) {
const tbody = findWorkoutsTbody();
if (!tbody) throw new Error('Tbody « Mes entraînements » introuvable');
const workouts = readWorkoutsFromTbody(tbody) || [];
for (const w of workouts) {
if (!seen.has(w.workoutId)) seen.set(w.workoutId, w.workoutName);
}
const { current, total } = readPageState();
console.log(`page ${current}/${total} — ${workouts.length} workouts (cumulé : ${seen.size})`);
if (current >= total) break;
if (!clickNext()) break;
const before = current;
let waited = 0;
while (waited < 5000 && readPageState().current === before) {
await sleep(150);
waited += 150;
}
await sleep(PAGE_WAIT);
pages++;
}
return [...seen.entries()].map(([id, name]) => ({ id, name }));
}
console.log('Collecte…');
const all = await collectAll();
console.log(`Total : ${all.length} workout(s)`);
console.table(all);
if (DRY_RUN) {
console.warn('DRY_RUN actif — passe-le à false pour supprimer.');
return all;
}
const results = [];
for (const { id, name } of all) {
const res = await fetch(`https://connect.garmin.com/gc-api/workout-service/workout/${id}`, {
method: 'DELETE',
credentials: 'include',
headers: {
'accept': '*/*',
'connect-csrf-token': csrf,
},
});
results.push({ id, name, ok: res.ok, status: res.status });
console.log(res.ok ? `✓ ${id} — ${name}` : `✗ ${id} — ${name} (${res.status})`);
await sleep(DELETE_DELAY);
}
const ok = results.filter(r => r.ok).length;
console.log(`Terminé. ok=${ok} ko=${results.length - ok}`);
return results;
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment