### Install and Run Web Dashboard Source: https://github.com/drkostas/hevy2garmin/blob/main/README.md Install the package and start the local web dashboard. Access it at localhost:8123 for setup and syncing. Ensure Python is installed. ```bash pip install hevy2garmin hevy2garmin serve ``` -------------------------------- ### Install from Source Source: https://github.com/drkostas/hevy2garmin/blob/main/README.md If not yet on PyPI, install directly from the source repository. This requires Git to clone the repository first. ```bash git clone https://github.com/drkostas/hevy2garmin.git && cd hevy2garmin && pip install . ``` -------------------------------- ### Clone Repository and Set Up Development Environment Source: https://github.com/drkostas/hevy2garmin/blob/main/README.md Clone the hevy2garmin repository, create and activate a virtual environment, and install development dependencies. This is the initial setup for contributing to the project. ```bash git clone https://github.com/drkostas/hevy2garmin.git cd hevy2garmin python -m venv .venv && source .venv/bin/activate pip install -e ".[dev]" pytest tests/ -v ``` -------------------------------- ### Clone and Set Up Development Environment Source: https://github.com/drkostas/hevy2garmin/blob/main/CONTRIBUTING.md Clone the repository, navigate into the directory, create and activate a virtual environment, and install development dependencies. ```bash git clone https://github.com/drkostas/hevy2garmin.git cd hevy2garmin python -m venv .venv && source .venv/bin/activate pip install -e ".[dev,cloud]" ``` -------------------------------- ### Install hevy2garmin locally with Git clone Source: https://github.com/drkostas/hevy2garmin/blob/main/README.md Instructions for cloning the repository and installing the hevy2garmin package in editable mode locally. ```bash cd hevy2garmin git pull origin main pip install -e . ``` -------------------------------- ### CLI Initialization and Sync Source: https://github.com/drkostas/hevy2garmin/blob/main/README.md Install the CLI tool and initialize it with Hevy API key and Garmin credentials. Use 'sync' to transfer workouts. ```bash pip install hevy2garmin # Interactive setup (Hevy API key + Garmin credentials) hevy2garmin init # Sync your 10 most recent workouts hevy2garmin sync # List recent workouts (checkmark = already synced) hevy2garmin list # Check sync status hevy2garmin status # Dry run (generate FIT files without uploading) hevy2garmin sync --dry-run # Sync last 5 workouts only hevy2garmin sync -n 5 ``` -------------------------------- ### Install with Cloud Support Source: https://github.com/drkostas/hevy2garmin/blob/main/README.md Install hevy2garmin with additional dependencies for cloud deployments, including Postgres support. This enables automatic backend detection via the DATABASE_URL environment variable. ```bash pip install hevy2garmin[cloud] ``` -------------------------------- ### Install/Upgrade hevy2garmin with pip Source: https://github.com/drkostas/hevy2garmin/blob/main/README.md Use this command to install or upgrade the hevy2garmin package using pip. ```bash pip install --upgrade hevy2garmin ``` -------------------------------- ### Example hevy2garmin activity description Source: https://github.com/drkostas/hevy2garmin/blob/main/README.md This is an example of the text description that hevy2garmin adds to Garmin activities, summarizing the workout session. The format varies slightly for cardio exercises. ```text ποΈ Push Day β±οΈ 52 min π₯ 387 kcal β€οΈ avg 118 bpm β’ Bench Press (Barbell): 3 sets Β· 80.0kg Γ 8 β’ Incline Dumbbell Press: 3 sets Β· 28.0kg Γ 10 β’ Cable Fly: 3 sets Β· 15.0kg Γ 12 β synced by hevy2garmin ``` -------------------------------- ### Test Postgres Backend Locally Source: https://github.com/drkostas/hevy2garmin/blob/main/README.md Install additional dependencies for cloud features and set the DATABASE_URL environment variable to test the PostgreSQL backend. This command runs all tests with the specified database configuration. ```bash pip install -e ".[dev,cloud]" DATABASE_URL=postgresql://user:pass@localhost:5432/hevy2garmin pytest tests/ -v ``` -------------------------------- ### Run Local Dashboard Server Source: https://github.com/drkostas/hevy2garmin/blob/main/CONTRIBUTING.md Start the hevy2garmin dashboard locally on a specified port. The development server will be accessible at http://localhost:8123. ```bash PYTHONPATH=src python -m hevy2garmin.cli serve --port 8123 ``` -------------------------------- ### systemd Service File for Linux Source: https://github.com/drkostas/hevy2garmin/blob/main/README.md Configuration for a systemd service to manage the hevy2garmin dashboard. Set environment variables for API keys and credentials. Enable and start the service using systemctl. ```ini [Unit] Description=hevy2garmin dashboard After=network.target [Service] ExecStart=hevy2garmin serve Restart=always User=your-username Environment=HEVY_API_KEY=your-key Environment=GARMIN_EMAIL=your-email [Install] WantedBy=multi-user.target ``` -------------------------------- ### Add Custom Exercise Mapping Source: https://github.com/drkostas/hevy2garmin/blob/main/README.md Add custom exercise mappings to Garmin categories via the command line. Ensure you have the 'hevy2garmin' tool installed and configured. ```bash hevy2garmin map "My Custom Exercise" --category 28 --subcategory 0 ``` -------------------------------- ### JavaScript Sync Logic for Hevy2Garmin Source: https://github.com/drkostas/hevy2garmin/blob/main/src/hevy2garmin/templates/dashboard.html Handles the synchronization process, including starting, stopping, and displaying progress. It makes API calls to sync workouts one by one and updates the UI accordingly. Use this for manual sync initiation. ```javascript let _syncStopped = false; function stopSync() { _syncStopped = true; document.getElementById('sync-stop-btn').style.display = 'none'; } async function syncNow() { _syncStopped = false; const btn = document.getElementById('sync-now-btn'); const stopBtn = document.getElementById('sync-stop-btn'); const progress = document.getElementById('sync-progress'); const text = document.getElementById('sync-progress-text'); const bar = document.getElementById('sync-progress-bar'); const result = document.getElementById('sync-result'); btn.disabled = true; btn.style.opacity = '0.5'; stopBtn.style.display = ''; progress.style.display = 'block'; result.innerHTML = ''; text.textContent = 'Starting sync...'; bar.style.width = '0%'; let total = 0; let synced = 0; let errors = 0; while (!_syncStopped) { try { const resp = await fetch('/api/sync-one', { method: 'POST' }); const data = await resp.json(); if (data.busy) { text.textContent = 'Another sync is already running. Please wait.'; break; } if (data.error && !data.title) { errors++; // Show user-friendly error messages const err = data.error; if (data.eu_consent) { text.textContent = 'Garmin requires upload consent.'; result.innerHTML = '
No HR data available' + (err.error ? ': ' + err.error : '') + '
'; return; } const data = await resp.json(); if (!data.hr_samples || data.hr_samples.length === 0) { container.innerHTML = 'No HR samples found for this workout.
'; return; } // Build summary line let summary = ''; if (data.avg_hr) summary += 'avg ' + data.avg_hr + ''; if (data.max_hr) summary += ' Β· max ' + data.max_hr + ''; if (data.calories) summary += ' Β· ' + data.calories + ' kcal'; summary += ' bpm'; // Exercise legend let legend = 'Failed to load HR data: ' + e.message + '
'; } } ``` -------------------------------- ### Configure Additional Mergeable Activity Types in config.json Source: https://github.com/drkostas/hevy2garmin/blob/main/README.md Define an array of Garmin activity types in your `config.json` that should be eligible for merging with Hevy workouts. This includes 'strength_training' by default, plus any additional types you specify. ```json "merge_activity_types": ["strength_training", "bouldering", "indoor_climbing"] ``` -------------------------------- ### Configure Additional Mergeable Activity Types Source: https://github.com/drkostas/hevy2garmin/blob/main/README.md Specify additional Garmin activity types to be merged with Hevy workouts by listing their internal names, comma-separated. This can be done in the settings UI or directly in the `config.json` file. ```text bouldering, indoor_climbing ``` -------------------------------- ### Python API Exercise Mapper Source: https://github.com/drkostas/hevy2garmin/blob/main/README.md Utilize the exercise mapper from the Python API to translate Hevy exercise names into Garmin-compatible categories. ```python # Just the exercise mapper from hevy2garmin.mapper import lookup_exercise cat, subcat, name = lookup_exercise("Bench Press (Barbell)") # (0, 1, "Bench Press (Barbell)") ``` -------------------------------- ### Re-sync Workout with Force Source: https://github.com/drkostas/hevy2garmin/blob/main/src/hevy2garmin/templates/workouts.html Re-syncs a workout with a force parameter to bypass duplicate checks. Reloads the page on success or shows an alert on failure. Handles network errors. ```javascript fetch('/api/unsync/' + hevyId, { method: 'POST', body: form }); const unsyncData = await unsyncResp.json(); if (!unsyncData.ok) { alert('Unsync failed: ' + (unsyncData.error || 'Unknown')); btn.disabled = false; btn.textContent = 'Re-sync'; return; } // Step 2: re-sync with force=1 (bypass dedup check) const syncResp = await fetch('/api/sync/' + hevyId + '?force=1', { method: 'POST' }); if (syncResp.ok) { window.location.reload(); } else { alert('Re-sync upload failed. The workout is now pending β try syncing from the dashboard.'); window.location.reload(); } } catch (e) { alert('Network error: ' + e.message); btn.disabled = false; btn.textContent = 'Re-sync'; } ``` -------------------------------- ### Show Unmapped Sub-IDs Source: https://github.com/drkostas/hevy2garmin/blob/main/src/hevy2garmin/templates/mappings.html Displays unmapped sub-IDs for a given category selection in a specific context (identified by 'idx'). It renders chips for these sub-IDs and shows the relevant helper element. ```javascript function showUnmappedSubs(select, idx) { const catId = select.value; const helper = document.getElementById('unmapped-sub-helper-' + idx); const chips = helper.querySelector('.sub-id-chips'); const entries = subIdIndex[catId] || []; if (entries.length === 0) { helper.style.display = 'none'; return; } chips.innerHTML = _renderSubChips(entries, 'unmapped-sub-' + idx); helper.style.display = 'block'; } ``` -------------------------------- ### Build Sub-ID Index from Mapping Table Source: https://github.com/drkostas/hevy2garmin/blob/main/src/hevy2garmin/templates/mappings.html This script builds an index of sub-IDs categorized by their respective categories from an HTML table. It's used to quickly look up sub-IDs associated with a category. ```javascript const subIdIndex = {}; document.querySelectorAll('#mappings-table tr.mapping-row').forEach(row => { const cells = row.querySelectorAll('td'); if (cells.length >= 4) { const name = cells[0].textContent.trim(); const cat = cells[2].textContent.trim(); const sub = cells[3].textContent.trim(); if (!subIdIndex[cat]) subIdIndex[cat] = []; subIdIndex[cat].push({ name, sub }); } }); ``` -------------------------------- ### Show Sub-IDs for a Category Source: https://github.com/drkostas/hevy2garmin/blob/main/src/hevy2garmin/templates/mappings.html Displays a list of sub-ID chips for a selected category. If no sub-IDs are found for the category, the helper element is hidden. ```javascript function showSubIds(catId) { const helper = document.getElementById('sub-id-helper'); const list = document.getElementById('sub-id-list'); const entries = subIdIndex[catId] || []; if (entries.length === 0) { helper.style.display = 'none'; return; } list.innerHTML = _renderSubChips(entries, 'map-subid'); helper.style.display = 'block'; } ``` -------------------------------- ### Re-synchronize Workout with Garmin Source: https://github.com/drkostas/hevy2garmin/blob/main/src/hevy2garmin/templates/workouts.html This asynchronous JavaScript function initiates a re-synchronization process for a workout. It first prompts the user for confirmation and then proceeds to unsync and delete the old Garmin activity before preparing to upload fresh data from Hevy. The button used to trigger this action is disabled and its text is updated during the process. ```javascript async function resyncWorkout(hevyId, btn) { if (!confirm('Re-sync this workout? The old Garmin activity will be deleted and a fresh one uploaded with the latest Hevy data.')) return; btn.disabled = true; btn.textContent = 'Re-syncingβ¦'; try { // Step 1: unsync + delete old Garmin activity const form = new FormData(); form.append('delete_garmin', 'true'); const unsyncResp = await ``` -------------------------------- ### Garmin Login Response Handler Source: https://github.com/drkostas/hevy2garmin/blob/main/src/hevy2garmin/templates/setup.html Processes the response from Garmin's login and MFA attempts. It handles success, MFA requests, and various error conditions, including rate limiting and CAPTCHA requirements. ```javascript async function handleGarminLoginResponse(data) { const result = document.getElementById('garmin_connect_result'); const mfaRow = document.getElementById('garmin_mfa_row'); if (data.status === 'success') { // Hide MFA row if it was shown mfaRow.style.display = 'none'; garminMfaSessionId = null; // Persist tokens on our server const storeResp = await fetch('/api/garmin-ticket', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tokens: { di_token: data.di_token, di_refresh_token: data.di_refresh_token, di_client_id: data.di_client_id, }}), }); const storeData = await storeResp.json(); if (storeData.ok) { result.innerHTML = '✓ Connected to Garmin'; } else { result.innerHTML = '✗ ' + (storeData.error || 'Failed to save tokens') + ''; } return; } if (data.status === 'needs_mfa') { garminMfaSessionId = data.session_id; mfaRow.style.display = ''; const method = data.mfa_method || 'email'; result.innerHTML = 'Garmin sent a verification code to your ' + method + '. Enter it below.'; document.getElementById('garmin_mfa_code').focus(); return; } if (data.status === 'mfa_failed') { result.innerHTML = '✗ ' + (data.message || 'Code rejected, try again') + ''; document.getElementById('garmin_mfa_code').select(); return; } if (data.status === 'invalid_credentials') { result.innerHTML = '✗ Invalid email or password'; return; } if (data.status === 'rate_limited') { result.innerHTML = '✗ Garmin rate-limited this account. Wait a few minutes and try again.'; return; } if (data.status === 'needs_captcha') { // Worker returns needs_captcha for both actual captcha challenges AND // Garmin's 427 response which appears when MFA-enabled accounts try the // direct portal API flow (Garmin forces those through the embed widget // browser flow instead). Either way, the fix is the same: show the // manual sign-in UI so the user can complete setup via their browser. const detail = data.message || 'Garmin requires the browser sign-in flow for this account.'; result.innerHTML = '⚠ ' + detail + ' Use the manual sign-in below:'; revealManualFallback(); return; } // Unknown / error status β reveal the manual fallback so the user has a path forward. const msg = (data && (data.message || data.error)) || 'Unexpected error'; result.innerHTML = '✗ ' + msg + '. ' } ``` -------------------------------- ### Render Sub-ID Chips Source: https://github.com/drkostas/hevy2garmin/blob/main/src/hevy2garmin/templates/mappings.html Generates clickable 'chips' for sub-IDs within a given category. Each chip displays the sub-ID and name, and clicking it sets the value of a specified input field to the sub-ID. ```javascript function _renderSubChips(entries, targetInput) { const seen = new Set(); return entries.filter(e => { if (seen.has(e.sub)) return false; seen.add(e.sub); return true; }) .sort((a, b) => parseInt(a.sub) - parseInt(b.sub)) .map(e => '' + '' + e.sub + ' ' + e.name + '') .join(''); } ``` -------------------------------- ### Save Hevy Tokens Source: https://github.com/drkostas/hevy2garmin/blob/main/src/hevy2garmin/templates/setup.html Saves Hevy API tokens to the server. Redirects to the homepage on success or displays an error message. Includes a timeout for redirection. ```javascript async function saveHevyTokens() { const hevyApiKey = document.getElementById('hevy_api_key').value.trim(); const garminUsername = document.getElementById('garmin_username').value.trim(); const garminPassword = document.getElementById('garmin_password').value.trim(); const result = document.getElementById('hevy-save-result'); result.innerHTML = 'Saving...'; try { const resp = await fetch('/api/hevy/tokens', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ hevy_api_key: hevyApiKey, garmin_username: garminUsername, garmin_password: garminPassword }) }); const storeData = await resp.json(); if (resp.ok) { result.innerHTML = '✓ Tokens saved successfully!'; // This is a one-time requirement for some accounts.'; setTimeout(function() { window.location.href = '/'; }, 5000); } else { result.innerHTML = '✗ ' + (storeData.error || 'Failed to save tokens') + ''; } } catch (e) { result.innerHTML = '✗ Network error β try again'; } } ``` -------------------------------- ### Enable Activity Merging in Hevy2Garmin Source: https://github.com/drkostas/hevy2garmin/blob/main/README.md Set `merge_mode` to `true` in your configuration to enable Hevy2Garmin to merge workout data into matching Garmin watch activities. This provides benefits like 1-second HR sampling and accurate training effect calculations. ```json { "merge_mode": true } ``` -------------------------------- ### Edit Mapping Functionality Source: https://github.com/drkostas/hevy2garmin/blob/main/src/hevy2garmin/templates/mappings.html Populates the mapping form with existing data for editing. It opens the details card, sets input values, and triggers the display of relevant sub-IDs for the selected category. ```javascript function editMapping(name, cat, subcat) { const details = document.querySelector('details.card'); if (details) details.open = true; document.getElementById('map-hevy-name').value = name; document.getElementById('map-category').value = cat; document.getElementById('map-subid').value = subcat; showSubIds(String(cat)); document.getElementById('mapping-form').scrollIntoView({ behavior: 'smooth', block: 'center' }); } ``` -------------------------------- ### Unsync Individual Workout Source: https://github.com/drkostas/hevy2garmin/blob/main/src/hevy2garmin/templates/workouts.html Unsyncs a specific workout after user confirmation. Disables the button during the operation and provides feedback on success or failure. Catches network errors. ```javascript async function unsyncWorkout(hevyId, btn) { if (!confirm('Remove this sync record? The workout will re-appear as "pending" and can be synced again.\n\nThe Garmin activity won\'t be deleted β manage that from Garmin Connect if needed.')) return; btn.disabled = true; btn.textContent = 'Removingβ¦'; try { const form = new FormData(); const resp = await fetch('/api/unsync/' + hevyId, { method: 'POST', body: form }); const data = await resp.json(); if (data.ok) { window.location.reload(); } else { alert('Failed: ' + (data.error || 'Unknown error')); btn.disabled = false; btn.textContent = 'Unsync'; } } catch (e) { alert('Network error: ' + e.message); btn.disabled = false; btn.textContent = 'Unsync'; } } ``` -------------------------------- ### Garmin MFA Submission Source: https://github.com/drkostas/hevy2garmin/blob/main/src/hevy2garmin/templates/setup.html Submits the Multi-Factor Authentication (MFA) code to Garmin. It validates the input and updates the UI to show the verification status. ```javascript async function garminSubmitMfa() { const code = document.getElementById('garmin_mfa_code').value.trim(); const result = document.getElementById('garmin_connect_result'); const btn = document.getElementById('garmin_mfa_btn'); if (!code) { result.innerHTML = 'Enter the code first'; return; } if (!garminMfaSessionId) { result.innerHTML = 'Session expired, click Connect again'; return; } btn.disabled = true; result.innerHTML = 'Verifying code...'; try { const resp = await fetch(GARMIN_WORKER_BASE + '/login-mfa', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: garminMfaSessionId, mfa_code: code }), }); const data = await resp.json(); await handleGarminLoginResponse(data); } catch (e) { result.innerHTML = '✗ Network error, try again'; } finally { btn.disabled = false; } } ``` -------------------------------- ### Toggle Detail Row Visibility Source: https://github.com/drkostas/hevy2garmin/blob/main/src/hevy2garmin/templates/mappings.html Toggles the display of a detail row associated with a mapping row. It adds or removes an 'open' class and changes the display style accordingly. ```javascript function toggleDetail(row) { const detail = row.nextElementSibling; if (detail.classList.contains('open')) { detail.classList.remove('open'); detail.style.display = 'none'; } else { detail.classList.add('open'); detail.style.display = 'table-row'; } } ``` -------------------------------- ### Filter Mappings Table Source: https://github.com/drkostas/hevy2garmin/blob/main/src/hevy2garmin/templates/mappings.html Filters the rows in the mappings table based on a search query. Rows that do not contain the query (case-insensitive) are hidden. ```javascript function filterMappings(query) { const q = query.toLowerCase(); document.querySelectorAll('#mappings-table tr.mapping-row').forEach(row => { const show = row.textContent.toLowerCase().includes(q); row.style.display = show ? '' : 'none'; row.nextElementSibling.style.display = 'none'; row.nextElementSibling.classList.remove('open'); }); } ```