Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions .github/workflows/region-pages-terraform-drift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: region-pages Terraform Drift

# Enforces the zero-drift rule for the region-pages GCP project: the committed
# Terraform under apps/region-pages/infra/terraform/cloud-run is the source of
# truth. This workflow runs `terraform plan` and fails if live infrastructure
# has drifted from the code β€” daily (catch console drift) and on every PR that
# touches the Terraform (catch drift before merge).
#
# Auth is keyless via the F3-Nation org Workload Identity pool (vars.WIF_PROVIDER,
# the same provider the deploy-* workflows use), impersonating the read-only
# github-actions-deploy@region-pages SA created in ci.tf.

on:
schedule:
- cron: "0 13 * * *" # daily at 13:00 UTC (~7-8am Central)
pull_request:
paths:
- "apps/region-pages/infra/terraform/cloud-run/**"
- ".github/workflows/region-pages-terraform-drift.yml"
workflow_dispatch:

concurrency:
group: region-pages-tf-drift
cancel-in-progress: false

permissions:
contents: read
id-token: write
pull-requests: write

jobs:
drift:
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/region-pages/infra/terraform/cloud-run
env:
# Plan only β€” never applied. secret_data and the image tag are ignored by
# the config (see secrets.tf / main.tf lifecycle blocks), so placeholders
# produce no diff and the workflow never touches real secret values.
TF_VAR_image: "ci-plan-placeholder"
TF_VAR_secret_values: >-
{"postgres-url":"x","f3-data-warehouse-url":"x","cloud-sql-warehouse-connection-name":"x","warehouse-db-user":"x","warehouse-db-password":"x","warehouse-db-name":"x","cron-secret":"x","slack-bot-auth-token":"x","slack-channel-id":"x"}
steps:
- uses: actions/checkout@v4

- name: Authenticate to GCP (Workload Identity Federation)
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ vars.WIF_PROVIDER }}
service_account: github-actions-deploy@region-pages.iam.gserviceaccount.com

- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2

- name: Set up Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.15.3"

- name: Terraform init
run: terraform init -input=false

- name: Terraform plan (detect drift)
id: plan
run: |
set +e
terraform plan -input=false -lock=false -no-color -detailed-exitcode
code=$?
set -e
echo "exitcode=$code" >> "$GITHUB_OUTPUT"
# 0 = in sync, 2 = drift detected, 1 = error
if [ "$code" = "1" ]; then
echo "::error::terraform plan failed."
exit 1
elif [ "$code" = "2" ]; then
echo "::error::Drift detected β€” region-pages infrastructure no longer matches Terraform. Reconcile by applying the committed config or importing the change."
exit 2
else
echo "No drift β€” region-pages matches Terraform."
fi

- name: Comment drift result on PR
if: github.event_name == 'pull_request' && always()
uses: actions/github-script@v7
with:
script: |
const code = '${{ steps.plan.outputs.exitcode }}';
const body = code === '0'
? 'βœ… **region-pages Terraform drift check:** in sync β€” live infrastructure matches the committed config.'
: code === '2'
? '⚠️ **region-pages Terraform drift check:** drift detected β€” `terraform plan` shows changes. Reconcile before merge (apply the committed config or import the out-of-band change).'
: '❌ **region-pages Terraform drift check:** `terraform plan` errored. See the job logs.';
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,4 @@ pr.md
.eslintcache

# claude code
.claude/worktrees/
.claude/worktrees/.secrets/
10 changes: 5 additions & 5 deletions apps/map/__tests__/setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { render } from "@testing-library/react";
import { vi } from "vitest";

// Must be set before vitest-canvas-mock is imported
// because jest-canvas-mock accesses the jest global during initialization
declare global {
var jest: typeof vi;
}
globalThis.jest = vi;
// because jest-canvas-mock accesses the jest global during initialization.
// Assign via an untyped view of globalThis so this does not redeclare the
// global `jest` (which collides with @types/jest once it is present anywhere
// in the workspace, e.g. apps/region-pages).
(globalThis as Record<string, unknown>).jest = vi;

