Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ WITHINGS_CLIENT_SECRET=""
FITBIT_CLIENT_ID=""
FITBIT_CLIENT_SECRET=""
FITBIT_SUBSCRIBER_VERIFICATION_CODE="your-verification-code"
FITBIT_ENABLE_INTRADAY_HEART_RATE="false"
# Set to "true" to enable intraday heart-rate fetches (may increase Fitbit API usage and rate limiting)

# Redis / DragonflyDB
REDIS_URL=redis://:dragonfly@localhost:6379
Expand Down
3 changes: 3 additions & 0 deletions app/composables/useDataStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export const useDataStatus = () => {
if (wellnessDateStr === todayStr) return { isStale: false, label: 'Up to Date' }

const daysAgo = getDaysAgo(latestWellnessDate)
if (daysAgo !== null && daysAgo < 0) {
return { isStale: true, label: 'Future date detected' }
}
if (daysAgo === 1) return { isStale: true, label: 'Yesterday (Sync Needed)' }

return { isStale: true, label: `${daysAgo} days old` }
Expand Down
15 changes: 12 additions & 3 deletions app/pages/connect-fitbit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<div>
<h2 class="text-xl font-semibold">Connect Fitbit</h2>
<p class="text-sm text-muted">
Connect your Fitbit account to sync nutrition history and food logs.
Connect your Fitbit account to sync nutrition, sleep, and heart-rate trends.
</p>
</div>
</div>
Expand All @@ -40,7 +40,8 @@
<li>• Daily calories and macros</li>
<li>• Logged foods and meal entries</li>
<li>• Water intake summaries</li>
<li>• Historical nutrition trends</li>
<li>• Sleep duration and stage summaries</li>
<li>• HRV and resting heart-rate trends</li>
</ul>
</div>

Expand Down Expand Up @@ -69,6 +70,14 @@
<UIcon name="i-heroicons-check-circle" class="w-4 h-4 text-green-600" />
<span>Read nutrition and food logs</span>
</div>
<div class="flex items-center gap-2">
<UIcon name="i-heroicons-check-circle" class="w-4 h-4 text-green-600" />
<span>Read sleep and recovery metrics</span>
</div>
<div class="flex items-center gap-2">
<UIcon name="i-heroicons-check-circle" class="w-4 h-4 text-green-600" />
<span>Read heart-rate and HRV summaries</span>
</div>
</div>
</div>
</div>
Expand Down Expand Up @@ -100,7 +109,7 @@
meta: [
{
name: 'description',
content: 'Connect your Fitbit account to sync nutrition history and food logs.'
content: 'Connect your Fitbit account to sync nutrition, sleep, and heart-rate trends.'
}
]
})
Expand Down
5 changes: 4 additions & 1 deletion app/pages/dashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -641,9 +641,12 @@
// Wellness modal handlers
function openWellnessModal() {
// Use today's date or the latest wellness date
wellnessModalDate.value = userStore.profile?.latestWellnessDate
const latestDate = userStore.profile?.latestWellnessDate
? new Date(userStore.profile.latestWellnessDate)
: getUserLocalDate()

const today = getUserLocalDate()
wellnessModalDate.value = latestDate > today ? today : latestDate
showWellnessModal.value = true
}

Expand Down
9 changes: 8 additions & 1 deletion server/api/integrations/fitbit/authorize.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,14 @@ export default defineEventHandler(async (event) => {
path: '/'
})

const scope = 'nutrition'
const scope = [
'nutrition',
'sleep',
'heartrate',
'weight',
'oxygen_saturation',
'respiratory_rate'
].join(' ')

const params = new URLSearchParams({
response_type: 'code',
Expand Down
24 changes: 24 additions & 0 deletions server/api/profile/dashboard.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { sportSettingsRepository } from '../../utils/repositories/sportSettingsR
import { wellnessRepository } from '../../utils/repositories/wellnessRepository'
import { nutritionRepository } from '../../utils/repositories/nutritionRepository'
import { workoutRepository } from '../../utils/repositories/workoutRepository'
import { getEndOfDayUTC, getUserTimezone } from '../../utils/date'
import { bodyMetricResolver } from '../../utils/services/bodyMetricResolver'

export default defineEventHandler(async (event) => {
Expand Down Expand Up @@ -49,12 +50,17 @@ export default defineEventHandler(async (event) => {
// Get Sport Settings via Repository (ensures Default exists)
const sportSettings = await sportSettingsRepository.getByUserId(user.id)
const defaultProfile = sportSettings.find((s: any) => s.isDefault)
const timezone = await getUserTimezone(user.id)
const latestAllowedDate = getEndOfDayUTC(timezone, new Date())

const [wellness, dailyMetric, latestBodyFatWellness] = await Promise.all([
// Query most recent wellness record with any meaningful values (not only resting HR)
prisma.wellness.findFirst({
where: {
userId: user.id,
date: {
lte: latestAllowedDate
},
OR: [
{ restingHr: { not: null } },
{ hrv: { not: null } },
Expand Down Expand Up @@ -110,6 +116,9 @@ export default defineEventHandler(async (event) => {
prisma.dailyMetric.findFirst({
where: {
userId: user.id,
date: {
lte: latestAllowedDate
},
OR: [
{ restingHr: { not: null } },
{ hrv: { not: null } },
Expand Down Expand Up @@ -137,9 +146,24 @@ export default defineEventHandler(async (event) => {
source: true
}
}),

prisma.wellness.findFirst({
where: {
userId: user.id,
date: {
lte: latestAllowedDate
},
weight: { not: null }
},
orderBy: { date: 'desc' },
select: { weight: true }
}),
prisma.wellness.findFirst({
where: {
userId: user.id,
date: {
lte: latestAllowedDate
},
bodyFat: { not: null }
},
orderBy: { date: 'desc' },
Expand Down
Loading