Select the types of activity you want to include in your feed.
Remove gymtracker-ads-admin.html and orbyt-icon.png: Delete the admin UI HTML file and the associated icon image to streamline the project and update links to the new admin interface.
···11+# Cloudflare Access Setup for Gym Tracker Ads Admin
22+33+This guide configures Cloudflare Access so you sign in with Google (or another provider) instead of using an API key.
44+55+## Prerequisites
66+77+- Cloudflare Zero Trust (free tier is fine)
88+- Your domain `jackhannon.net` on Cloudflare with the zone active
99+1010+## Step 1: Enable Zero Trust
1111+1212+1. Go to [Cloudflare Dashboard](https://dash.cloudflare.com) → **Zero Trust**
1313+2. If prompted, create a team (e.g. `jackhannon` — you'll get a `*.cloudflareaccess.com` subdomain)
1414+1515+## Step 2: Create an Access Application for the Admin
1616+1717+**Important:** Both `/admin` and `/api/admin` must be protected. If only `/admin` is protected, unauthenticated requests can hit `/api/admin/ads` directly.
1818+1919+1. In Zero Trust: **Access** → **Applications** → **Add an application**
2020+2. Choose **Self-hosted**
2121+3. Configure:
2222+ - **Application name**: `Gym Tracker Ads Admin`
2323+ - **Session Duration**: 24 hours (or your preference)
2424+ - **Application domain**:
2525+ - **Subdomain**: `gymtracker`
2626+ - **Domain**: `jackhannon.net`
2727+ - **Path**: `/admin` (you will add `/api/admin` in Step 3)
2828+ - Click **Next**
2929+3030+4. Add a **Policy**:
3131+ - **Policy name**: `Require Google login`
3232+ - **Action**: **Allow**
3333+ - **Configure rules** → **Add include**:
3434+ - **Selector**: Emails ending in → `@jackhannon.net` (or use "Emails" and add your email)
3535+ - Or: **Login methods** → Add **Google**
3636+ - Click **Next**
3737+3838+5. **Protect the API** — You must also protect `/api/admin` (required, not optional):
3939+ - The admin UI at `/admin` fetches and saves ads via `/api/admin/ads`. If `/api/admin` is not protected, anyone can list and modify ads without signing in.
4040+4141+## Step 3: Protect `/api/admin` (required)
4242+4343+To protect the admin API so only signed-in users can fetch/save ads:
4444+4545+**Option A — Same application, broader path**
4646+4747+Edit the application you created. Change the **Path** to include both:
4848+- Path: `/admin`
4949+- Add another path: `/api/admin`
5050+5151+(Cloudflare Access lets you add multiple paths in one application.)
5252+5353+**Option B — Second application**
5454+5555+1. **Add an application** → Self-hosted
5656+2. **Application domain**:
5757+ - Subdomain: `gymtracker`
5858+ - Domain: `jackhannon.net`
5959+ - Path: `/api/admin`
6060+3. Use the same policy as above (e.g. Google login).
6161+6262+## Step 4: Ensure Public API Stays Open
6363+6464+The public endpoint `GET https://gymtracker.jackhannon.net/api/ads` must **not** require Access. The VT Gym Tracker app fetches ads without auth.
6565+6666+- If you only protect `/admin` and `/api/admin`, `/api/ads` stays public.
6767+- If you protect `/api/*`, add a **Bypass** policy that runs first:
6868+ - Policy: **Bypass**
6969+ - Include: **Everyone**
7070+ - Path: `/api/ads` (exact)
7171+7272+## Step 5: Add an Identity Provider (Google)
7373+7474+1. Zero Trust → **Settings** → **Authentication**
7575+2. **Login methods** → **Add new**
7676+3. Choose **Google**
7777+4. Follow the prompts (create OAuth credentials in Google Cloud Console if needed)
7878+7979+## Step 6: Verify Access Path Coverage
8080+8181+Before testing, confirm in Zero Trust that both paths are protected:
8282+8383+- [ ] `/admin` — Admin UI page
8484+- [ ] `/api/admin` — Admin API (includes `/api/admin/ads`, `/api/admin/stats`)
8585+8686+If you use a single application with multiple paths, ensure both are included. If you use separate applications, ensure each has the correct path.
8787+8888+## Step 7: Test
8989+9090+1. Visit **https://gymtracker.jackhannon.net/admin**
9191+2. You should see the Cloudflare Access login page
9292+3. Sign in with Google
9393+4. You should land on the admin UI with no API key needed
9494+5. Click **Refresh** — it should load your ads
9595+6. Edit and save — it should work without an API key
9696+9797+## Troubleshooting
9898+9999+- **401 Unauthorized** on `/admin`: Access may not be protecting that path yet, or the JWT isn’t being sent. Confirm the Access application path matches `/admin` and `/api/admin`.
100100+- **CORS errors**: The Worker allows `gymtracker.jackhannon.net`. If you use another origin, add it to `ALLOWED_ORIGINS` in the Worker.
101101+- **Public API blocked**: Ensure `/api/ads` is not covered by a “Require” policy, or add a Bypass policy for it.
102102+103103+## Deploy After Setup
104104+105105+After configuring Access:
106106+107107+```bash
108108+npm run deploy
109109+```
110110+111111+The Worker serves `/admin` and `/api/admin/ads` and checks for the `Cf-Access-Jwt-Assertion` header. Cloudflare adds this header when the request has passed Access, so no API key is required.
+54-7
workers/gymtracker-ads-api/README.md
···66771. **Install dependencies:** `npm install`
8899-2. **Set admin API key** (required for PUT):
99+2. **Set admin API key** (required for PUT and schedule GET):
1010 ```bash
1111 npx wrangler secret put ADMIN_API_KEY
1212 ```
···141415153. **Deploy:** `npm run deploy`
16161717+4. **(Optional) PostHog analytics** — To show ad impressions, clicks, and CTR in the Overview:
1818+ - Create a [Personal API key](https://us.posthog.com/settings/user-api-keys#personal-api-keys) with `query:read` scope
1919+ - `npx wrangler secret put POSTHOG_PERSONAL_API_KEY`
2020+ - Project ID and host are in `wrangler.jsonc` vars; override via `POSTHOG_PROJECT_ID` / `POSTHOG_HOST` if needed
2121+ - The app must send `ad_impression` and `ad_tap` events to PostHog
2222+2323+## Initial KV State
2424+2525+Before any ad has been PUT, GET returns `404` with `{ "error": "No active ad" }`. The app handles this gracefully (shows nothing). After deploy, use the Admin UI or `curl` to add your first ad.
2626+1727## Endpoints
18281929| Method | Path | Auth | Description |
···2333| PUT | /api/ads | X-API-Key header | Upsert ad config (by id) |
2434| OPTIONS | /api/ads | None | CORS preflight |
25352626-## Scheduling
3636+The API filters ads by `start_at` and `end_at` on the server. Multiple ads can be scheduled; the public GET returns the one active "now". Legacy single-ad config is auto-migrated on first request.
3737+3838+## Schema (AdConfig)
3939+4040+| Field | Type | Required | Notes |
4141+|-------|------|----------|-------|
4242+| `id` | string | yes | Unique; e.g. `sponsor-2025-q1` |
4343+| `active` | boolean | yes | Whether ad can be shown |
4444+| `sponsor` | string | yes | Non-empty |
4545+| `headline` | string | yes | Non-empty |
4646+| `subline` | string \| null | no | Optional |
4747+| `cta` | string | yes | Call-to-action text |
4848+| `destination_url` | string | yes | Must be HTTPS |
4949+| `image_url` | string \| null | tier-dependent | Required for `banner` and `feature`; must be HTTPS |
5050+| `logo_url` | string \| null | no | Optional; if set, must be HTTPS |
5151+| `creative_version` | string | no | Defaults to `""` |
5252+| `placement` | string | no | Defaults to `"home_feed"` |
5353+| `start_at` | string | no | ISO date string; start of active window |
5454+| `end_at` | string | no | ISO date string; end of active window |
5555+| `tier` | `"text" \| "banner" \| "feature"` | no | Defaults to `"banner"` (normalized to lowercase) |
5656+5757+**Tier rules:** `text` — `image_url` optional. `banner` / `feature` — `image_url` required, valid HTTPS.
5858+5959+**Active logic:** An ad is active only if `active` is true, `now >= start_at` (if set), and `now <= end_at` (if set). If multiple ads are active, the one with the latest `start_at` is returned.
6060+6161+Also see [ad-config-schema.md](https://github.com/Hann8n/VTGymTracker/blob/main/docs/ad-config-schema.md) in the VT Gym Tracker repo.
27622828-- Use `start_at` and `end_at` (ISO8601) to schedule when an ad is live.
2929-- Multiple ads can be scheduled; the API returns the one active "now" on public GET.
3030-- Ads without dates are always eligible (if active). Overlapping windows: most recently started wins.
6363+## Seed First Config
31643232-## Admin UI
6565+After deploy, add your first ad via Admin UI or `curl`:
33663434-Manage ads at https://jackhannon.net/gymtracker-ads-admin.html
6767+```bash
6868+# Placeholder (inactive) — GET returns 404 until you activate
6969+curl -X PUT https://gymtracker.jackhannon.net/api/ads \
7070+ -H "Content-Type: application/json" \
7171+ -H "X-API-Key: YOUR_ADMIN_API_KEY" \
7272+ -d @seed-ad.json
7373+7474+# Test ad (active) — use for end-to-end verification
7575+curl -X PUT https://gymtracker.jackhannon.net/api/ads \
7676+ -H "Content-Type: application/json" \
7777+ -H "X-API-Key: YOUR_ADMIN_API_KEY" \
7878+ -d @seed-ad-active.json
7979+```
8080+8181+**Admin UI:** Visit [https://gymtracker.jackhannon.net/admin](https://gymtracker.jackhannon.net/admin) — sign in with Cloudflare Access (Google or your configured provider). See [ACCESS_SETUP.md](ACCESS_SETUP.md) to configure.