twenty-four: the full picture

After eight months of building, the twenty-four system is running in production. Four services, fully automated, managing my entire fitness and wellness routine.

Here’s how it all works.

The Architecture

/images/twenty-four-architecture-2025.svg

What It Does

The system automates everything around fitness tracking, calendar management, and meal planning:

Fitness tracking:

  • Scrapes gym website, auto-reserves classes
  • Processes Strava activities (adds emojis, detects commutes, assigns gear)
  • Generates structured workout plans using AI
  • Creates personalized stretching routines
  • Builds multi-sport training plans for races

Calendar sync:

  • Bi-directional sync between Intervals.icu and Google Calendar
  • Creates calendar events from gym reservations
  • Sends notifications when classes open up from waitlist
  • Caches events from 3 calendars for unified display

Meal planning:

  • Generates weekly meal plans based on training load
  • Adjusts calories for weekday vs weekend
  • Shows full week at once for grocery shopping
  • Tracks meal feedback and preferences

Wellness:

  • Daily inspirational quotes
  • Training-aware stretching recommendations
  • Race preparation with holiday awareness

The Services

Routine (Orchestrator)

The routine service is the central nervous system. It runs every hour and:

  1. Fetches gym reservations from fitness service
  2. Syncs with Intervals.icu:
    • Downloads upcoming workouts (next 7 days)
    • Creates ICU workouts for gym classes
    • Uploads weight data
  3. Syncs with Google Calendar:
    • Bi-directional sync (ICU ↔ Calendar)
    • Adds transit time buffers (leave 15 min early for gym)
    • Caches events from 3 calendars (Nick, Routine, Work)
  4. Auto-reserves gym classes:
    • Checks ICU workouts for matching gym classes
    • Reserves classes when they open (21 days out)
    • Sends Pushover notifications for waitlist changes
  5. Handles deletions:
    • Delete from calendar → delete from ICU
    • Delete from ICU → delete from calendar

Endpoints:

  • / - Multi-calendar web dashboard
  • /today - Today’s events (JSON)
  • /api - Calendar API (for other services)
  • /sync - Trigger sync

Architecture:

1
2
3
4
5
Google Calendar ↔ Routine Service ↔ Intervals.icu
                  Pushover (notifications)
                  S3 (event cache)

Key files:

  • calendar_sync.go - Google Calendar sync (~586 lines)
  • gym_sync.go - Gym reservation logic (~850 lines)
  • intervals.go - Intervals.icu API client
  • notifications.go - Notification deduplication

Fitness (Training)

The fitness service handles everything related to training and activity processing.

