Skip to content

rdyson/cadence

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

30 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Cadence πŸͺΏ

A few hours a week, compounding. At a goose's pace.

A reusable, self-hosted progress tracker for working through any structured goal with friends or colleagues. Drop in a CSV of items, configure your users and timeline, deploy to AWS, and get a shared dashboard with per-user checkboxes, progress tracking, and a countdown to completion.

Runs on AWS free tier.

Use cases

  • Certification study groups (AWS, CKA, CISSP, etc.)
  • Monthly reading lists
  • 30-day coding challenges
  • Quarterly OKRs
  • Any N-period goal with M collaborators

Features

  • Any interval β€” week, month, day, year, sprint, quarter
  • N users β€” defined in config, each with their own login
  • Per-user checkboxes β€” you can only check your own; others are read-only
  • Progress bars β€” items completed + hours completed per user
  • Countdown β€” days remaining to completion date
  • Persistent state β€” stored in DynamoDB, survives page refreshes
  • Auth β€” AWS Cognito email/password login
  • Static frontend β€” no server, just S3 + CloudFront
  • Themes β€” clean default or dark sci-fi LCARS theme, with a β˜€οΈ/πŸŒ™ toggle per user
  • Local dev mode β€” iterate on the UI without deploying
  • Validate script β€” verify all AWS resources are healthy

Screenshots

Default theme LCARS theme
Desktop β€” Default theme Desktop β€” LCARS theme
Mobile β€” Default theme Mobile β€” LCARS theme

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Browser                                                β”‚
β”‚  index.html + app.js + style.css + cadence.json         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚               β”‚
                 static assets     API calls
                       β”‚          (JWT in header)
                       β–Ό               β–Ό
               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
               β”‚  CloudFront  β”‚  β”‚ API Gateway  β”‚
               β”‚  (HTTPS CDN) β”‚  β”‚ (HTTP API)   β”‚
               β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
                      β”‚                 β”‚
                      β”‚          β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”
                      β”‚          β”‚   Cognito    β”‚
                      β”‚          β”‚   JWT Auth   β”‚
                      β”‚          β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
                      β–Ό                 β–Ό
               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
               β”‚   S3 Bucket  β”‚  β”‚    Lambda    β”‚
               β”‚  (frontend)  β”‚  β”‚  (API logic) β”‚
               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
                                        β”‚
                                        β–Ό
                                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                 β”‚   DynamoDB   β”‚
                                 β”‚ (user state) β”‚
                                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Data flow:

  • The frontend is a static SPA served from S3 via CloudFront.
  • On login, Cognito issues a JWT.
  • Every API call includes the JWT in the Authorization header.
  • API Gateway validates it against the Cognito User Pool before the request reaches Lambda.
  • Lambda reads/writes checkbox state in DynamoDB, keyed by the user's email extracted from the JWT claims.

Prerequisites

Before you start, you need:

Requirement Notes
AWS account Create one free
AWS CLI v2 Install guide β€” run aws configure to set up credentials
Python 3.11+ python3 --version to check
Linux or macOS Windows users: use WSL2

Your AWS credentials need sufficient permissions to create DynamoDB tables, Lambda functions, API Gateway APIs, Cognito User Pools, IAM roles, S3 buckets, and CloudFront distributions. An admin-level IAM user works; a scoped policy is better for production.

Estimated AWS cost: negligible. This project uses services well within the free tier:

  • DynamoDB: 25GB storage + 200M requests/month free
  • Lambda: 1M requests/month free
  • S3: 5GB storage + 20k GET requests/month free
  • CloudFront: 1TB data transfer + 10M requests/month free (first 12 months)
  • Cognito: 50,000 MAUs free

Quick Start

1. Clone and install

git clone https://github.com/rdyson/cadence.git
cd cadence

python3 -m venv .venv
source .venv/bin/activate      # Windows (WSL): same command
pip install pyyaml boto3

2. Configure

cp cadence.example.yaml cadence.yaml
cp items.example.csv items.csv

Open cadence.yaml and set:

  • name β€” your project name
  • completion_date β€” your target end date (ISO 8601: YYYY-MM-DD)
  • interval β€” week, month, day, etc.
  • users β€” one entry per person, with id, name, and email
  • aws.region β€” your preferred AWS region (e.g. eu-west-2, us-east-1)

Open items.csv (or replace it with your own). The build script reads the column names from cadence.yaml β†’ columns, so your CSV just needs a consistent header row.

If you need a break in the schedule, you can define a visible empty period in cadence.yaml and simply leave that period unused in the CSV. This is useful for skip weeks, buffer weeks, holidays, or catch-up periods.

