Skip to content

Repository files navigation

Fitbit Health Explorer

Open-source WHOOP / Bevel alternative — a free, local-first health dashboard for your Fitbit data.

A desktop Electron app that connects to the Google Health API (the successor to the deprecated Fitbit Web API) and renders a WHOOP / Bevel‑style dashboard from your Fitbit wearable data: derived scores (Recovery, Sleep, Strain, Readiness, Physio Age) plus recovery vitals, daily activity, sleep stage timeline, and workouts — all computed locally with no subscription fees.

Works with Fitbit Charge 6, Pixel Watch, Fitbit Sense 2, Fitbit Versa 4, and any device that syncs to the Google Health API.

Fitbit Health Explorer — Dashboard Preview

Why?

  • 🆓 Free — no monthly subscription like WHOOP ($30/mo) or Bevel
  • 🔒 Private — all data stays on your machine, no third-party servers
  • 📊 Detailed — sleep timeline graph with hover crosshair, stage breakdown, trend charts
  • Fast — cache-first loading with smart incremental sync
  • 🎨 Beautiful — liquid-glass UI with light/dark mode

Keywords & Comparisons

Fitbit dashboard, WHOOP alternative, Bevel alternative, free WHOOP, open source health dashboard, Fitbit recovery score, Fitbit sleep score, Fitbit strain score, Fitbit readiness score, HRV tracking, sleep stages, Google Health API, Fitbit Charge 6 dashboard, Pixel Watch dashboard, health metrics, wearable analytics, quantified self, biometrics dashboard.


Raw metrics are synced to a local store and turned into baseline‑relative scores.

Prerequisites

Follow the official Google Health API setup guide to create your project, then:

  1. A Google Cloud project with the Google Health API enabled.
  2. An OAuth 2.0 Client ID of type Desktop app. Download its JSON and save it as credentials.json in this folder (it has an "installed" key).
  3. On the OAuth Audience page, add your Google account as a test user.
  4. On the Data Access page, add these scopes (read‑only):
    • googlehealth.profile.readonly
    • googlehealth.activity_and_fitness.readonly
    • googlehealth.sleep.readonly
    • googlehealth.health_metrics_and_measurements.readonly

Run

npm install
npm start

Click Connect with Google. Your browser opens Google's consent screen; after you approve, the app captures the auth code on a temporary localhost redirect, stores tokens in token.json, and loads your dashboard. Subsequent launches skip the login and refresh the token automatically.

To open DevTools: set DEVTOOLS=1 && npm start (Windows) or DEVTOOLS=1 npm start.

Appearance

The UI uses a modern liquid‑glass (glassmorphism) style — frosted, translucent cards over a softly animated gradient background — with a custom title bar (titleBarStyle: 'hidden' + overlay) so the window controls blend into the header.

Toggle light / dark mode with the 🌙 / ☀️ button in the header. The choice is saved (localStorage) and the native title‑bar overlay colors update to match.

Architecture