import "@testing-library/jest-dom";
import "vitest-canvas-mock";
Expand Down
16 changes: 16 additions & 0 deletions apps/region-pages/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
node_modules
.next
.git
.github
.env
.env.*
!.env.example
npm-debug.log*
firebase-debug.log*
firebase-debug.*.log
.firebase
infra
coverage
.DS_Store
*.local
.secrets
40 changes: 40 additions & 0 deletions apps/region-pages/.env.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# =============================================================================
# F3 Region Pages β€” Local Development Environment (example)
# =============================================================================
# Copy to .env.local and fill in values. Aligns with the repo-wide local dev
# stack (root docker-compose.yml): Postgres on :5433. Start it first:
#
# docker compose up -d # from the monorepo root
# pnpm --filter f3-region-pages db:setup:local # migrate + seed
#
# env.ts loads .env.${NODE_ENV} (NODE_ENV defaults to "local" for db tooling;
# Next.js also auto-loads .env.local in dev).

# -- App database (region-pages' own schema; shared local Postgres) -----------
# Its Drizzle migrations create the region-pages tables in this database.
POSTGRES_URL=postgresql://f3local:f3local@localhost:5433/f3nation

# -- F3 data warehouse (read-only source the ingest reads from) ---------------
# `direct` mode requires a real warehouse connection string (from GCP Secret
# Manager / a read replica). Point it at the warehouse you have access to.
WAREHOUSE_DB_CONNECTION_MODE=direct
F3_DATA_WAREHOUSE_URL=postgresql://USER:PASS@WAREHOUSE_HOST:5432/f3data

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This .env.local.example file is used to create the local dev's .env file for this app. Could this URL be changed to point to a valid PG DB that's running locally in Docker?


# -- Ingest cron endpoint (POST /api/ingest) ----------------------------------
CRON_SECRET=local-dev-cron-secret-change-me

# -- Slack notifications (ingest success/failure/skip) β€” optional locally ------
SLACK_BOT_AUTH_TOKEN=xoxb-...
SLACK_CHANNEL_ID=

NODE_ENV=local

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm familiar with the usage of development, production, and test values here. Does this app use local for a particular use case?


# -- Canonical domain / SEO (production) --------------------------------------
# Base URL used for metadataBase, canonical tags, OpenGraph. Set to the canonical
# domain in prod (e.g. https://regions.f3nation.com); defaults to f3workouts.com.
# NEXT_PUBLIC_URL=https://regions.f3nation.com
#
# When set, src/middleware.ts 308-redirects any non-canonical host (e.g.
# f3regions.com, f3region.info) to this host, preserving path+query. Leave UNSET
# until regions.f3nation.com is cut over β€” middleware is inert without it.
# NEXT_PUBLIC_CANONICAL_HOST=regions.f3nation.com
51 changes: 51 additions & 0 deletions apps/region-pages/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*
!.env*.example

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

firebase-debug.log
pr.md.context/**/logs.json
.context/logs.json
pr.md
tsconfig.tsbuildinfo
pglite-debug.log
.vscode/
*.log
10 changes: 10 additions & 0 deletions apps/region-pages/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Ignore build directories
.next
node_modules
drizzle/migrations

# Ignore other common files
*.min.js
*.bundle.js
*.map
pnpm-lock.yaml
7 changes: 7 additions & 0 deletions apps/region-pages/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"tabWidth": 2,
"printWidth": 80
}
1 change: 1 addition & 0 deletions apps/region-pages/AGENTS.md
63 changes: 63 additions & 0 deletions apps/region-pages/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# CLAUDE.md β€” LLM Context Index

> **Meta: This file MUST stay <8kb, compressed, LLM-friendly (not human-friendly). When editing, preserve density. Strip prose, use terse notation. This is a machine-readable index, not documentation.**

## Stack

Next.js 15 / React 19 / TypeScript (strict, `@/*` = `./src/*`) / Drizzle ORM / PostgreSQL (Supabase) / TailwindCSS / Node 20.18.2 (`.nvmrc`)

## Commands

```
dev # localhost:3000
build # ~480 static pages
lint | format | typecheck | test | test:watch | test:coverage (70% threshold)
db:setup:local | db:migrate | db:push | db:seed | db:reset | db:generate:migration
```

## Architecture

