Daily head-to-head voting for Chattanooga locals. Two options enter, you pick the winner.
scrumble/
├── app/ # Frontend (static HTML/CSS/JS)
│ ├── index.html # Main voting page
│ ├── submit.html # User submission form
│ ├── history.html # Past matchups list
│ ├── admin.html # Legacy redirect to /admin
│ ├── admin/ # Admin login + panel shell
│ ├── about.html # About page
│ ├── jobs.html # Jobs/hiring page
│ ├── main.js # Voting logic, API calls
│ ├── admin.js # Admin panel logic
│ ├── config.js # API base + preview overrides
│ ├── styles.css # Main styles + navbar
│ ├── admin.css # Admin panel styles
│ └── public/ # Images (mayor photos)
├── backend/ # AWS Lambda Function URL
│ ├── app.py # Python Lambda handler
│ └── handler.js # Legacy Node handler (unused)
├── docs/ # Product documentation
│ ├── seed-data.json # Initial matchup data
│ └── *.md # Vision, data model, setup docs
├── scripts/
│ ├── seed.py # DynamoDB seeding script
│ └── setup_scrumble_cc_cloudfront.sh # CloudFront + Route53 setup
├── template.yaml # AWS SAM infrastructure
└── autodeploy.sh # One-command deployment
- Python 3.x (Lambda handler + seed script)
- AWS SAM CLI (build/deploy)
- AWS CLI (S3 sync + CloudFront invalidation)
./autodeploy.shcd terraform
terraform init
terraform apply -var-file="environments/prod/terraform.tfvars" -var="admin_key=$ADMIN_KEY"See terraform/README.md for detailed Terraform usage.
This will:
- Build and deploy Lambda backend (SAM)
- Sync frontend to S3
- Invalidate CloudFront cache (if configured)
python3 scripts/seed.py scrumble-data- Open
app/index.htmlin browser for frontend - Set API URL in
app/config.js(window.SCRUMBLE_API_BASE) - Admin panel:
app/admin/index.html(orhttps://scrumble.cc/admin) - History:
app/history.html
- Dynamic matchup rendering (any number of active matchups)
- Vote counts hidden until you vote (encourages engagement)
- Auto-scroll to next unvoted matchup
- Vote state persists in localStorage
- Countdown timers for each matchup
- Submission form with confirmation summary
- Optional email and matchup rationale
- Login-gated admin UI at
/admin - Card-based layout with real-time stats
- Inline editing for start/end times, cadence, and messages
- Quick extend buttons (+1d, +7d, +14d)
- Reset votes with confirmation
- View user submissions
- Responsive navbar with hamburger menu
- Pages: Vote, Submit, History, About, Jobs
Frontend: Vanilla JS, CSS (dark theme with gold accents)
Backend: Python 3.12, AWS Lambda Function URL (no API Gateway)
Database: DynamoDB
Deployment: AWS SAM, S3, CloudFront
Infrastructure as Code: Terraform (see terraform/ directory)
CDN: CloudFront (Distribution: E2F6VQWXTCO8OB)
GET /matchup- Get active matchupsPOST /vote- Cast a voteGET /history- Get past matchupsPOST /submit- Submit matchup suggestion
POST /admin/login- Validate admin keyGET /admin/matchups- List active matchups (ignores time window)GET /admin/submissions- List submissionsPATCH /admin/matchup/:id- Update matchup (starts_at, ends_at, cadence, message, active)POST /admin/matchup/:id/reset-votes- Reset votes to 0POST /admin/activate- Activate a matchupPOST /admin/matchup- Create new matchup
S3 Bucket: scrumble.cc
CloudFront: E2F6VQWXTCO8OB
DynamoDB Table: scrumble-data
Admin Key: Set via SAM deploy prompts; admin UI stores it in sessionStorage
CloudFront Invalidation: set SCRUMBLE_CF_DISTRIBUTION_ID for ./autodeploy.sh
SCRUMBLE_BUCKET(default:scrumble.cc) for./autodeploy.shSCRUMBLE_CF_DISTRIBUTION_ID(default:E2F6VQWXTCO8OB) for./autodeploy.sh
Scrumble now includes a production-style IaC/ops scaffold:
- Terraform environment separation (
terraform/environments/dev,terraform/environments/prod) - Remote state backend templates (
backend.hcl.example) - OIDC least-privilege bootstrap module (
terraform/bootstrap/oidc) - CloudWatch alarms + dashboard module (
terraform/modules/monitoring) - CI Terraform deploy path (
.github/workflows/terraform-infra.yml) - Rollback and first-deploy runbooks (
runbooks/rollback-terraform.md,docs/ops/terraform-first-deploy.md)
Prod note:
- Existing SAM-created prod resources are now adopted into Terraform state (DynamoDB + Lambda).
terraform/environments/prod/terraform.tfvarsis pinned to the real Lambda physical name to avoid import/apply failures.
- Workflow:
.github/workflows/ci.yml - Runs on PRs and pushes to
main - Performs:
- Python compile checks (
backend/,scripts/,list_all_data.py) - SAM template validation (
sam validate) - Deploy script executable checks
- Python compile checks (
- Workflow:
.github/workflows/deploy.yml - Trigger: manual only (
workflow_dispatch) - Default: no frontend deploy and ops monitoring disabled
- This prevents unattended deploys and surprise AWS spend.
Required GitHub secrets for deploy:
AWS_DEPLOY_ROLE_ARN(OIDC role for GitHub Actions)ADMIN_KEY(SAM parameter)
Optional deploy secrets:
SCRUMBLE_BUCKET(required only whendeploy_frontend=true)SCRUMBLE_CF_DISTRIBUTION_ID(optional invalidation)OPS_ALARM_EMAIL(optional SNS email subscription)
Monitoring resources are defined in template.yaml and are disabled by default with:
EnableOpsMonitoring=false
When enabled, stack creates:
- SNS topic:
scrumble-ops-alerts - CloudWatch alarms:
scrumble-lambda-errorsscrumble-lambda-p95-duration-highscrumble-lambda-throttles
- CloudWatch dashboard:
scrumble-ops
To enable manually:
sam deploy --parameter-overrides AdminKey=... EnableOpsMonitoring=true OpsAlarmEmail=you@example.com- Incident response runbook:
runbooks/incident-response.md - High error rate runbook:
runbooks/high-error-rate.md - Sample postmortem:
docs/postmortems/2026-02-11-image-ingest-cost-spike.md - Ansible config-management artifact:
ops/ansible/
- Vote boosting: Base 150 + time-based variance for demo
- Fingerprinting via localStorage for vote tracking
- Mayor matchup has red/white/blue patriotic theme
- EVA theme easter egg (NERV MODE button) - anime styling
- Preview overrides live in
app/config.js(window.SCRUMBLE_PREVIEW_OVERRIDES)