File Role
main.js Electron main process, window + IPC wiring
health-client.js OAuth flow, token refresh, API calls, dashboard building
insights.js Orchestrator: sync → persist → score → assemble; cache-first reads
sync.js Flattens API data into per‑day records and persists them
data-store.js Local JSON store (metrics, scores, journal, settings)
baselines.js Rolling mean/SD per metric (personal ranges)
scoring.js Pure scoring functions (Recovery, Sleep, Strain, Readiness)
preload.js Secure contextBridge between renderer and main
index.html / styles.css / renderer.js Dashboard UI (gauges, cards)
tests/ Unit tests (npm test, Node's built‑in test runner)

Local data lives in the OS userData dir under Electron, or ./data/ in tests. Run unit tests with npm test.

Cache-first loading (stale-while-revalidate)

On open, the app paints the cached dashboard + scores instantly from the local store (no network wait), shows an "Updating…" indicator, then fetches fresh data in the background and repaints when it arrives. If the live refresh fails, the cached view stays and the indicator reads "showing cached". The cache is rebuilt from data-store.js records via insights.getCachedInsights().

Smart incremental sync

The default view is 30 days, and history up to 90 days is retained locally for baselines and trends. Sync is incremental rather than re-fetching everything:

  • First run (empty store): a one-time full backfill of ~90 days.
  • Subsequent refreshes: only the last 3 days are re-fetched (today + a 2-day overlap that catches late-arriving data such as last night's sleep or an evening workout), then merged into the store. A typical refresh pulls ~3 days instead of 90.
  • Gaps: if the app hasn't been opened for a while, it fetches only from the first missing day up to today.
  • Timestamp skip: if a sync happened within the last 3 minutes and nothing is missing, the network call is skipped entirely and the view is served from the store.

The minimal window is computed by insights.computeSyncWindow() from the stored per-date records plus the lastSyncAt timestamp; the returned view is always assembled from the full local store, so a 30- or 90-day view stays complete even though only a few days were fetched.

Scores (baseline‑relative)

  • Recovery (0–100%) — weighted z‑scores of HRV, resting HR, sleep, and respiratory rate vs a trailing 30‑day baseline, mapped through the normal CDF (≈50% at your baseline). Needs ~2 weeks of history before it activates.
  • Sleep (0–100%) — duration vs need, efficiency, restorative % (deep+REM), and disturbances.
  • Strain (0–21) — logarithmic cardiovascular load from active zone minutes, active energy, and steps (tunable constants).
  • Readiness (0–100%) — blend of recovery, sleep, and inverse recent strain.
  • Physio Age (years) — WHOOP-inspired estimate from recovery, sleep, readiness, and strain balance (best near moderate-high strain), smoothed across days, with a "pace of aging" multiplier.

All weights/constants live in store settings so they can be tuned without code changes. Tap the on any score gauge in the app for an in‑context explanation of what it means, how it’s calculated, and today’s live breakdown.

Google Health API notes (learned while building)

  • Base URL: https://health.googleapis.com, paths under /v4/users/me/....
  • Data type IDs are kebab‑case in URLs (heart-rate, oxygen-saturation, body-fat), but snake_case in filter params (heart_rate). camelCase returns 400 Invalid data type ID.
  • dailyRollUp ranges are capped (heart‑rate fails beyond ~14 days), so long ranges are fetched in ≤14‑day chunks and merged.
  • Methods on /v4/users/me/dataTypes/{type}/dataPoints:
    • listGET (intraday / log data), supports ?filter= and ?pageSize=.
    • rollUpPOST :rollUp, body { range:{startTime,endTime}, windowSize:"86400s" }.
    • dailyRollUpPOST :dailyRollUp, body { range:{start:{date},end:{date}}, windowSizeDays:1 }.
    • Not every type supports every action; the API error lists allowed actions.
  • Singletons: GET /v4/users/me/profile, /identity, /settings.

Verified working data types (with a Fitbit Charge 6)

Metric Data type ID Method used
Resting heart rate daily-resting-heart-rate list
Heart rate variability daily-heart-rate-variability list
SpO₂ daily-oxygen-saturation list
Respiratory rate daily-respiratory-rate list
Steps steps dailyRollUp
Active energy active-energy-burned dailyRollUp
Heart rate (avg/min/max) heart-rate dailyRollUp
Active Zone Minutes active-zone-minutes dailyRollUp
Sleep (stages) sleep list
Workouts exercise list

Token & scope notes

  • In Testing publishing status, refresh tokens expire after 7 days. Move the app to Production for long‑lived refresh tokens.
  • Supporting more than 100 users requires a Google third‑party security review.

Security

credentials.json and token.json contain secrets and are git‑ignored. Never commit them.

About

Free, open-source WHOOP/Bevel alternative — a local-first Fitbit health dashboard with recovery, sleep, strain & readiness scores via Google Health API

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages