- TypeScript 86.8%
- C 9.3%
- JavaScript 3%
- Swift 0.5%
- HTML 0.1%
- Other 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
Build and Push Docker Image / build (push) Successful in 58s
|
||
| .forgejo/workflows | ||
| android | ||
| docs/plans | ||
| fitsky-pebble | ||
| functions | ||
| ios | ||
| lexicons | ||
| public | ||
| scripts | ||
| src | ||
| tests | ||
| .dockerignore | ||
| .gitignore | ||
| capacitor.config.ts | ||
| CLAUDE.md | ||
| Dockerfile | ||
| eslint.config.js | ||
| index.html | ||
| LICENSE | ||
| nginx.conf | ||
| package.json | ||
| playwright.config.ts | ||
| playwright.offline.config.ts | ||
| pnpm-lock.yaml | ||
| README.md | ||
| tsconfig.app.json | ||
| tsconfig.json | ||
| tsconfig.node.json | ||
| vite.config.ts | ||
FitSky
Social fitness tracking built on the AT Protocol.
Log workouts, track progress, and share with friends — all stored in your personal data repository.
Self-hosted fork
This fork runs at https://fit.dr0p.in. Source: https://git.bunk3r.dev/c0nrad/fitsky-app.
-
Daily workout log (
/): FitNotes-style Today screen with Start New Workout and Copy Previous Workout, plus bottom navigation for Body, Calendar, Start, Charts and Settings. The top menu holds routines, exercise management, goals and backups; the stopwatch opens rest controls. Start opens exercise selection with a shortcut to today's scheduled routine. Populated days show exercise cards immediately below the date, with readable set values, visible notes and direct inline set editing. The bottom action becomes Exercise; starting another workout and copying previous workouts remain in the top menu. Day cards also offer Add set and Repeat last set (which logs immediately, with Undo). Inline entries recover after navigation or reload, and grouped exercises advance inline. The picker prioritizes recently logged exercises and offers a Recent filter. -
Automatic set saving: Save set and Update set persist the session locally and queue an active workout for sync. Finish workout ends the session using the same record identity. Completed-workout corrections sync without a separate Update workout step. Tap a set to edit it inline; Undo set change restores the last addition, correction, deletion or reorder, even after reload. Unfinished text stays in the device draft and is not treated as a logged set. The compact status expands to sync diagnostics only when needed.
-
Reviewed Bluesky sharing: completed, synced workouts have Share workout actions on the day view and in Calendar. The composer shows editable text, the workout URL and link card; it never adds private titles, notes, weights or health measurements automatically. Authorizing posting returns to the preview without publishing or opting into legacy automatic sharing. Confirmation atomically creates the feed post and its deterministic workout link, guarded by the repository revision and the reviewed workout CID. Retries reuse a persisted post identity; other devices check the same remote link. Uncertain attempts stay frozen until checked. Sharing state is included in device backups but is never posted by background recovery. Manual posts are marked so legacy workout edits do not replace their custom captions. AT Protocol records remain public: app visibility flags are display controls, not encryption.
-
Local demo (
/demo, development server only): sample history and in-memory saves, with authentication skipped and no PDS writes. Suitable for recording and UI testing. Real-account drafts are recovered from account-scoped browser storage after reload. Demo workouts stay in memory. Unreadable stored drafts are preserved and reported rather than reset. -
Calendar & history in the daily log: filter loaded sessions by date, title, or exercise; edit an existing strength session or repeat it as a new workout. Edits preserve original timestamps, duration, routine links, visibility and extra details. PDS edits use the original record CID to reject stale overwrites. Existing Bluesky posts are not modified by this editor.
-
Copy to date: use the copy icon on a history session, select today or a past date, choose exercises and optionally include notes. The copied sets open in a new editable draft; the original and existing destination sessions are not overwritten. Repeat remains the one-tap shortcut for copying all exercises to today. Future workout scheduling is separate.
-
Exercise progress: weight, reps, volume, distance, duration and estimated 1RM charts, inclusive custom date ranges, selectable points and rep-count records. Comparisons use the preceding equal-length local-date range: maxima for strength and totals for additive metrics. 1RM is an Epley estimate from weighted sets of at most 10 reps.
-
FitNotes catalog and tracking: all 96 defaults from the FitNotes 25.1 reference catalog, plus 19 legacy names retained without renaming workout history (115 built-ins total). The daily logger supports all ten combinations of weight, reps, distance and time. Time is editable to the second; distance and weight remain integer metres and grams internally. Distance/time activities accept either measurement alone. A five-minute walk needs no distance; new cardio entries do not copy missing measurements from history. Single-activity summaries use the logged time rather than time spent entering the workout. Corrections and repetitions update derived cardio totals while preserving separately recorded session durations. Weight and rep inputs select their existing values on entry for quick replacement.
-
Notes: workout notes, account-scoped device-local exercise cues, and per-set notes. Set notes survive edits, draft recovery, copying, PDS saves, JSON/CSV export and FitNotes import. Optional set distance/time/notes and exercise tracking-type fields extend this fork's lexicon; upstream FitSky clients may not display them.
-
Goals: editable weight/reps, minimum distance or time per session, and sessions/week targets. Frequency uses local Monday–Sunday weeks and optional per-session distance/time minimums. Strength requires both values in one working set. Global workout goals and per-exercise goals use loaded history. Target dates are reminders, not achievement cutoffs.
-
Routine planning: single- or multi-day routines, ordered exercises, grouped supersets, and preset sets including warm-ups, notes and timed/distance values. Logged sets start empty. Save a current session as a new routine or update only its loaded day. Updates use CID checks to reject stale edits. Future planned sessions are account/device-local schedule entries; starting one opens a log for today, and successful save clears its schedule entry. Imported programs can carry initial dates in their routine records; these install once per device/account when routines load. Already removed or completed local dates are not re-added.
-
Exercise management: create exercises and categories, set equipment, mark favorites and configure weight increments. Preferences are account-scoped and local by default; historical exercise names are not rewritten. Default steps are 2.5 kg or 5 lbs; custom pound increments stay in display units where representable. Formatting suppresses conversion noise while preserving exact stored grams when an entry is reopened.
-
Offline outbox: daily-log saves are durably queued in browser storage before network writes. With a signed-in session, reconnecting or Retry sync sends them using stable record keys and compare-and-swap. An accepted write with a lost response is acknowledged by record comparison on retry. Pending writes can be inspected/exported. For conflicts, Keep server version removes only the local intent; Save queued version as new workout atomically recovers the local values under a new record key after a fresh server check. Server records are never overwritten by recovery. Revisions of the same active session coalesce while offline, including after an uncertain upload. Stored revision checks reject stale tabs; remote edits still require review. Edits from a different session cannot replace an already queued record. Queue coordination requires Web Locks (HTTPS or loopback). Background sync while the app is closed is not implemented.
-
Offline reopening: after an online sign-in and app-cache preparation, FitSky can reopen into the remembered account's local workspace without OAuth requests. Workouts and routines are cached in IndexedDB; drafts, settings, measurements and pending saves remain account-scoped. Finished-but-unsynced workouts remain visible and editable with their original record keys. Reconnecting restores the authenticated session before uploading. Explicit sign-out disables local reopening. Creating/editing online routines and changing public-profile settings still require a connection. Keep browser data intact; clearing site data removes offline copies.
-
Complete training backup (
/data): Back up everything downloads one versioned JSON file containing workouts, routines, preferences, goals, measurements, draft, pending saves, schedules, program-install markers and plate inventories. Online exports fetch every workout/routine page; offline exports identify cached/partial history. Credentials and referenced image bytes are excluded. Restore is restricted to the same account, validates the entire file, and retains a pre-restore recovery copy. A durable journal rolls back interrupted or failed local restores. Other open FitSky tabs must be closed during restore. Missing online records recover at their original keys; existing online records are kept, and pending edits retain compare-and-swap conflict checks. Restore does not publish private measurements or goals to the PDS. Files are limited to 50 MiB and 11,000 cached workout/routine records. An unreadable local section can be exported for repair, but must be repaired before that recovery file can be restored. -
Workout-only backup & import (
/data): export loaded workout records as versioned JSON or spreadsheet-safe CSV. Load full history to export the full account. JSON preserves record data but does not embed referenced images or include goals, exercise settings, drafts or the outbox. Import preview checks exact record duplicates; confirmation adds new records through an atomic outbox batch with deterministic TID keys. Existing records are never overwritten. Importing publishes to your PDS, not just local storage. -
FitNotes import: reads raw
.fitnotesSQLite backups in a local worker with bundled WASM. Supports all ten FitNotes exercise tracking types, workout timing where available, and notes. Missing times use local noon with a visible warning. Unsupported exercise types/days and other FitNotes data are reported, not presented as imported. Invalid/oversize fields stop the import rather than truncate them. Keep the original backup: FitNotes-format export, CSV import and full FitNotes database migration are not implemented. Imports are bounded to 10 MiB / 10,000 workouts. -
Training screens (
/training): dedicated charts, goals, body tracking and exercise management. Secondary screens share titled back navigation, dark cards and touch-sized form controls. Charts appear before their explanations; routine schedules, optional goal conditions and advanced settings expand on demand. Exercise settings open only when creating or editing. Charts initially select a logged exercise and an available measurement, including time-only cardio. The daily log is the single primary workout/history screen. Older/live,/logand goal shortcuts redirect to current screens; explicit/legacy/*routes retain older activity-level editors. -
Body measurements: bodyweight, waist, body fat and custom measurements with date/time, notes, edit/delete, history, charts and JSON export. Canonical kg/cm/% values preserve precision across unit changes. These records stay private on this device and are excluded from PDS sync.
-
Cross-device settings sync: opt-in manual Sync now in Settings for exercise preferences and goals, using
in.dr0p.fit.trainingSettings/self. Authorizing this feature adds its explicit repository scope. The PDS record is public, as the UI explains; measurements, credentials, drafts and queued workouts are excluded. Changed-on-both-sides data requires a reviewed choice, with CID checks and a local rollback backup before applying remote settings. -
Plate calculator: open from weight entry; configure the bar and available plates per side. Uses bounded inventory-aware calculation, shows attainable weight and shortfall explicitly, and keeps kg/lb inventory configurations locally per account.
-
Daily session controls: select a logged set to move it up/down or copy its values and notes into the next unsaved set. Rest can be disabled, set from presets or configured in seconds (0–3600), restarted manually, extended by 30 seconds or dismissed. Rest defaults are account-scoped; the active deadline and selected exercise are recovered with real-account drafts. The timer uses wall-clock time, so backgrounding/reloading does not restart the rest.
-
Unfinished set entry: each exercise slot keeps its raw input, edit selection, notes and warm-up mode through exercise switching, navigation and draft recovery. These entries follow reordered slots and removal undo. Finishing a workout focuses any unfinished entry instead of silently omitting it. A pending entry keeps its original units when preferences change; save or clear it to use the new units. Inputs are not logged or published until Save set.
-
Exercise order & supersets: move exercise slots without detaching routine targets, including repeated exercise names. Group two or more slots to alternate after each new set and rest at round boundaries. Manual alternation remains available. Order, groups, and routine-slot mapping survive draft recovery; groups are retained in saved workouts and JSON backups. Copying only one member clears its now-incomplete group. FitNotes group-table import is still not supported.
-
Warm-up sets: mark sets in Track. They are visible in history and backups, but excluded from working-set routine progress, superset advancement, strength records and weight/reps goals. Warm-ups rest on the current exercise. Total volume, distance and duration still include them.
-
Exercise removal: remove an exercise from the order panel and undo the latest removal, including after reload. Undo restores its logged sets, notes, routine slot and group membership; it remains available until another exercise edit or a successful workout save.
-
Custom exercise names are reused from saved routines and workout history.
-
Weights are stored in grams and converted at the input/display boundary.
Run regression tests with pnpm exec tsx --test scripts/*.test.ts.
Run browser regressions with pnpm test:e2e (first install Chromium with
pnpm exec playwright install chromium webkit). The suite drives the actual pages
against a synthetic, paginated PDS adapter. It covers draft navigation/reload, unit conversion,
visibility defaults, repeated exercises in routines, full-history exports/deletion, goal form
isolation, corrupt preferences, offline saves, lost acknowledgements and conflicting edits.
Scrolling regressions use actual wheel input, Chromium touch gestures, and narrow WebKit
viewports, including reaching bottom controls and releasing the exercise-picker scroll lock.
The fixtures are excluded from the production container and never access a real account.
Device-backup tests exercise actual IndexedDB transactions, write-failure rollback, interrupted
restores, cross-tab exclusion, account isolation, private-data export, and missing-record recovery.
pnpm exec playwright test --config=playwright.offline.config.ts builds the production PWA and
verifies offline reopening across full Chromium process restarts, queued edits and explicit sign-out.
WebKit verifies the local boot/save/reconnect and restore paths with the app shell supplied by the
test server. Playwright WebKit's offline service-worker navigation returns an internal engine error;
an installed-PWA relaunch on a physical iPhone remains a manual check.
The mobile session test covers the empty home, routine cues, previous values, warm-ups,
navigation/reload recovery, offline edits and undo, finishing, immediate saved-workout corrections,
copying and resuming a remote active session. Run it in both engines with
pnpm exec playwright test tests/browser/active-session.spec.ts --browser=chromium and
pnpm exec playwright test tests/browser/active-session.spec.ts --browser=webkit.
Reduced viewports model an open keyboard; these are browser tests, not physical iPhone tests.
Build with pnpm build. Forgejo builds an amd64 nginx image on pushes to main;
deployment uses the image SHA in k8s-apps/fitsky/deployment.yaml.
The nginx deployment serves generic social previews; the upstream Cloudflare functions below
are not deployed by this fork.
Features
- Log Workouts — Running, cycling, swimming, hiking, weightlifting, yoga, HIIT, and more. Track duration, distance, pace, sets, reps, and weight.
- Workout Plans — Create reusable strength training plans, share them, and start structured live sessions from them.
- Live Workout Tracking — Let friends follow along in real-time as you work out.
- Dashboard & Metrics — Customize your dashboard with widgets for weekly progress, goal tracking, activity heatmaps, and trends.
- Goals — Set distance, duration, workout count, or calorie targets on weekly, monthly, or yearly periods.
- Social Feed — Follow other users and see their workouts. Like, repost, and reply.
- Cross-post to Bluesky — Workouts automatically post to Bluesky with a rich summary card. Engagement flows back as notifications.
- Visibility Controls — Granular per-workout settings to hide calories, heart rate, notes, etc.
- Pebble Watch App — Log workouts from your wrist with plan sync, heart rate tracking, and live posting.
Architecture
FitSky is a client-only application — there is no backend server. All data lives in each user's AT Protocol personal data repository (PDS), using custom lexicons for workouts, goals, plans, and profiles.
Cloudflare Pages hosts the SPA and runs edge functions for:
- OG image generation — Dynamic social preview cards using Satori + resvg-wasm
- Middleware — Injects OpenGraph meta tags into the SPA HTML for link previews (iMessage, Slack, Discord, etc.)
Project Structure
src/ React SPA source
pages/ Route pages (Dashboard, Feed, LogWorkout, etc.)
components/ Shared UI components
hooks/ React Query hooks for AT Protocol operations
api/ ATProto API functions (CRUD for workouts, plans, goals)
lib/ Utilities (colors, formatting, OAuth)
types/ TypeScript types for lexicon records
stores/ Zustand stores (auth, toast)
functions/ Cloudflare Pages Functions
_middleware.ts OG tag injection for all routes
api/og/ OG image generation endpoints
og/ Satori JSX components for OG cards
lexicons/ AT Protocol lexicon definitions (app.fitsky.*)
fitsky-pebble/ Pebble smartwatch app
src/c/ C source (workout tracking, menus, HR, GPS)
src/pkjs/ PebbleKit JS bridge (phone ↔ watch communication)
scripts/ Development tools
generate-og.tsx Local OG image preview generator
ios/ Capacitor iOS shell
android/ Capacitor Android shell
Getting Started
Prerequisites
Install & Run
# Install dependencies
pnpm install
# Start dev server
pnpm dev
The app runs at http://localhost:5173 and connects to the AT Protocol network. Sign in with your Bluesky account.
Build
pnpm build
Deploy
The app deploys to Cloudflare Pages. The functions/ directory is automatically picked up for edge functions.
pnpm wrangler pages deploy dist --project-name fitsky-app
OG Image Preview
Test OpenGraph card images locally before deploying:
# Preview a plan's OG card
pnpm og:plan <did> <rkey>
# Preview a workout's OG card
pnpm og:workout <did> <rkey>
# Custom output path
pnpm og:plan <did> <rkey> -o preview.png
Output defaults to /tmp/og-plan-preview.png or /tmp/og-workout-preview.png.
Pebble Watch App
See fitsky-pebble/ for the Pebble smartwatch app. Requires the Pebble SDK.
cd fitsky-pebble
pebble build
pebble install --emulator basalt
Lexicons
All data is stored using custom AT Protocol lexicons under the app.fitsky namespace.
| Lexicon | Description |
|---|---|
app.fitsky.workout |
Workout records with typed detail unions for cardio (distance, pace, elevation, steps, HR zones), strength (exercises with sets/reps/weight), flexibility, and HIIT. Supports live tracking, plan references, milestones, and per-field visibility. |
app.fitsky.workoutPlan |
Reusable workout templates for weightlifting, bodyweight, yoga, and HIIT with named exercises and target sets/reps. Supports cloning with attribution. |
app.fitsky.goal |
Fitness goals targeting distance, workout count, duration, or calories over weekly, monthly, or yearly periods. |
app.fitsky.blueskyPost |
Links a workout to its cross-posted Bluesky post via AT URIs, enabling engagement aggregation back into FitSky. |
app.fitsky.trend |
Dashboard trend snapshots with data points, summary stats, chart style, and an image blob for sharing. |
app.fitsky.profile |
FitSky-specific profile with custom banner and bio, falling back to Bluesky profile when not set. |
Lexicon definitions are in lexicons/.
Tech Stack
- Frontend — React 19, React Router, TanStack Query, Zustand, Tailwind CSS 4, Lucide icons
- AT Protocol —
@atproto/api,@atproto/oauth-client-browser - OG Images — Satori (JSX → SVG), resvg-wasm (SVG → PNG), Cloudflare Pages Functions
- Native — Capacitor (iOS/Android shell), Apple Health / Google Health Connect via
@capgo/capacitor-health - Watch — Pebble C SDK with PebbleKit JS bridge
- Hosting — Cloudflare Pages
Contributing
Contributions are welcome! Please open an issue first to discuss what you'd like to change.
- Fork the repo
- Create your branch (
git checkout -b my-feature) - Make your changes
- Run
pnpm buildto verify - Open a pull request