3. Deploy

python3 scripts/setup.py
python3 scripts/deploy.py

setup.py deploys all AWS infrastructure via a single CloudFormation stack (~10 minutes on first run):

  • DynamoDB, Lambda, API Gateway, Cognito, S3, CloudFront
  • OTP auth triggers + SES verification (if otp: true)
  • Creates Cognito users from cadence.yaml

All created resource IDs are written back to cadence.yaml automatically.

deploy.py builds cadence.json from your config + CSV, syncs the frontend to S3, updates Lambda code, and invalidates CloudFront.

Why CloudFront? S3 website URLs are HTTP only. Cognito requires HTTPS. CloudFront provides HTTPS and is free tier eligible.

4. Sign in

Each user in cadence.yaml gets a Cognito account with a randomly generated temporary password, printed in the script output:

  βœ“ Created user: Rob (rob@example.com) β€” temp password: a8Kz3xQ_mNpR!A1a
  βœ“ Created user: Adam (adam@example.com) β€” temp password: bT7wYc2_hLsJ!A1a

On first sign-in, Cognito will prompt each user to set their own password. This is handled automatically by the login screen β€” they'll see a "Set new password" field appear after their first attempt.

Share the dashboard URL and each user's temporary password with them.


CSV format

Your CSV needs at minimum a title column and a period column. Hours are optional.

Title,Hours,Week
Introduction to the topic,0.5,1
Deep dive: subtopic A,2.0,1
Deep dive: subtopic B,1.5,2

Column names must match the columns settings in cadence.yaml. Defaults are Title, Hours, Week.

Rows are automatically skipped if:

  • The title is blank
  • The title starts with -- (e.g. -- Foo bar comment rows)
  • The period value is not a valid integer (e.g. section header rows with no week number)

This means you can use a spreadsheet with section headers and totals β€” Cadence will ignore them cleanly.

Skip weeks / empty periods

Cadence can show a period even when it has no items, as long as that period is explicitly configured in cadence.yaml.

Example:

completion_date: "2026-05-10"

period_labels:
  4: "Week 4 (March 23)"
  5: "Week 5 (March 30) Skip"
  6: "Week 6 (April 6) Skip"
  7: "Week 7 (April 13)"

period_descriptions:
  5: "Skip week"
  6: "Skip week"

Then keep your real items in later numbered periods in items.csv (for example, move old week 5 items to period 7).

This preserves existing checkbox data as long as the item titles themselves stay the same.


Config reference

See cadence.example.yaml for a fully annotated example.

Field Required Description
name βœ… Project display name
completion_date βœ… Target end date (YYYY-MM-DD). Move this out if you add skip/buffer periods and want the countdown to match.
interval βœ… week / month / day / year / sprint / quarter
csv βœ… Path to your CSV (relative to cadence.yaml)
columns.title βœ… CSV column name for item titles
columns.period βœ… CSV column name for period numbers
columns.hours β€” CSV column name for time estimates (omit to hide hours)
users βœ… List of { id, name, email }
theme β€” Default theme for new visitors: default or lcars. Users can toggle freely; their choice is saved in localStorage.
otp β€” Set to true to enable passwordless email OTP login (see Email OTP login)
ses_sender_email β€” SES-verified sender address for OTP emails (required if otp: true)
period_labels β€” Override period headings (e.g. 1: "Week 1 β€” March 2"). Explicitly configured empty periods are still rendered, which enables skip weeks.
period_descriptions β€” Optional per-period subtitle text (e.g. 5: "Skip week")
aws.region βœ… AWS region
aws.dynamodb_table βœ… DynamoDB table name
aws.cognito_user_pool_id β€” Set automatically by setup.py
aws.cognito_client_id β€” Set automatically by setup.py
aws.api_url β€” Set automatically by setup.py
aws.s3_bucket β€” Set automatically by setup.py
aws.cloudfront_url β€” Set automatically by setup.py

Architecture

CloudFront (HTTPS)
      β”‚
      β–Ό
S3 Bucket
  β”œβ”€β”€ index.html
  β”œβ”€β”€ app.js
  β”œβ”€β”€ style.css
  └── cadence.json  ← baked from cadence.yaml + items.csv at deploy time

      β”‚ (JWT in Authorization header)
      β–Ό
API Gateway (Cognito JWT authorizer)
      β”‚
      β–Ό
Lambda (lambda_function.py)
      β”‚
      β–Ό
DynamoDB
  └── Table: one item per user, map of checked item titles

How auth works:

  1. Cognito issues a JWT on login.
  2. The browser includes it in every API request.
  3. API Gateway validates the token against your Cognito User Pool before the Lambda ever runs.
  4. The Lambda extracts the username from the validated claims β€” no auth logic in application code.

