# CC Soccer D11 - Session Handoff
**Date:** May 19, 2026
**Branch:** `main`

---

## Current State

### 🎉 SITE IS LIVE at ccsoccer.com
Soft-launch mode (IP whitelist active). Board members beta testing. Real users will be invited once seasonal registration is set up.

### Code ✅ LOCAL AHEAD OF TEST/PROD
Two changes committed locally, need deploying (see deploy steps below):
1. Cache invalidation fix in `ccsoccer.module`
2. Module updates via composer (`composer.lock` changed)

---

## ⚠️ NEXT DEPLOY NEEDED

`composer.lock` has changed — must run `composer install` on both servers after deploying:

```bash
ssh ccsoccer

# TEST
ccsDeploy && ccsUpdb && ccsCr
PATH=/opt/cpanel/ea-php83/root/usr/bin:$PATH /opt/cpanel/ea-php83/root/usr/bin/php /opt/cpanel/composer/bin/composer install --ignore-platform-req=ext-intl

# PROD
ccsProdDeploy && ccsProdUpdb && ccsProdCr
PATH=/opt/cpanel/ea-php83/root/usr/bin:$PATH /opt/cpanel/ea-php83/root/usr/bin/php /opt/cpanel/composer/bin/composer install --ignore-platform-req=ext-intl
```

---

## Session Work — May 19–20, 2026

### Custom 403/404 Error Pages ✅ (locally, needs deploy)
**What:** Branded `page--system-403.html.twig` and `page--system-404.html.twig` in `ccsoccer_theme` replacing the stock Drupal error pages. Dark hero matching the homepage welcome section, with header-nav-style CTAs (plain text links + red pill for the primary action). 403 branches on `logged_in`: anonymous users see Sign in / Reset password / Back to home; authenticated users see Back to home / Contact us. 404 offers Back to home / Teams / Schedule / Register.

**How:** Drupal core does not emit `page__system_4xx` theme suggestions automatically, so added `hook_theme_suggestions_page_alter()` in `ccsoccer_theme.theme` that reads the request's HTTP exception and appends the suggestion. Also intentionally did NOT render `{{ page.content }}` inside the error templates — that variable holds Drupal's default "Page not found / requested page could not be found" markup plus a breadcrumb that would duplicate (and contradict) the custom hero.

**Files changed:**
- `web/themes/custom/ccsoccer_theme/ccsoccer_theme.theme` (new theme suggestions hook)
- `web/themes/custom/ccsoccer_theme/templates/page--system-403.html.twig` (new)
- `web/themes/custom/ccsoccer_theme/templates/page--system-404.html.twig` (new)
- `web/themes/custom/ccsoccer_theme/css/layout.css` (new `.error-section` block)

**Verify on test/prod after deploy:** hit any bogus URL for 404; hit `/admin` while logged out for 403. Theme-only change — no `composer install`, `updb`, or `cim` needed.

### Cache Invalidation Fix ✅ (locally, needs deploy)
**Problem:** Authenticated users (logged-in) would not see season/tournament changes on `/register` until `drush cr`. Anonymous users saw updates immediately.

**Root cause:** Drupal only fires `entity_list:season` cache tag on insert/delete, not on update. Editing a season (e.g. changing reg dates) only fires `season:{id}`, but if that season wasn't previously in the query results, its tag wasn't in the cached render array — so nothing busted.

**Fix:** Added `Cache::invalidateTags(['entity_list:season'])` to `ccsoccer_season_update()` and `Cache::invalidateTags(['entity_list:tournament'])` to `ccsoccer_tournament_update()`. Also removed the old `cache('render')->invalidateAll()` which wasn't reaching the dynamic page cache bin anyway.

**File changed:** `web/modules/custom/ccsoccer/ccsoccer.module`

**Verified:** Works locally and on TEST. Rey (17rey17 test account) confirmed summer coed season visible after fix.

**Note:** If you're not seeing a season on `/register` and you're logged in, first check if you're already registered for it — registered seasons are intentionally hidden from the list.

