before claude fix #1

This commit is contained in:
2025-12-23 06:09:34 -08:00
parent c505fb69a6
commit a23fa1b30d
83 changed files with 5682 additions and 0 deletions

View File

@@ -0,0 +1,308 @@
<!DOCTYPE html>
<html>
<head>
<title>Fitbit-Garmin Sync Dashboard</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 Dashboard</h1>
<div class="row mb-4">
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">Weight Records</h5>
<p class="card-text">Total: <span id="total-weights">0</span></p>
<p class="card-text">Synced: <span id="synced-weights">0</span></p>
<p class="card-text">Unsynced: <span id="unsynced-weights">0</span></p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">Activities</h5>
<p class="card-text">Total: <span id="total-activities">0</span></p>
<p class="card-text">Downloaded: <span id="downloaded-activities">0</span></p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">Sync Status</h5>
<div class="d-grid gap-2">
<button class="btn btn-primary" type="button" id="sync-weight-btn">Sync Weight</button>
<button class="btn btn-secondary" type="button" id="sync-activities-btn">Sync Activities</button>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h3>Recent Sync Logs</h3>
<div class="table-responsive">
<table class="table table-striped" id="sync-logs-table">
<thead>
<tr>
<th>Operation</th>
<th>Status</th>
<th>Start Time</th>
<th>Records Processed</th>
<th>Records Failed</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="5">Loading logs...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="row mt-5">
<div class="col-md-12">
<h3>Health Metrics</h3>
<div class="card">
<div class="card-body">
<div class="d-grid gap-2 d-md-flex justify-content-md-start">
<button class="btn btn-info me-md-2" type="button" id="sync-metrics-btn">Sync Health Metrics</button>
<button class="btn btn-outline-info me-md-2" type="button" id="view-metrics-btn">View Health Data Summary</button>
<button class="btn btn-outline-info" type="button" id="query-metrics-btn">Query Metrics</button>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-5">
<div class="col-md-12">
<h3>Activity Files</h3>
<div class="card">
<div class="card-body">
<div class="d-grid gap-2 d-md-flex justify-content-md-start">
<button class="btn btn-outline-secondary me-md-2" type="button" id="list-activities-btn">List Stored Activities</button>
<button class="btn btn-outline-secondary" type="button" id="download-activities-btn">Download Activity File</button>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-5">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<div class="d-grid gap-2 d-md-flex justify-content-md-start">
<a href="/setup" class="btn btn-primary me-md-2">Setup & Configuration</a>
<a href="/docs" class="btn btn-outline-secondary" target="_blank">API Documentation</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-5">
<div class="col-md-12">
<h3>Health Metrics</h3>
<div class="card">
<div class="card-body">
<div class="d-grid gap-2 d-md-flex justify-content-md-start">
<button class="btn btn-info me-md-2" type="button" id="sync-metrics-btn">Sync Health Metrics</button>
<button class="btn btn-outline-info me-md-2" type="button" id="view-metrics-btn">View Health Data Summary</button>
<button class="btn btn-outline-info" type="button" id="query-metrics-btn">Query Metrics</button>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-5">
<div class="col-md-12">
<h3>Activity Files</h3>
<div class="card">
<div class="card-body">
<div class="d-grid gap-2 d-md-flex justify-content-md-start">
<button class="btn btn-outline-secondary me-md-2" type="button" id="list-activities-btn">List Stored Activities</button>
<button class="btn btn-outline-secondary" type="button" id="download-activities-btn">Download Activity File</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Load dashboard data when page loads
document.addEventListener('DOMContentLoaded', function() {
loadDashboardData();
// Set up sync buttons
document.getElementById('sync-weight-btn').addEventListener('click', syncWeight);
document.getElementById('sync-activities-btn').addEventListener('click', syncActivities);
// Set up metrics buttons
document.getElementById('sync-metrics-btn').addEventListener('click', syncHealthMetrics);
document.getElementById('view-metrics-btn').addEventListener('click', viewHealthSummary);
document.getElementById('query-metrics-btn').addEventListener('click', queryMetrics);
// Set up activity file buttons
document.getElementById('list-activities-btn').addEventListener('click', listActivities);
document.getElementById('download-activities-btn').addEventListener('click', downloadActivityFile);
});
async function loadDashboardData() {
try {
const response = await fetch('/api/status');
const data = await response.json();
document.getElementById('total-weights').textContent = data.total_weight_records;
document.getElementById('synced-weights').textContent = data.synced_weight_records;
document.getElementById('unsynced-weights').textContent = data.unsynced_weight_records;
document.getElementById('total-activities').textContent = data.total_activities;
document.getElementById('downloaded-activities').textContent = data.downloaded_activities;
// Update logs table
const logsBody = document.querySelector('#sync-logs-table tbody');
logsBody.innerHTML = '';
data.recent_logs.forEach(log => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${log.operation}</td>
<td>${log.status}</td>
<td>${log.start_time}</td>
<td>${log.records_processed}</td>
<td>${log.records_failed}</td>
`;
logsBody.appendChild(row);
});
} catch (error) {
console.error('Error loading dashboard data:', error);
}
}
async function syncWeight() {
try {
const response = await fetch('/api/sync/weight', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
alert(`Weight sync initiated: ${data.message}`);
// Refresh dashboard data
loadDashboardData();
} catch (error) {
console.error('Error syncing weight:', error);
alert('Error initiating weight sync: ' + error.message);
}
}
async function syncActivities() {
try {
const response = await fetch('/api/sync/activities', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ days_back: 30 })
});
const data = await response.json();
alert(`Activity sync initiated: ${data.message}`);
// Refresh dashboard data
loadDashboardData();
} catch (error) {
console.error('Error syncing activities:', error);
alert('Error initiating activity sync: ' + error.message);
}
}
async function syncHealthMetrics() {
try {
const response = await fetch('/api/sync/metrics', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
alert(`Health metrics sync initiated: ${data.message}`);
} catch (error) {
console.error('Error syncing health metrics:', error);
alert('Error initiating health metrics sync: ' + error.message);
}
}
async function viewHealthSummary() {
try {
const response = await fetch('/api/health-data/summary');
const data = await response.json();
alert(`Health Summary:
Steps: ${data.total_steps || 0}
Avg Heart Rate: ${data.avg_heart_rate || 0}
Sleep Hours: ${data.total_sleep_hours || 0}
Avg Calories: ${data.avg_calories || 0}`);
} catch (error) {
console.error('Error fetching health summary:', error);
alert('Error fetching health summary: ' + error.message);
}
}
async function queryMetrics() {
try {
const response = await fetch('/api/metrics/query');
const data = await response.json();
alert(`Found ${data.length} health metrics`);
} catch (error) {
console.error('Error querying metrics:', error);
alert('Error querying metrics: ' + error.message);
}
}
async function listActivities() {
try {
const response = await fetch('/api/activities/list');
const data = await response.json();
alert(`Found ${data.length} stored activities`);
} catch (error) {
console.error('Error listing activities:', error);
alert('Error listing activities: ' + error.message);
}
}
async function downloadActivityFile() {
try {
// For demo purposes, we'll use a placeholder ID
// In a real implementation, this would prompt for activity ID or list available activities
const activityId = prompt('Enter activity ID to download:', '12345');
if (activityId) {
// This would initiate a download of the stored activity file
window.open(`/api/activities/download/${activityId}`, '_blank');
}
} catch (error) {
console.error('Error downloading activity file:', error);
alert('Error downloading activity file: ' + error.message);
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,335 @@
<!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>
<!-- Current Status Section -->
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<h5 class="card-title">Current Status</h5>
<div id="status-info">
<p>Loading status...</p>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-body">
<h5 class="card-title">Garmin Connect Credentials</h5>
<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>
</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>
</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="submit" class="btn btn-primary">Save Garmin Credentials</button>
</form>
<!-- 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">
</div>
<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">
<h5 class="card-title">Fitbit API Credentials</h5>
<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>
</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>
</div>
<button type="submit" class="btn btn-primary">Save Fitbit Credentials</button>
</form>
<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>
</div>
</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">
<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>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Load initial status information
loadStatusInfo();
// Setup form event listeners
document.getElementById('garmin-credentials-form').addEventListener('submit', saveGarminCredentials);
document.getElementById('fitbit-credentials-form').addEventListener('submit', saveFitbitCredentials);
document.getElementById('fitbit-callback-form').addEventListener('submit', completeFitbitAuth);
});
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 = `
<div class="row">
<div class="col-md-4">
<p><strong>Total Weight Records:</strong> ${statusData.total_weight_records}</p>
<p><strong>Synced to Garmin:</strong> ${statusData.synced_weight_records}</p>
</div>
<div class="col-md-4">
<p><strong>Unsynced Records:</strong> ${statusData.unsynced_weight_records}</p>
<p><strong>Total Activities:</strong> ${statusData.total_activities}</p>
</div>
<div class="col-md-4">
<p><strong>Downloaded Activities:</strong> ${statusData.downloaded_activities}</p>
</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) {
garminStatusContainer.innerHTML = `
<div class="alert ${authData.garmin.authenticated ? 'alert-success' : 'alert-warning'}">
<h6>Garmin Authentication Status</h6>
<p><strong>Username:</strong> ${authData.garmin.username || 'Not set'}</p>
<p><strong>Authenticated:</strong> ${authData.garmin.authenticated ? 'Yes' : 'No'}</p>
${authData.garmin.token_expires_at ? `<p><strong>Token Expires:</strong> ${new Date(authData.garmin.token_expires_at).toLocaleString()}</p>` : ''}
${authData.garmin.last_login ? `<p><strong>Last Login:</strong> ${new Date(authData.garmin.last_login).toLocaleString()}</p>` : ''}
<p><strong>Domain:</strong> ${authData.garmin.is_china ? 'garmin.cn' : 'garmin.com'}</p>
</div>
`;
}
// Update Fitbit auth status
const fitbitStatusContainer = document.getElementById('fitbit-auth-status');
if (authData.fitbit) {
fitbitStatusContainer.innerHTML = `
<div class="alert ${authData.fitbit.authenticated ? 'alert-success' : 'alert-warning'}">
<h6>Fitbit Authentication Status</h6>
<p><strong>Client ID:</strong> ${authData.fitbit.client_id ? authData.fitbit.client_id.substring(0, 10) + '...' : 'Not set'}</p>
<p><strong>Authenticated:</strong> ${authData.fitbit.authenticated ? 'Yes' : 'No'}</p>
${authData.fitbit.token_expires_at ? `<p><strong>Token Expires:</strong> ${new Date(authData.fitbit.token_expires_at).toLocaleString()}</p>` : ''}
${authData.fitbit.last_login ? `<p><strong>Last Login:</strong> ${new Date(authData.fitbit.last_login).toLocaleString()}</p>` : ''}
</div>
`;
}
}
} catch (error) {
console.error('Error loading status info:', error);
}
}
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 (data.status === 'mfa_required') {
// Show MFA section if MFA is required and store session ID
document.getElementById('garmin-mfa-section').style.display = 'block';
// Store the session ID for later use when submitting the MFA code
window.garmin_mfa_session_id = data.session_id;
alert('Multi-factor authentication required. Please enter the verification code sent to your device.');
} else {
alert(data.message || 'Garmin credentials saved successfully');
// Refresh status after saving
loadStatusInfo();
// Hide MFA section if showing
document.getElementById('garmin-mfa-section').style.display = 'none';
}
} catch (error) {
console.error('Error saving Garmin credentials:', error);
alert('Error saving Garmin credentials: ' + error.message);
}
}
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',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(credentials)
});
const data = await response.json();
alert(data.message || 'Fitbit credentials saved successfully');
// Show the authorization link
const authLink = document.getElementById('auth-link');
authLink.href = data.auth_url;
document.getElementById('auth-url-container').style.display = 'block';
} catch (error) {
console.error('Error saving Fitbit credentials:', error);
alert('Error saving Fitbit credentials: ' + error.message);
}
}
async function completeFitbitAuth(event) {
event.preventDefault();
const formData = new FormData(event.target);
const callbackData = {
callback_url: formData.get('callback_url')
};
try {
const response = await fetch('/api/setup/fitbit/callback', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
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) {
console.error('Error completing Fitbit OAuth:', error);
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();
if (!mfaCode) {
alert('Please enter the verification code');
return;
}
try {
const response = await fetch('/api/setup/garmin/mfa', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
verification_code: mfaCode,
session_id: window.garmin_mfa_session_id
})
});
const data = await response.json();
if (response.ok) {
alert(data.message || 'MFA verification successful');
document.getElementById('garmin-mfa-section').style.display = 'none';
// Clear the MFA code field
document.getElementById('mfa-code').value = '';
// Refresh status after MFA
loadStatusInfo();
} else {
alert('MFA verification failed: ' + data.message);
}
} catch (error) {
console.error('Error submitting MFA code:', error);
alert('Error submitting MFA code: ' + error.message);
}
}
</script>
</body>
</html>