- RSC default; `'use client'` only for interactivity
- Build-aware cache keys via `NEXT_BUILD_ID` β€” see `src/utils/fetchWorkoutLocations.ts` / `getBuildAwareCacheKey()`
- ISR via `generateStaticParams()` in `[regionSlug]/page.tsx`
- Chunk error recovery: `src/components/ChunkErrorRecovery.tsx`

## Key Files

```
drizzle/schema.ts # DB schema (regions, workouts, seedRuns)
drizzle/db.ts # DB instance
src/utils/fetchWorkoutLocations.ts # Central data fetch + caching
src/app/[regionSlug]/page.tsx # Region page (ISR)
src/app/api/ingest/route.ts # Cron ingest endpoint
src/components/ChunkErrorRecovery.tsx # Client chunk error recovery
```

## API: POST /api/ingest

Upstash QStash cron. Bearer `CRON_SECRET`. Idempotent (20h window). Prunes stale data, seeds regions/workouts from warehouse, enriches metadata. Slack notifications on all outcomes (success/failure/skip). See `.context/slack.md`.

```bash
curl -X POST http://localhost:3000/api/ingest -H "Authorization: Bearer $CRON_SECRET"
```

## Env (`.env.local.example`)

```
POSTGRES_URL # Supabase connection
F3_DATA_WAREHOUSE_URL # Warehouse connection
CRON_SECRET # Ingest auth
SLACK_BOT_AUTH_TOKEN # Bot OAuth token (xoxb-...)
SLACK_CHANNEL_ID # Notification channel
```

## Agent Skills (`.agents/skills/`)

- `vercel-react-best-practices` β€” 57 React/Next.js perf rules (8 categories). Use when writing/reviewing components.
- `vercel-composition-patterns` β€” Composition patterns for scalable components. Use for boolean-prop refactors, reusable APIs, React 19 patterns.

## Context Index (`.context/`)

| File | Topic |
| ---------- | ------------------------------------------------------------------------------------------------------- |
| `slack.md` | Slack bot integration: `sendSlackNotification`, env vars, notification triggers, warehouse Slack tables |
73 changes: 73 additions & 0 deletions apps/region-pages/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Contributing to F3 Region Pages

Thank you for your interest in contributing to the F3 Region Pages project! This guide will help you get started with the development environment setup and basic workflow.

## Prerequisites

- [Node.js](https://nodejs.org/) >= 24.14.1 (monorepo `engines.node`; see root `.nvmrc`)
- [pnpm](https://pnpm.io/) (the monorepo package manager)
- [Docker](https://www.docker.com/) (for the shared local database β€” root `docker-compose.yml`)
- [Docker](https://www.docker.com/) (for local database setup)

## Development Environment Setup

1. **Clone the repository**

```bash
git clone https://github.com/F3-Nation/f3-region-pages.git
cd f3-region-pages
```

2. **Set up Node.js environment**

```bash
nvm install # Installs the version specified in the root .nvmrc (24.14.1)
nvm use # Switches to the project's Node.js version
```

3. **Install development dependencies**

```bash
npm i -D # Shorthand for npm install --save-dev
```

4. **Set up local database**

```bash
npm run db:setup:local # Starts Supabase, sets up environment, and seeds database
```

5. **Start the development server**

```bash
npm run dev # Starts Next.js development server
```

The application will be available at [http://localhost:3000](http://localhost:3000)

## Available Scripts

- `npm run dev` - Start development server
- `npm run build` - Build for production
- `npm run test` - Run tests
- `npm run lint` - Run linting
- `pnpm db:setup:local` - One-shot local DB setup (starts the shared Postgres via root `docker compose`, migrates, seeds)
- `pnpm db:reset` - Reset the database
- `pnpm db:migrate` - Run database migrations
- `pnpm db:seed` - Seed the database with initial data

## Workflow

1. Create a new branch for your feature or bugfix
2. Make your changes
3. Write tests for your changes
4. Run tests and make sure they pass
5. Submit a pull request

## Code Style and Guidelines

This project follows the Next.js conventions and uses TypeScript. Please ensure your code is properly typed and follows the existing patterns in the codebase.

## Need Help?

If you have any questions or need help, please open an issue on the GitHub repository.
Loading
Loading