### Module Updates ✅ (locally, needs deploy)
Updated via `composer update` locally:
- Drupal core 11.3.8 → 11.3.9
- Commerce Core 3.3.3 → 3.3.5
- Commerce Authorize.Net 8.x-1.13 → 8.x-1.14
- Masquerade 8.x-2.1 → 8.x-2.2
- Views data export 8.x-1.8 → 8.x-1.10

**WebAuthn (Passkey) 2.0.0-rc7 → 2.1.0-beta1 — HELD** — version stage jump (rc → beta), touches auth. Update separately with care and test passkey login flow after.

### SMS Notifications — No Changes Needed ✅
Spot-checked registration confirmation SMS on TEST — reads cleanly as standalone text. Jersey purchase texts also fine.

**Open question (get board/beta feedback):** Registration confirmation SMS includes a URL at the end (`/register`), which triggers an iMessage link preview card. Should the URL be dropped since the email has full details? Also: URL points to `/register` (available registrations) not `/my-registrations` — fix the route if keeping it.

### Email — Order Confirmation (notes for next session) 📋
Tested registration confirmation email on TEST — looks good overall. Items to fix next session:
1. Subject line — add season name (e.g. "Registration Confirmation - COED Late Summer 2026")
2. Heading font color — salmon/pink, should be proper red
3. Contact email — `info@ccsoccer.com` → `ccsoccer@ccsoccer.com`
4. "Manage your registrations" copy — expand to mention creating or joining groups
5. Logo image broken on TEST (shows `?` box) — investigate

**File:** `web/modules/custom/ccsoccer/src/EventSubscriber/OrderCompleteSubscriber.php`

---

## Next Steps (Priority Order)

### Immediate — Deploy
1. Deploy to TEST (`ccsDeploy && ccsUpdb && ccsCr` + composer install)
2. Verify cache fix works on TEST (edit a season, confirm it shows immediately for logged-in user)
3. Deploy to prod (`ccsProdDeploy && ccsProdUpdb && ccsProdCr` + composer install)

### This Weekend
4. **Order confirmation email fixes** (subject, color, contact email, copy) — see notes above
5. **Welcome email design conversation** — three related pieces:
   - Drupal core "welcome" email at `/admin/config/people/accounts`
   - First-time vs returning registrant confirmation content
   - Bulk welcome email to 1,727 migrated users (direct to `/user/password`)
6. **Audit NotificationService emails** — asterisk → `<strong>`, green → red, line breaks, tone

### Before Opening Whitelist to Public
7. **Passkey + reCAPTCHA test** — ask Haley/Myk/Kyle to test on their devices
8. **Welcome/bulk email** to all migrated users
9. **Create real SLO Friendly 2026 season + tournament** — Andrew
10. **Verify Commerce checkout end-to-end** with real payment

### Mid-term
11. Geo/country blocking via Cloudflare
12. `test@ccsoccer.com` mailbox decision
13. Andrew's local environment DB update
14. SPF/DKIM email authentication setup (emails occasionally slow/filtered for some providers)
15. `slofriendly` role reference cleanup
16. Fix admin-only "Send aborted by hook_mail()" error on order completion
17. WebAuthn passkey update (2.0.0-rc7 → 2.1.0-beta1) — do carefully, test auth flow

### Post-Launch / Ongoing
18. Verify credit balances against D7
19. Check registrations/credits issued after April 16 dump date
20. Remove `beta_tester` role from all users
21. slofriendlysoccer.com URL forward
22. Archive/delete D7 waivers directory from server
23. Eventually delete `ccsoccer_site_d7_archive/`

### Development (Deferred)
24. Three-tier button methodology pass
25. CSS consolidation — design tokens
26. Team handling refactor
27. Profile picture migration — board decision pending
28. ~~Custom 403/404 error pages~~ ✅ Done — see Session Work (May 20)
29. EPL team names (Andrew)

---

## DB Quick Reference

### Production DB (live)
- DB: `n6ac4b5_d11live`
- User: `n6ac4b5_ccsoccer_user`
- Password: `vGL3KWO(K8C;`