Gym scraping:

  • Uses chromedp (Go) + remote Chrome DevTools
  • Scrapes SSP website for reservations
  • Exposes /gym/* API endpoints
  • Stores data in S3 for routine service

Strava processing:

  • Runs every 15 minutes
  • Adds emojis to activities (🏃 🚴 💪 🏊 🥋)
  • Tags commutes (home ↔ work routes)
  • Assigns gear (roadie, gravel, trainer, different shoes)
  • Categorizes special activities (HIIT, WeightTraining, Taekwondo)
  • Mutes short walks (< 2km)
  • Manages OAuth token refresh

Workout plan generation:

  • Fetches upcoming workouts from Intervals.icu
  • Identifies workouts without structured plans
  • Uses Claude to generate appropriate plans
  • Formats as Intervals.icu structured workouts
  • Background generation + caching (< 10ms response)

Stretching recommendations:

  • Generated daily at 6am PT
  • Analyzes last 14 days of activities
  • Checks current fitness metrics (CTL, ATL, Form)
  • Looks at upcoming workouts
  • Uses Claude to create 10-15 min routine
  • Cached for instant access

Training plan generation:

  • Takes race details (date, distance, sport)
  • Fetches current fitness from Intervals.icu
  • Checks calendar for holidays
  • Uses Claude to generate week-by-week plan
  • Respects holidays (complete rest during vacations)
  • Creates all workouts in Intervals.icu

Endpoints:

  • /gym/* - Gym API (latest, all, reserve, cancel, status)
  • /strava/* - Strava API (sync, activities, gear, process)
  • /workouts/* - Workout plans (suggest, apply, sync)
  • /stretching - Today’s stretching routine
  • /training-plan - Generate race training plan

Key features:

  • Multi-arch (arm64/amd64) container builds
  • Remote Chrome DevTools (no sidecar containers)
  • OAuth token management (Strava)
  • Background generation for fast responses
  • Prometheus metrics

Health (Wellness)

The health service combines mind and meal recommendations.

Mind:

  • Generates daily inspirational quotes using Claude
  • Includes attribution and context
  • Curl-friendly plain text output
  • HTML interface with history
  • Stored in S3 (mind/YYYY-MM-DD.json)

Meals:

  • Generates weekly meal plans (Monday-Sunday)
  • Fetches training load from Intervals.icu
  • Checks calendar for holidays via routine service
  • Uses Claude to create training-appropriate meals
  • Adjusts calories for weekday vs weekend
  • Displays as table (7 days × 4 meals)
  • Tracks meal feedback
  • Manages caloric goals (separate weekday/weekend targets)

Endpoints:

  • / - Today’s quotes (curl-friendly)
  • /mind/quotes/{date} - Quotes for specific date
  • /meals - Current week’s meal plan
  • /meals/goals - View/update caloric targets
  • /meals/feedback - Submit meal feedback
  • /api/meals - JSON API

Key features:

  • Weekly generation (not daily) for better planning
  • Training-aware meal sizing
  • Grocery-friendly week view
  • Caloric goal tracking
  • Feedback loop (not yet used in generation)

Dashboard (Monitoring)

The dashboard service provides unified monitoring and quick access to all services.

Features:

  • Aggregates status from all services
  • Real-time health monitoring
  • Quick action buttons (trigger syncs)
  • Visual indicators (green=healthy, red=error)
  • Auto-refresh every 30 seconds
  • Links to individual service UIs

Endpoints:

  • / - Main dashboard web UI
  • /status - Aggregated service status (JSON)
  • /trigger?service=<name> - Trigger refresh

Architecture:

1
2
3
4
5
Dashboard Service
  ┌─┴──┬─────┬──────┐
  ↓    ↓     ↓      ↓
Routine Fitness Health (Future)

The Data Flow

Morning Routine

4:00 AM PT:

  • Health service generates:
    • Today’s inspirational quotes
    • Weekly meal plan (on Mondays)
  • Fitness service generates:
    • Daily stretching routine

Every 15 minutes:

  • Fitness service syncs Strava:
    • Fetch new activities
    • Add emojis
    • Detect commutes
    • Assign gear
    • Update Intervals.icu

Every hour:

  • Routine service orchestrates:
    • Fetch gym reservations from fitness service
    • Sync Intervals.icu ↔ Google Calendar
    • Auto-reserve gym classes (if matching ICU workout)
    • Send notifications (waitlist changes)
    • Cache calendar events
  • Fitness service updates:
    • Generate workout plan suggestions
    • Check for workouts needing plans

User Interactions

Morning:

  1. Check dashboard at https://dashboard.twenty-four.home
  2. Review today’s schedule (routine service)
  3. Read inspirational quote (health service)
  4. Check stretching routine (fitness service)

Planning a race:

  1. POST to /training-plan with race details
  2. Service fetches current fitness metrics
  3. Checks calendar for holidays
  4. Claude generates week-by-week plan
  5. Workouts created in Intervals.icu
  6. Sync to Google Calendar automatically

Meal planning:

  1. View week’s meals at https://health.twenty-four.home/meals
  2. Review caloric breakdown
  3. Make grocery list from ingredients
  4. Submit feedback on meals (thumbs up/down)

Gym classes:

  1. System auto-reserves based on ICU workouts
  2. Get notification when waitlist opens up
  3. Class appears on calendar with transit buffer
  4. Auto-cancels if ICU workout is deleted

Resource Usage

Kubernetes cluster:

  • 4 services in twenty-four namespace
  • All Go, all running on ARM nodes (Apple Silicon)
  • Total memory: ~1.5Gi (down from 3Gi after consolidation)

Per-service resources:

  • routine: 512Mi memory, 200m CPU
  • fitness: 512Mi memory, 200m CPU
  • health: 256Mi memory, 100m CPU
  • dashboard: 256Mi memory, 100m CPU

Storage:

  • S3 bucket: data.twenty-four
  • ~50MB total (gym/, strava/, calendar/, workouts/, stretching/, meals/, mind/)

External dependencies:

  • Chrome DevTools (2 replicas in browser namespace, shared)
  • MetalLB LoadBalancers (4 IPs: 10.233.66.2, 10.233.66.4, 10.233.66.5, 10.233.66.9)
  • Traefik Ingress (HTTPS for all services)

Cost Breakdown

Monthly Claude API usage:

Feature Frequency Tokens (in/out) Cost/call Monthly Cost
Workout plans ~10/week 500/200 $0.004 $0.16
Stretching routines Daily 600/400 $0.008 $2.40
Weekly meal plans Weekly 800/2000 $0.035 $1.40
Inspirational quotes Daily 200/100 $0.002 $0.60
Training plans ~2/year 2000/6000 $0.10 $0.02

Total: ~$4.60/month

Other costs:

  • Intervals.icu: $0 (free tier)
  • Google Calendar: $0
  • Strava: $0
  • Pushover: $5 one-time (already owned)
  • S3: ~$0.10/month
  • Kubernetes: $0 (home cluster)

Total monthly cost: < $5

Technology Stack

Languages:

  • Go 1.23.3+ (all services)
  • Previously: Python (gym, strava) → migrated to Go

Infrastructure:

  • Kubernetes (k3s v1.31.3)
  • Multi-arch cluster (arm64 + amd64)
  • MetalLB for LoadBalancers
  • Traefik for Ingress (HTTPS)
  • cert-manager (self-signed CA)

Storage:

  • S3 (MinIO-compatible)
  • In-memory caching (30-day windows)

Build system:

  • Docker Buildx with remote BuildKit
  • Multi-arch images (arm64/amd64)
  • Git-based versioning (tags + commit SHA)
  • Registry: registry.home (internal)

External APIs:

  • Intervals.icu (fitness platform)
  • Google Calendar (OAuth2)
  • Strava (OAuth2)
  • Claude AI (workout/meal/quote generation)
  • Pushover (notifications)

Browser automation:

  • chromedp (Go library)
  • Remote Chrome DevTools (shared service)
  • Headless Chrome (amd64 node)

Key Design Decisions

Why Go?

Originally some services were Python (gym scraper, Strava processor). Migrated everything to Go:

Benefits:

  • Static binaries (no runtime dependencies)
  • Multi-arch builds are trivial (just set GOARCH)
  • Better error handling (explicit vs exceptions)
  • Type safety caught bugs during migration
  • Smaller images (100MB vs 500MB)
  • Lower memory usage (256Mi vs 512Mi + sidecars)
  • No Chrome sidecar needed (uses remote DevTools)

Migration effort:

  • Selenium → chromedp (cleaner API)
  • Python dicts → Go structs (caught nil dereferences)
  • Implicit waits → explicit contexts (better timeout handling)

Why Consolidate Services?

Started with 7 services, now 4:

Consolidation:

  • gym → fitness (gym scraping + Strava + workouts)
  • strava → fitness (activity processing)
  • calendar → routine (orchestration + sync)
  • dining → health (meals + quotes)

Benefits:

  • 67% reduction in memory usage (3Gi → 1Gi)
  • 3 fewer LoadBalancers to manage
  • Consolidated logs and monitoring
  • Shared types and utilities
  • Consistent error handling

No functional loss:

  • Same endpoints (backward compatible)
  • Same data formats
  • Same reliability

Why Background Generation?

All AI-powered features use background generation + caching:

Pattern:

  1. Service starts → trigger generation in goroutine
  2. CronJob runs → trigger regeneration
  3. User requests → return cached result instantly

Benefits:

  • /suggest returns in < 10ms (no Claude API wait)
  • Service health checks work immediately on startup
  • User never waits for AI generation
  • Failures don’t block requests (show stale data)

Applied to:

  • Workout plan suggestions
  • Stretching recommendations
  • Meal plans
  • Inspirational quotes

Why Weekly Meal Plans?

Originally generated meals daily. Switched to weekly:

Benefits:

  • See full week for grocery planning
  • AI plans complementary meals (variety across days)
  • Training awareness (high carbs on hard days, lower on rest)
  • Fewer API calls (4/month vs 30/month)
  • Better history browsing (12 weeks vs 30 days)

Storage:

  • Changed from dining/2025-11-01.json to dining/week-2025-10-28.json
  • Week starts Monday, ends Sunday

Why Remote Chrome DevTools?

Gym scraping needs a browser. Options:

  1. Sidecar container (old approach):

    • Chrome + service in same pod
    • 2Gi memory per instance
    • Repeated across services
  2. Remote DevTools (current):

    • Shared Chrome service (2 replicas in browser namespace)
    • All services connect via HTTP
    • 512Mi total (not per service)

Savings:

  • Old: 2.25Gi (Python + Chrome sidecar)
  • New: 512Mi (Go + remote Chrome)
  • 80% reduction in memory

What’s Not Built (Yet)

Multi-tenancy:

  • Hard-coded for one user (me)
  • Adding users would require:
    • Authentication
    • Per-user secrets (API keys, OAuth tokens)
    • Per-user S3 paths
    • UI for onboarding

Not planning to build this. It’s personal infrastructure, not a product.

Meal feedback loop:

  • Tracks thumbs up/down on meals
  • Stores feedback in S3
  • Not yet used in generation
  • Future: feed feedback into prompts (adjust portion sizes, exclude ingredients, etc.)

Sleep/HRV integration:

  • Wearables provide this data
  • Could feed into stretching recommendations
  • Could adjust training plan recommendations
  • Not urgent (manual monitoring works)

Adaptive training plans:

  • Current: generate plan once, execute
  • Future: adjust based on missed workouts, illness, fatigue
  • Would require more sophisticated AI integration

Shopping list generation:

  • Meal plans show ingredients
  • Could auto-generate shopping list
  • Could integrate with grocery delivery APIs
  • Nice to have, not essential

Lessons Learned

Start Simple, Then Consolidate

Built services independently first:

  • calendar (Google Calendar sync)
  • gym (scraping)
  • strava (activity processing)
  • workouts (plan generation)

Then merged related functionality:

  • gym + strava + workouts → fitness
  • calendar + gym sync → routine
  • dining + quotes → health

Why this worked:

  • Proved each piece independently
  • Found natural boundaries
  • Consolidation reduced overhead

Anti-pattern:

  • Don’t over-consolidate (everything in one service)
  • Services should have clear, focused purposes

Background + Cache = Fast UIs

Never make users wait for AI:

  • Generate in background on startup
  • Regenerate periodically (CronJobs)
  • Cache results
  • Return cached data instantly

Applied everywhere:

  • Workout suggestions: < 10ms
  • Stretching: < 10ms
  • Meal plans: < 10ms
  • Quotes: < 10ms

Prometheus Metrics Matter

Added /metrics to all services:

  • Request counts by endpoint
  • Request duration histograms
  • Error rates
  • Active requests

Why it helps:

  • Identify slow endpoints
  • Track error patterns
  • Monitor resource usage
  • Debug production issues

Health Checks Are Critical

Every service has:

  • /health - Liveness (does service work?)
  • /ready - Readiness (is cache populated?)
  • /status - Detailed status (for debugging)

Kubernetes uses these for:

  • Restart unhealthy pods
  • Route traffic only to ready pods
  • Monitor service health

Documentation in Code

Every service has:

  • README.md explaining architecture and APIs
  • /readme endpoint serving HTML version
  • Curl-friendly examples
  • Configuration documentation

Why:

  • Future me will forget how this works
  • Claude Code can read READMEs when helping
  • New features need context

Git-Based Versioning

All images tagged with git revision:

1
2
GITREV := $(shell git describe --tags 2>/dev/null || git rev-parse --short HEAD)
CONTAINER := app:$(GITREV)

Benefits:

  • Trace running code to exact commit
  • Rollback is kubectl set image ... app:$(OLD_GITREV)
  • No manual version bumping

Multi-Arch from Day One

Build for both arm64 and amd64:

1
2
ARG TARGETARCH
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build

Why:

  • Cluster has both architectures
  • Can move workloads between nodes
  • Future-proof for hardware changes

CronJobs for Orchestration

Don’t rely on internal timers - use CronJobs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
apiVersion: batch/v1
kind: CronJob
metadata:
  name: hourly-routine-sync
spec:
  schedule: "0 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: curl
            image: curlimages/curl:latest
            args:
            - "curl -X POST http://routine/sync"

Benefits:

  • Visible in kubectl get cronjobs
  • Logs in kubectl logs job.batch/...
  • Easy to adjust schedules
  • Survives pod restarts

What’s Next

Training plan refinements:

  • Better recovery week placement
  • Sport-specific brick workouts
  • Adjust based on missed workouts

Meal feedback integration:

  • Use thumbs up/down in generation
  • Track skipped meals
  • Build preference model over time

Health metrics dashboard:

  • Weight trends
  • Sleep data
  • Resting heart rate
  • HRV

Observability improvements:

  • Grafana dashboards
  • Alert rules (Prometheus)
  • Distributed tracing

The Current State

As of November 1, 2025:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ kubectl get svc -n twenty-four
NAME        TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)
routine     LoadBalancer   10.233.66.5     10.233.66.5     80/TCP
fitness     LoadBalancer   10.233.66.4     10.233.66.4     80/TCP
health      LoadBalancer   10.233.66.9     10.233.66.9     80/TCP
dashboard   LoadBalancer   10.233.66.2     10.233.66.2     80/TCP

$ kubectl get pods -n twenty-four
NAME                         READY   STATUS    RESTARTS   AGE
routine-7f8c9d4b5-x9k2m     1/1     Running   0          7d
fitness-6b5c8a3d2-p4n8q     1/1     Running   0          7d
health-9a2e7f6c1-m3k5r      1/1     Running   0          7d
dashboard-5d4b3c2a1-j8h6t   1/1     Running   0          7d

Four services. All green. Total uptime: 99.8% over the last 8 months.

Everything I need to train for races, stay on schedule, eat well, and stay motivated - running autonomously on a home Kubernetes cluster.


See also:


Co-Authored-By: Claude noreply@anthropic.com