Scripts

Script When to run Description
scripts/setup.py Once (first time) Deploy all AWS infrastructure via CloudFormation
scripts/deploy.py After any changes Build + upload to S3 + update Lambda + invalidate cache
scripts/build.py After editing CSV/config Builds frontend/cadence.json
scripts/validate.py Anytime Checks all AWS resources are healthy
scripts/dev.py During development Local dev server with mock API (no AWS needed)
scripts/teardown.py To remove everything Deletes the CloudFormation stack and all resources

setup.py is safe to re-run β€” it updates the existing stack if one exists.


Email OTP login

Cadence supports passwordless email login as an alternative to passwords. Users enter their email, receive a 6-digit code, and enter it to sign in. This uses Cognito's custom auth flow with SES for email delivery.

Setup

  1. Add to cadence.yaml:
otp: true
ses_sender_email: noreply@yourdomain.com
  1. Run setup (creates or updates the stack with OTP resources):
python3 scripts/setup.py
  1. Redeploy:
python3 scripts/deploy.py

SES sandbox vs production

New AWS accounts start in SES sandbox mode, which restricts who you can send email to:

Mode Sender Recipients Daily limit
Sandbox Must be verified Must each be verified 200 emails/day
Production Must be verified Anyone 50,000+/day

For sandbox mode (small teams, testing): setup-otp.sh automatically sends SES verification emails to the sender and all users in cadence.yaml. Each person must click the verification link in their inbox before OTP will work for them.

For production mode (larger teams, no recipient verification): request production access in the SES console or via CLI:

aws sesv2 put-account-details \
  --production-access-enabled \
  --mail-type TRANSACTIONAL \
  --use-case-description "Sending one-time login codes for a private app" \
  --website-url "https://your-cadence-url" \
  --region your-region

This is typically approved within 24 hours.


Adding a new user

  1. Add them to users in cadence.yaml
  2. Run python3 scripts/setup.py (updates the stack if needed, creates the new Cognito user)
  3. Run python3 scripts/deploy.py (rebuilds cadence.json with the new user column)
  4. Share the dashboard URL + the temporary password from the setup output

Local development

Iterate on the frontend without deploying to AWS:

python scripts/dev.py

This starts a local server at http://localhost:8000 that:

  • Serves the frontend from frontend/
  • Mocks the API with a local JSON file (.dev-state.json)
  • Auto-builds cadence.json from your config
  • Skips auth β€” auto-logs in as the first user
  • Checkbox state persists across refreshes (stored locally)

Options:

python scripts/dev.py --port 3000       # custom port
python scripts/dev.py --skip-build      # don't rebuild cadence.json

No AWS credentials, no internet connection required. Edit HTML/CSS/JS, refresh the browser.


Validate

Check that all AWS resources exist and are properly configured:

python scripts/validate.py

This checks: config file, DynamoDB table, Cognito pool + users, Lambda function, API Gateway (including a live 401 test), S3 bucket + files, CloudFront reachability, and IAM role + policies.

Useful for debugging after changes, verifying a fresh setup, or diagnosing "it was working yesterday" issues.


Troubleshooting

Login fails with "Incorrect username or password" The user may not have been created. Check that setup.py completed successfully and that the email in cadence.yaml matches what was used to create the Cognito user.

Checkboxes don't save / API errors in console Check that aws.api_url is set in cadence.yaml (written by setup.py). Rebuild and redeploy: python3 scripts/deploy.py.

Dashboard shows "Error loading cadence.json" Run python scripts/build.py to generate frontend/cadence.json, then redeploy.

CloudFront returns stale content after deploy deploy.py creates a CloudFront invalidation automatically. If content still appears stale, wait 1–2 minutes for the invalidation to propagate.

"Access Denied" from S3 The S3 bucket is private by design. Traffic must go through CloudFront. Check that your CloudFront distribution has an Origin Access Control (OAC) set up pointing to the bucket β€” setup.py handles this automatically via the CloudFormation template.


Teardown

To remove all AWS resources:

python3 scripts/teardown.py

This deletes the CloudFormation stack (which removes all resources β€” DynamoDB, Lambda, API Gateway, Cognito, S3, CloudFront, IAM role) and cleans up the generated values in cadence.yaml. You'll be prompted to type destroy to confirm.

To set up again afterwards, re-run python3 scripts/setup.py as described in Quick Start.


License

MIT

About

A reusable, self-hosted progress tracker for working through any structured goal with friends or colleagues.

Resources

Stars

Watchers

Forks

Contributors