### TEST DB
- DB: `n6ac4b5_d11test`
- User: `n6ac4b5_ccsoccer_user`
- Password: `vGL3KWO(K8C;`

### D7 archive DB
- DB: `n6ac4b5_ccsoccer`
- User: `n6ac4b5_ccsoccer_user`
- Password: `vGL3KWO(K8C;`

### Local D11 DB
- Admin: `admin` / `TJ4XxyYGCd`

---

## Server Directory Structure
```
/home/n6ac4b5/public_html/
  ccsoccer_site/            ← PRODUCTION (ccsoccer.com)
  ccsoccer_site_d7_archive/ ← D7 archive (rollback if needed)
  test_ccsoccer_site/       ← TEST (test.ccsoccer.com)
  slofriendly_redirect/     ← slofriendlysoccer.com redirect
```

---

## Email Architecture

### Pipeline
```
NotificationService / OrderCompleteSubscriber
  → Symfony Mailer pipeline
    → Inline CSS + Theme wrapper (email.html.twig)
    → URL to absolute + Wrap and convert
  → Google Workspace SMTP (TEST + prod)
  → Mailpit (local DDEV only)
```

### Order Email Flow
- Season/tournament order → `sendRegistrationConfirmation()` (red branded)
- Jersey-only order → `sendJerseyOrderConfirmation()` (red branded)
- Commerce default "Order #X confirmed" → suppressed via `hook_mail_alter()`

### SMTP config (in settings.local.php, never in git)
- Host: `smtp.gmail.com`, Port: `465`, TLS: true
- User: `ccsoccer@ccsoccer.com`
- App password: `sqygkfykzwrziota`

### Notification gating
- `site_instance = 'local'` → board members only (Mailpit catches all)
- `site_instance = 'test'` → board members only (real emails)
- `site_instance = 'production'` → all users get real emails

### reCAPTCHA Keys
- Site key: `6LchguosAAAAC5kLFmKj0xCGdEWXNntLacANpVN`
- Secret key: `6LchguosAAAANBJ8ikcrZvYQMxs6-FM2YHbzPEl`
- Domains: ccsoccer.com, test.ccsoccer.com

---

## Key Facts / Gotchas

### beta_tester role — do not delete from config/sync
Always `git checkout config/sync/user.role.beta_tester.yml` after `drush cex`.

### Google SMTP App Password
`sqygkfykzwrziota` — in `settings.local.php` on TEST and prod servers only.

### Symfony Mailer — User override
Do NOT enable the "User" override — replaces HTML with plain text.

### Symfony Mailer — email template
`web/themes/custom/ccsoccer_theme/templates/email/email.html.twig`

### DB import fix for InMotion
```bash
gunzip -c dump.sql.gz | sed 's/DEFINER=[^*]*\*/\*/' | gzip > dump-clean.sql.gz
```

### Two-file CSS sync
- `web/modules/custom/ccsoccer/css/ccsoccer-base.css` (admin)
- `web/themes/custom/ccsoccer_theme/css/base.css` (public)

### composer install on servers
When `composer.lock` changes locally, must run on each server after deploying:
```bash
PATH=/opt/cpanel/ea-php83/root/usr/bin:$PATH /opt/cpanel/ea-php83/root/usr/bin/php /opt/cpanel/composer/bin/composer install --ignore-platform-req=ext-intl
```

---

## Server Quick Reference
```bash
# SSH in
ssh ccsoccer

# TEST deploy
ccsDeploy && ccsCr

# PROD deploy
ccsProdDeploy && ccsProdCr

# Full deploy with DB updates
ccsDeploy && ccsUpdb && ccsCim && ccsCr
ccsProdDeploy && ccsProdUpdb && ccsProdCim && ccsProdCr

# General
ll    # ls -la
..    # cd ..
```

## .htaccess — both servers
Not in git — protected via `git update-index --skip-worktree web/.htaccess`
Whitelist IPs: Caleb home, Andrew, Dave Farris, Haley, Creighton, Caleb TX, Sean, Yong, Blair, Chris R work, Kyle, Myk.

