Skip to content

0jonjo/calcpace_web

Repository files navigation

Calcpace Web

Live: https://calcpace.app

A sports activity tracker for runners and cyclists with a public pace Calculator and unit Converter — built to practice modern Rails infrastructure and deployment. This app serves as a production dogfooding environment for the open-source calcpace gem.

Goals

  • Master VPS deployment with Kamal + Docker on DigitalOcean (vs. Heroku/PaaS)
  • Practice TDD with Minitest and CI/CD with GitHub Actions
  • Follow 37signals/Basecamp design principles — thin controllers, rich models, Current attributes

Stack

Layer Technology
Backend Ruby on Rails 8
Frontend ERB + Tailwind CSS
Database PostgreSQL 17
Cache / Jobs Redis + Sidekiq
Email Resend API
Monitoring AppSignal (APM + error tracking)
Deploy Kamal + Docker
Hosting DigitalOcean (VPS)
File Storage Cloudflare R2 (Active Storage — S3-compatible)
Maps Leaflet.js + OpenStreetMap (no API key required)

Requirements

  • Ruby 3.4.4 (via asdf)
  • Docker + Docker Compose
  • PostgreSQL client (libpq)

Local Setup

# 1. Clone and install dependencies
git clone https://github.com/0jonjo/calcpace_web.git
cd calcpace_web
bundle install

# 2. Start PostgreSQL and Redis via Docker
docker compose up -d

# 3. Create and migrate the database
bin/rails db:create db:migrate

# 4. Start the development server (Rails + Tailwind watcher)
bin/dev

Open http://localhost:3000.

Note: A system Redis on port 6379 will conflict. The compose file maps Redis to port 6380 to avoid it.

Running Tests

bin/rails test

Public Tools

The calculator and converter are publicly accessible at calcpace.app — no login required:

Calculator (/calculator)

Compute running and cycling metrics:

  • Pace — given distance and total time
  • Total Time — given distance and pace
  • Distance — given total time and pace
  • Race Predictor — estimate finish time for standard race distances (5K, 10K, half marathon, marathon) from a known result

Supports both metric (km) and imperial (mi) unit systems.

Converter (/converter)

Convert between common sports units:

  • Distance — km ↔ mi
  • Speed — km/h ↔ mph
  • Pace — min/km ↔ min/mi

All calculations are powered by the calcpace gem.

Guest Account

A guest account with sample activities (a run and a bike ride) is available to explore the app:

  • Email: guest@calcpace.app
  • Password: set via GUEST_PASSWORD env var in production

The guest profile is automatically reset every Monday at 3am by the GuestResetJob.

Registration (Invite Only)

Registration is invite-only. To create an invite and send the email in production:

kamal app exec --reuse "bin/rails invite:create[person@example.com]"

Output:

Invite created and sent!
  Email   : person@example.com
  Code    : BETA-X7K2P9
  Expires : 2026-04-27 (30 days)
  Link    : https://calcpace.app/registration/new?invite_code=BETA-X7K2P9

The invite email is sent automatically via Resend. The link pre-fills the invite code on the registration form. Codes expire after 30 days and are single-use.

To create an account locally, generate an invite first:

bin/rails invite:create[person@example.com]

Then open the pre-filled link printed in the console (or visit http://localhost:3000/registration/new and enter the code manually).

Transactional Emails

Handled by Resend (free tier: 3,000 emails/month). All emails are queued via Sidekiq.

Event Email
Invite sent Invite code + pre-filled registration link
Account created Welcome email
Password reset requested Reset link (expires in 15 min)
Password changed Security notification

Activity Features

GPS / GPX Upload

Activities support an optional .gpx file upload (stored in Cloudflare R2). When a GPX file is attached:

  1. GpxParseJob runs asynchronously via Sidekiq
  2. The job parses trackpoints from the file using the gpx gem
  3. Trackpoints are bulk-inserted into the trackpoints table
  4. Distance and elevation gain are calculated via the calcpace gem (TrackCalculator module — Haversine formula) and stored on the activity

The activity show page (both logged-in and public) renders an interactive OpenStreetMap map via Leaflet.js and a per-km splits table, calculated on-the-fly from the stored trackpoints.

Public Profile

Users can opt in to a public profile at /:username. Each activity can individually be set to public or private. Public activities are listed on the profile page and have a dedicated public detail page at /:username/activities/:id.

Image Uploads

Profile avatars and activity photos are stored in Cloudflare R2 via Active Storage. Supported formats: JPEG, PNG, WebP (max 5 MB). GPX files: XML format (max 20 MB).

Background Jobs

Sidekiq processes the job queue using the Redis accessory. A separate worker container runs alongside the web container in production (see config/deploy.yml).

Job Trigger Description
GuestResetJob Every Monday at 3am (sidekiq-cron) Wipes guest user activities and recreates the profile
GpxParseJob On GPX file upload Parses trackpoints, calculates distance/elevation, bulk-inserts into DB

CI/CD

GitHub Actions runs the full test suite on every push to main. Deploys are triggered by GitHub Releases. See .github/workflows/.

Deployment

Deployment is handled by Kamal targeting a DigitalOcean VPS. See config/deploy.yml.

About

A sports activity tracker for runners and cyclists with a public calculators and unit converter. This app serves a dogfooding for the open-source calcpace gem

Topics

Resources

Stars

Watchers

Forks

Contributors