This commit is contained in:
2026-01-01 07:14:18 -08:00
parent 25745cf6d6
commit c45e41b6a9
100 changed files with 8068 additions and 2424 deletions

View File

@@ -1,15 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<title>Fitbit-Garmin Sync - Setup</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h1>Fitbit-Garmin Sync - Setup</h1>
<ul class="nav nav-tabs mb-4">
<li class="nav-item">
<a class="nav-link" href="/">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/activities">Activities</a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/setup">Setup</a>
</li>
</ul>
<div class="mb-3">
<button type="button" class="btn btn-info" id="load-from-consul-btn">Load Config from Consul</button>
</div>
@@ -27,7 +41,7 @@
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card">
@@ -36,46 +50,55 @@
<form id="garmin-credentials-form">
<div class="mb-3">
<label for="garmin-username" class="form-label">Username</label>
<input type="text" class="form-control" id="garmin-username" name="username" required autocomplete="username">
<input type="text" class="form-control" id="garmin-username" name="username" required
autocomplete="username">
</div>
<div class="mb-3">
<label for="garmin-password" class="form-label">Password</label>
<input type="password" class="form-control" id="garmin-password" name="password" required autocomplete="current-password">
<input type="password" class="form-control" id="garmin-password" name="password"
required autocomplete="current-password">
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="garmin-china" name="is_china">
<label class="form-check-label" for="garmin-china">Use China domain (garmin.cn)</label>
</div>
<button type="button" class="btn btn-secondary" id="test-garmin-btn">Test Garmin Credentials</button>
<button type="submit" class="btn btn-primary" id="save-garmin-btn" disabled>Save Garmin Credentials</button>
<button type="button" class="btn btn-info" id="test-garmin-token-btn">Test Current Garmin Token</button>
<button type="button" class="btn btn-secondary" id="test-garmin-btn">Test Garmin
Credentials</button>
<button type="submit" class="btn btn-primary" id="save-garmin-btn" disabled>Save Garmin
Credentials</button>
<button type="button" class="btn btn-info" id="test-garmin-token-btn">Test Current Garmin
Token</button>
<button type="button" class="btn btn-danger ms-2" id="clear-garmin-btn">Clear
Credentials</button>
</form>
<!-- Garmin Authentication Status -->
<div id="garmin-auth-status-text" class="mt-3">
<p>Current auth state: <span class="badge bg-secondary">Not Tested</span></p>
</div>
<div id="garmin-token-test-result" class="mt-3"></div>
<!-- Garmin Authentication Status -->
<div id="garmin-auth-status" class="mt-3">
<p>Loading Garmin authentication status...</p>
</div>
<!-- MFA Section -->
<div id="garmin-mfa-section" class="mt-3" style="display: none;">
<h6>Multi-Factor Authentication (MFA)</h6>
<div class="mb-3">
<label for="mfa-code" class="form-label">Enter Verification Code</label>
<input type="text" class="form-control" id="mfa-code" placeholder="Enter code from your authenticator app or SMS">
<input type="text" class="form-control" id="mfa-code"
placeholder="Enter code from your authenticator app or SMS">
</div>
<button type="button" class="btn btn-primary" id="submit-mfa-btn">Submit Verification Code</button>
<button type="button" class="btn btn-primary" id="submit-mfa-btn">Submit Verification
Code</button>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-body">
@@ -83,74 +106,115 @@
<form id="fitbit-credentials-form">
<div class="mb-3">
<label for="fitbit-client-id" class="form-label">Client ID</label>
<input type="text" class="form-control" id="fitbit-client-id" name="client_id" required autocomplete="username">
<input type="text" class="form-control" id="fitbit-client-id" name="client_id" required
autocomplete="username">
</div>
<div class="mb-3">
<label for="fitbit-client-secret" class="form-label">Client Secret</label>
<input type="password" class="form-control" id="fitbit-client-secret" name="client_secret" required autocomplete="new-password">
<input type="password" class="form-control" id="fitbit-client-secret"
name="client_secret" required autocomplete="new-password">
</div>
<button type="button" class="btn btn-secondary" id="test-fitbit-btn">Test Fitbit Credentials</button>
<button type="submit" class="btn btn-primary" id="save-fitbit-btn" disabled>Save Fitbit Credentials</button>
<div class="mb-3">
<label for="fitbit-redirect-uri" class="form-label">Redirect URI</label>
<input type="text" class="form-control" id="fitbit-redirect-uri" name="redirect_uri"
value="http://localhost:8000/fitbit_callback"
placeholder="http://localhost:8000/fitbit_callback">
<div class="form-text">Must match exactly what you entered in the Fitbit Developer
Dashboard. Leave blank if only one is registered.</div>
</div>
<button type="button" class="btn btn-secondary" id="test-fitbit-btn">Test Fitbit
Credentials</button>
<button type="submit" class="btn btn-primary" id="save-fitbit-btn" disabled>Save Fitbit
Credentials</button>
<button type="button" class="btn btn-info" id="test-fitbit-token-btn">Test Current Fitbit
Token</button>
</form>
<div id="fitbit-token-test-result" class="mt-3"></div>
<div class="mt-3">
<div id="auth-url-container" style="display: none;">
<p>After saving credentials, click the link below to authorize:</p>
<a id="auth-link" class="btn btn-secondary" href="#" target="_blank">Authorize with Fitbit</a>
<a id="auth-link" class="btn btn-secondary mb-3" href="#" target="_blank">Authorize with
Fitbit</a>
</div>
</div>
<!-- OAuth Flow Section (Moved here) -->
<div id="fitbit-oauth-flow-section"
style="display: none; border-top: 1px solid #eee; padding-top: 15px;">
<h5>Complete Fitbit OAuth Flow</h5>
<form id="fitbit-callback-form">
<div class="mb-3">
<label for="callback-url" class="form-label">Paste full callback URL from
browser</label>
<input type="url" class="form-control" id="callback-url" name="callback_url"
required>
</div>
<button type="submit" class="btn btn-success">Complete OAuth Flow</button>
</form>
</div>
<!-- Fitbit Authentication Status -->
<div id="fitbit-auth-status" class="mt-3">
<p>Loading Fitbit authentication status...</p>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-4" id="fitbit-oauth-flow-section" style="display: none;">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<h5 class="card-title">Complete Fitbit OAuth Flow</h5>
<form id="fitbit-callback-form">
<div class="mb-3">
<label for="callback-url" class="form-label">Paste full callback URL from browser</label>
<input type="url" class="form-control" id="callback-url" name="callback_url" required>
</div>
<button type="submit" class="btn btn-success">Complete OAuth Flow</button>
</form>
</div>
</div>
</div>
</div>
<!-- Section removed here -->
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('DOMContentLoaded', function () {
// Load initial status information
loadStatusInfo();
// Setup form event listeners
document.getElementById('load-from-consul-btn').addEventListener('click', loadFromConsul);
document.getElementById('test-garmin-btn').addEventListener('click', testGarminCredentials);
document.getElementById('test-garmin-token-btn').addEventListener('click', testGarminToken);
document.getElementById('clear-garmin-btn').addEventListener('click', clearGarminCredentials);
document.getElementById('garmin-credentials-form').addEventListener('submit', saveGarminCredentials);
document.getElementById('test-fitbit-btn').addEventListener('click', testFitbitCredentials);
document.getElementById('fitbit-credentials-form').addEventListener('submit', saveFitbitCredentials);
document.getElementById('fitbit-credentials-form').addEventListener('submit', saveFitbitCredentials);
document.getElementById('fitbit-callback-form').addEventListener('submit', completeFitbitAuth);
document.getElementById('test-fitbit-token-btn').addEventListener('click', testFitbitToken);
});
async function testFitbitToken() {
const resultDiv = document.getElementById('fitbit-token-test-result');
resultDiv.innerHTML = '<p>Testing token...</p>';
try {
const response = await fetch('/api/setup/fitbit/test-token', { method: 'POST' });
const data = await response.json();
if (response.ok) {
resultDiv.innerHTML = `<div class="alert alert-success"><strong>Success!</strong> ${data.message}</div>`;
} else {
resultDiv.innerHTML = `<div class="alert alert-danger">${data.message || 'Failed to test token'}</div>`;
}
} catch (error) {
resultDiv.innerHTML = `<div class="alert alert-danger">Error: ${error.message}</div>`;
}
}
async function testGarminToken() {
const resultDiv = document.getElementById('garmin-token-test-result');
resultDiv.innerHTML = '<p>Testing token...</p>';
try {
const response = await fetch('/api/setup/garmin/test-token', { method: 'POST' });
const data = await response.json();
if (response.ok) {
resultDiv.innerHTML = `<div class="alert alert-success"><pre>${JSON.stringify(data, null, 2)}</pre></div>`;
} else {
resultDiv.innerHTML = `<div class="alert alert-danger">${data.detail || 'Failed to test token'}</div>`;
// Show detail (FastAPI standard) or message (our custom response)
resultDiv.innerHTML = `<div class="alert alert-danger">${data.detail || data.message || 'Failed to test token'}</div>`;
}
} catch (error) {
resultDiv.innerHTML = `<div class="alert alert-danger">Error: ${error.message}</div>`;
@@ -158,35 +222,56 @@
}
async function loadFromConsul() {
alert('Attempting to load config from Consul and save to backend...');
// alert('Attempting to load config from Consul...');
console.log('loadFromConsul function called');
const btn = document.getElementById('load-from-consul-btn');
const originalText = btn.innerText;
btn.innerText = 'Loading...';
btn.disabled = true;
try {
const response = await fetch('/api/setup/load-consul-config', { method: 'POST' });
console.log('Response received:', response);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || 'Failed to load config from Consul');
}
const data = await response.json();
if (data.status === "mfa_required") {
alert(data.message);
loadStatusInfo(); // Refresh the status info to potentially show MFA section
} else {
alert(data.message || 'Configuration loaded from Consul successfully.');
loadStatusInfo(); // Refresh the status info
console.log('Config data:', data);
// Populate Garmin Form
if (data.garmin) {
if (data.garmin.username) document.getElementById('garmin-username').value = data.garmin.username;
if (data.garmin.password) document.getElementById('garmin-password').value = data.garmin.password;
if (data.garmin.is_china !== undefined) document.getElementById('garmin-china').checked = data.garmin.is_china;
}
// Populate Fitbit Form
if (data.fitbit) {
if (data.fitbit.client_id) document.getElementById('fitbit-client-id').value = data.fitbit.client_id;
if (data.fitbit.client_secret) document.getElementById('fitbit-client-secret').value = data.fitbit.client_secret;
if (data.fitbit.redirect_uri) document.getElementById('fitbit-redirect-uri').value = data.fitbit.redirect_uri;
}
alert(data.message || 'Configuration loaded. Please review and save your credentials.');
} catch (error) {
console.error('Error loading config from Consul:', error);
alert('Error loading config from Consul: ' + error.message);
} finally {
btn.innerText = originalText;
btn.disabled = false;
}
}
async function loadStatusInfo() {
try {
// Get general status
const statusResponse = await fetch('/api/status');
const statusData = await statusResponse.json();
// Update status info
const statusContainer = document.getElementById('status-info');
statusContainer.innerHTML = `
@@ -204,12 +289,12 @@
</div>
</div>
`;
// Get authentication status from a new API endpoint
const authStatusResponse = await fetch('/api/setup/auth-status');
if (authStatusResponse.ok) {
const authData = await authStatusResponse.json();
// Update Garmin auth status
const garminStatusContainer = document.getElementById('garmin-auth-status');
if (authData.garmin) {
@@ -234,7 +319,7 @@
}
garminStatusContainer.innerHTML = `<div class="alert ${authData.garmin.authenticated ? 'alert-success' : 'alert-warning'}">${garminStatusHtml}</div>`;
}
// Update Fitbit auth status
const fitbitStatusContainer = document.getElementById('fitbit-auth-status');
if (authData.fitbit) {
@@ -247,13 +332,21 @@
${authData.fitbit.last_login ? `<p><strong>Last Login:</strong> ${new Date(authData.fitbit.last_login).toLocaleString()}</p>` : ''}
</div>
`;
// Show/Hide Sync Section
const syncSection = document.getElementById('fitbit-sync-section');
if (authData.fitbit.authenticated) {
syncSection.style.display = 'block';
} else {
syncSection.style.display = 'none';
}
}
}
} catch (error) {
console.error('Error loading status info:', error);
}
}
async function testGarminCredentials() {
const form = document.getElementById('garmin-credentials-form');
const formData = new FormData(form);
@@ -274,9 +367,9 @@
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
const data = await response.json();
if (data.status === 'mfa_required') {
statusText.innerHTML = `<p>Current auth state: <span class="badge bg-warning">MFA Required</span></p>`;
document.getElementById('garmin-mfa-section').style.display = 'block';
@@ -287,7 +380,7 @@
saveBtn.disabled = false;
alert('Garmin authentication successful. You can now save the credentials.');
} else {
statusText.innerHTML = `<p>Current auth state: <span class="badge bg-danger">Authentication Failed</span></p>`;
statusText.innerHTML = `<p>Current auth state: <span class="badge bg-danger">Authentication Failed</span></p>`;
alert(data.message || 'Garmin authentication failed.');
}
} catch (error) {
@@ -299,23 +392,23 @@
async function saveGarminCredentials(event) {
event.preventDefault();
const formData = new FormData(event.target);
const credentials = {
username: formData.get('username'),
password: formData.get('password'),
is_china: formData.get('is_china') === 'on' || formData.get('is_china') === 'true'
};
try {
const response = await fetch('/api/setup/garmin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
const data = await response.json();
if (response.ok) {
alert('Garmin credentials saved successfully');
loadStatusInfo();
@@ -327,7 +420,33 @@
alert('Error saving Garmin credentials: ' + error.message);
}
}
async function clearGarminCredentials() {
if (!confirm('Are you sure you want to clear stored Garmin credentials? This will require re-authentication.')) {
return;
}
try {
const response = await fetch('/api/setup/garmin', {
method: 'DELETE'
});
const data = await response.json();
if (response.ok) {
alert(data.message || 'Garmin credentials cleared.');
loadStatusInfo();
// Reset status text
document.getElementById('garmin-auth-status-text').innerHTML = `<p>Current auth state: <span class="badge bg-secondary">Not Tested</span></p>`;
} else {
alert(data.message || 'Error clearing credentials.');
}
} catch (error) {
console.error('Error clearing Garmin credentials:', error);
alert('Error clearing Garmin credentials: ' + error.message);
}
}
async function testFitbitCredentials() {
const form = document.getElementById('fitbit-credentials-form');
const formData = new FormData(form);
@@ -345,7 +464,7 @@
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
const data = await response.json();
if (response.ok) {
@@ -366,13 +485,13 @@
async function saveFitbitCredentials(event) {
event.preventDefault();
const formData = new FormData(event.target);
const credentials = {
client_id: formData.get('client_id'),
client_secret: formData.get('client_secret')
};
try {
const response = await fetch('/api/setup/fitbit', {
method: 'POST',
@@ -381,9 +500,9 @@
},
body: JSON.stringify(credentials)
});
const data = await response.json();
if(response.ok) {
if (response.ok) {
alert('Fitbit credentials saved successfully');
loadStatusInfo();
} else {
@@ -394,15 +513,55 @@
alert('Error saving Fitbit credentials: ' + error.message);
}
}
async function syncFitbitWeight(scope) {
const resultDiv = document.getElementById('fitbit-sync-result');
resultDiv.innerHTML = `<div class="spinner-border spinner-border-sm text-primary" role="status"></div> Syncing ${scope === '30d' ? 'latest' : 'all'} data...`;
try {
const response = await fetch('/api/sync/fitbit/weight', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ scope: scope })
});
const data = await response.json();
if (response.ok) {
resultDiv.innerHTML = `<div class="alert alert-success mt-2">${data.message}</div>`;
} else {
resultDiv.innerHTML = `<div class="alert alert-danger mt-2">${data.detail || data.message || 'Sync failed'}</div>`;
}
} catch (error) {
resultDiv.innerHTML = `<div class="alert alert-danger mt-2">Error: ${error.message}</div>`;
}
}
async function completeFitbitAuth(event) {
event.preventDefault();
const formData = new FormData(event.target);
const callbackUrl = formData.get('callback_url');
let code = callbackUrl;
// Try to extract code parameter if it looks like a URL
try {
if (callbackUrl.includes('?')) {
const url = new URL(callbackUrl);
const params = new URLSearchParams(url.search);
if (params.has('code')) {
code = params.get('code');
}
}
} catch (e) {
console.warn("Could not parse URL, assuming input is the code itself", e);
}
const callbackData = {
callback_url: formData.get('callback_url')
code: code
};
try {
const response = await fetch('/api/setup/fitbit/callback', {
method: 'POST',
@@ -411,10 +570,10 @@
},
body: JSON.stringify(callbackData)
});
const data = await response.json();
alert(data.message || 'Fitbit OAuth flow completed successfully');
// Refresh status after completing OAuth
loadStatusInfo();
} catch (error) {
@@ -422,10 +581,10 @@
alert('Error completing Fitbit OAuth: ' + error.message);
}
}
// Handle MFA submission
document.getElementById('submit-mfa-btn').addEventListener('click', submitMFA);
async function submitMFA() {
const mfaCode = document.getElementById('mfa-code').value.trim();
const statusText = document.getElementById('garmin-auth-status-text');
@@ -435,7 +594,7 @@
alert('Please enter the verification code');
return;
}
statusText.innerHTML = `<p>Current auth state: <span class="badge bg-info">Verifying MFA...</span></p>`;
try {
@@ -444,14 +603,14 @@
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
body: JSON.stringify({
verification_code: mfaCode,
session_id: window.garmin_mfa_session_id
})
});
const data = await response.json();
if (response.ok) {
statusText.innerHTML = `<p>Current auth state: <span class="badge bg-success">MFA Verification Successful</span></p>`;
saveBtn.disabled = false;
@@ -471,4 +630,5 @@
}
</script>
</body>
</html>