## Git Workflow
- Always `git pull` before `git push`
- `main` is the primary branch
- `settings.local.php` is NOT in git
- Always update SESSION_HANDOFF.md as part of every commit
- Always `git checkout config/sync/user.role.beta_tester.yml` after `drush cex`

---

## Andrew's Pre-Go-Live Checklist

**Status:** Draft assembled by Andrew on May 19, 2026 from a sweep across this handoff, the April 25 → May 16 archive sessions, MIGRATION_STEPS.md, MIGRATION_STRATEGY.md, the March 16 security assessment, and DEPLOYMENT_GUIDE.md. **Caleb and Andrew to review together next session** and reconcile against the existing "Next Steps" list above.

Items are grouped by urgency, not size. Some items already appear in "Next Steps" above with different priority — that mismatch is intentional and is the main thing the next session should reconcile.

### Group 1 — Hard blockers (before lifting the IP whitelist)

1. **Geo/country blocking via Cloudflare — restrict to United States.** Currently filed as "Mid-term" item 11 above; Andrew's read is that this belongs here. With ~1,727 migrated users and a US-only league, blocking non-US traffic at the edge cuts off the bulk of automated abuse, credential-stuffing, and scraping before it touches InMotion.
2. **Passkey + reCAPTCHA cross-device test** — Haley, Myk, Kyle, plus an Android tester if possible. Already item 7 in "Before Opening Whitelist." The vanishing-CAPTCHA bug (below) makes this doubly important.
3. **Resolve the Passkey RP ID question.** Passkeys are domain-bound. Any passkey registered against `test.ccsoccer.com` will not work on `ccsoccer.com` unless `drupal/wa` is configured with the RP ID set to `ccsoccer.com` *before* users enroll. Flagged as a pending decision in the May 6 and May 13 archives.
4. **End-to-end Commerce checkout with a real card on prod.** Season registration path, jersey-only path, confirm Authorize.Net charges the live gateway, confirm the order confirmation email lands, confirm My Account reflects the order.
5. **Create the real SLO Friendly 2026 season + tournament** — Andrew. Already item 9 above.
6. **Bulk welcome / password-reset email to the 1,727 migrated users.** All migrated users have random passwords — they cannot log in until they receive a `/user/password` link. Without this, the site is effectively locked even after the whitelist comes off.
7. **Remove the IP whitelist from `web/.htaccess`** on go-live day. The file is `skip-worktree` protected, so the edit happens directly on each server.
8. **Delete `ccsoccer-d11-migrated-200users.sql` from the repo root.** Still present (~23 MB) and contains real user data. Called out in the March 16 security assessment as "do before launch."
9. **Confirm `trusted_host_patterns`** on prod `settings.local.php` is set to `^ccsoccer\.com$` and `^www\.ccsoccer\.com$`. (Security assessment Finding #4.)
10. **Confirm Devel is uninstalled on prod** (`drush pm:uninstall devel devel_generate`) and `development.services.yml` is not loaded. (Security assessment Findings #9, #10.)
11. **Confirm `hash_salt`, `update_free_access = FALSE`, and `error_level = 'hide'`** are set in prod `settings.local.php`. (DEPLOYMENT_GUIDE.md Production Security Checklist; security assessment Finding #3.)

### Group 2 — Strongly recommended before opening to the public

Not strict blockers, but high-leverage. Anything missing here on day 1 will show up as user pain or admin firefighting.

12. **SPF / DKIM / DMARC for `ccsoccer.com`** through Google Workspace SMTP. The May 13 session noted password reset emails going to spam; current item 14 still flags "emails occasionally slow/filtered." The May 16 session removed the old AuthSMTP DKIM and PowerDMARC records — the Google Workspace DKIM/DMARC for the new SMTP path needs to actually be added. Without alignment, a large fraction of the 1,727 welcome emails will land in spam.
13. **Fix the vanishing-CAPTCHA login bug.** See the dedicated section below. New users hitting this on day 1 will think the site is broken.
14. **Confirm `www` vs non-`www` canonical redirect** and **HTTPS enforcement** at the Apache / cPanel layer.
15. **Enable CSS/JS aggregation** in Drupal Performance settings. Off by default, big first-paint win.
16. ~~Custom 403 / 404 pages.~~ ✅ Done May 20 — see Session Work. Branded templates shipped in `ccsoccer_theme`; deploy with the next push.
17. **Cron configured on prod** via cPanel Cron Jobs, every 15–30 min. Without it, notifications and scheduled cache work silently stop.
18. **Backup strategy.** Verify InMotion's nightly backup is enabled, plus a periodic off-server snapshot of `n6ac4b5_d11live`.
19. **Order confirmation email fixes** (subject includes season name, heading → proper red, contact email → `ccsoccer@ccsoccer.com`, logo image fix, copy expansion). Already item 4 in "This Weekend."
20. **NotificationService email audit** — asterisks → `<strong>`, green → red, line breaks, tone. Already item 6.
21. **`slofriendly` config role reference cleanup** — currently emits a `Config user.role.slofriendly not found` warning on `cim`. Non-breaking but called out in multiple sessions as "worth cleaning up before launch."
22. **Fix the admin-only "Send aborted by hook_mail()" error on order completion** — already item 16.
23. **CSP headers in report-only mode.** Security assessment Finding #14 — gives real-world telemetry before going to enforcing.
24. **Decision on `test@ccsoccer.com` mailbox** for the TEST server (item 12).

### Group 3 — First 24–72 hours after opening

These are already tracked in the post-launch list; restating here so they don't get lost in launch-day adrenaline.

25. **Assign `permanent_override` role** to Haley, Layne, Julie, Myk, Kyle.
26. **Verify credit balances against D7** for a sample of known users.
27. **Check credits/registrations issued in D7 after the April 16, 2026 dump date** — these are NOT in the migrated DB and need to be reconciled manually.
28. **Remove `beta_tester` role from all users** — gating disappears automatically once `site_instance = production`.
29. **WebAuthn passkey update** 2.0.0-rc7 → 2.1.0-beta1, carefully, with passkey login tested afterward.
30. **`slofriendlysoccer.com` URL forward.**
31. **Archive / delete D7 waivers directory** from the server; eventually delete `ccsoccer_site_d7_archive/`.
32. **Monitor login success rate, password reset request volume, and unhandled exceptions** in dblog and the Symfony Mailer error log for the first 72 hours.

### Items Andrew thinks should be re-prioritized in "Next Steps" above

Three items currently filed as "Mid-term" or "Deferred" look to Andrew like they should move into "Before Opening Whitelist to Public":

- **Item 11 (Geo/country blocking via Cloudflare)** — strongest single defense once the IP whitelist comes off.
- **Item 14 (SPF/DKIM)** — without it, the bulk welcome email lands in spam for a large fraction of users and effectively breaks the launch.

(Item 28, custom 403/404 pages, was on this list as of May 19; shipped May 20.)

To discuss with Caleb.

---

## Vanishing CAPTCHA on Login

**Status:** Confirmed bug. Reproduced from screenshots by Andrew on May 19, 2026. Root cause identified in the `captcha` contrib module code. Fix proposed, not yet applied.

### Symptom

On the login form (`/user/login`), after a failed password attempt:

1. User submits username + wrong password. Page rerenders with two error messages — "Math question (5 + 0 =) field is required" and "Unrecognized username or password" — and shows the CAPTCHA box.
2. User fills in the CAPTCHA correctly and re-submits with another wrong password. Page rerenders with **only** "Unrecognized username or password" — the CAPTCHA box is **gone** from the form.
3. User tries again with no CAPTCHA visible. Page rerenders with "Math question (3 + 8 =) field is required" AND "Unrecognized username or password" — the CAPTCHA reappears with a different question.

User-facing effect: the third submission feels like the form is broken — it's asking for a field the user can't see.

### Root cause

Two interacting pieces of the `captcha` contrib module produce this:

**1. The persistence setting is `1` (install default).**

From `web/modules/contrib/captcha/config/install/captcha.settings.yml`:

```yaml
persistence: 1   # CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_INSTANCE
```

This is not currently overridden in `config/sync/` — `captcha.settings.yml` is not committed, and `captcha` / `recaptcha` aren't in `core.extension.yml` either. The site is running on the install default.

**2. After a correct CAPTCHA + wrong password, the rebuilt form omits the CAPTCHA.**

In `web/modules/contrib/captcha/captcha.module` around line 584, a successful CAPTCHA validation sets the session row to `SOLVED` and nulls the token:

```php
\Drupal::database()->update('captcha_sessions')
  ->condition('csid', $csid)
  ->fields([
    'status' => CaptchaConstants::CAPTCHA_STATUS_SOLVED,
    'token' => NULL,
  ])
  ...
```

Then `web/modules/contrib/captcha/captcha.inc` line 151 short-circuits the next render for that session:

```php
if ($captcha_session_status == CaptchaConstants::CAPTCHA_STATUS_SOLVED) {
  return FALSE;   // CAPTCHA NOT required → not rendered
}
```

So after the correct "5" answer in step 2, the rebuild in step 3 omits the CAPTCHA — the form looks like a plain login.

When the user then submits step 4, the previous CAPTCHA session's token is gone (nulled above). On the next form load the CAPTCHA element processor generates a **new** `captcha_sid` for a fresh session. That fresh session is **not** solved → `_captcha_required_for_user()` returns TRUE → the field is now required. But the new CAPTCHA was never shown on the previous render, so the user submits with it empty and gets a "field is required" error with a brand-new challenge.

### Side note worth flagging

The May 16 archive says "reCAPTCHA — enabled on login, password reset, registration forms," and ARCHITECTURE_DECISIONS.md commits to Google reCAPTCHA as the strategy. But the user-visible challenge on the login form is the **Math** challenge from the `captcha` module, not Google reCAPTCHA. The `recaptcha` module is enabled, but the login form's CAPTCHA point is set to `captcha/Math` (the install default). Worth verifying at `/admin/config/people/captcha/captcha-points` — the row for `user_login_form` likely says `captcha/Math` rather than `recaptcha/reCAPTCHA`.

### Proposed fix

Three small steps, all in admin UI plus a config export:

**A. Set persistence to "Always add a challenge."** At `/admin/config/people/captcha`, change the persistence radio from "Omit challenges in a multi-step/preview workflow…" to **"Always add a challenge."** That's `persistence: 0` (`CAPTCHA_PERSISTENCE_SHOW_ALWAYS`). CAPTCHA appears on every render — slightly more friction on retry but predictable, and the right setting for an auth form. This alone eliminates the vanishing behavior.

**B. Decide: Math or reCAPTCHA on the login form.** At `/admin/config/people/captcha/captcha-points`, either:

- Change `user_login_form` from `captcha/Math` → `recaptcha/reCAPTCHA` to match the documented architectural intent, **or**
- Document explicitly that Math is the deliberate choice (lighter, no Google round-trip, works without keys) and update ARCHITECTURE_DECISIONS.md accordingly.

**C. Export and commit the config.** After the changes:

```bash
ddev drush cex -y
git checkout config/sync/user.role.beta_tester.yml   # per the rule above
git add config/sync/captcha.* config/sync/core.extension.yml config/sync/recaptcha.*
git commit -m "Fix vanishing CAPTCHA on login: persistence=always, set login form challenge"
```

This pulls `captcha.settings.yml` (with `persistence: 0`), the captcha point for `user_login_form`, and the `captcha` / `recaptcha` rows in `core.extension.yml` into git so the fix is reproducible across environments.

### Verification

Easy to reproduce: bad password, fill CAPTCHA, bad password again, watch the form. After the fix the CAPTCHA should be visible on every render with a fresh challenge after each failed attempt.

### Why this matters before mass access

Item 13 in "Andrew's Pre-Go-Live Checklist" above. The first 1,727 migrated users are about to receive password reset emails. A material fraction of them will mistype their new password at least once — the current behavior will make that look like a broken site. Worth fixing before the bulk email goes out.
