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

---

## Current State

### Code ✅ LOCAL + TEST IN SYNC
All of Caleb's and Andrew's work is merged to main and deployed to TEST.

### Migration ✅ COMPLETE — TEST NOW ON LIVE DATA
- Fresh D7 dump taken April 16, 2026 from production D7 DB
- Migration run locally: 1,727 users, 581 credits, 5,072 registrations, 42 seasons
- Exported as `d11-beta-2026-04-16-clean.sql.gz` (DEFINER-stripped)
- Imported to `n6ac4b5_d11prod` on InMotion
- TEST site (`test.ccsoccer.com`) now points at `n6ac4b5_d11prod` — **real user data, real emails**
- All board member roles assigned (including kaimark)

### Waitlist Migration ✅ COMPLETE
- 241 `ccsoccer_waitlist` entities created on TEST from D7 `league_manager_waitlist`
- Covers all seasons from Coed/Mens March-April 2024 through Coed/Mens Spring 2026
- Skipped users already registered for the corresponding season (38 skipped)
- One user (Osman Harac, uid 94531) created from scratch — signed up after April 16 dump
- D7 connection added to TEST `settings.local.php` for migration script use
- **Note:** Waitlist migration scripts were one-off files, created and deleted on server — not in git

### Passkeys ✅ DEPLOYED TO TEST
- `drupal/wa` 2.0.0-rc7 installed locally and on test.ccsoccer.com
- Touch ID / passkey login working
- Password login and reset still work as fallback

### Server Aliases ✅ SET UP
Bash aliases saved to `~/.bashrc` on InMotion server:
- `ccsDeploy` — cd to site dir + git pull
- `ccsCr` — drush cr
- `ccsUpdb` — drush updb -y
- `ccsCim` — drush cim -y
- Full deploy: `ccsDeploy && ccsUpdb && ccsCim && ccsCr`

### Deploy Key ✅ SET UP
SSH deploy key configured on InMotion — `ccsDeploy` no longer prompts for GitHub credentials.

---

## Session Work — April 17, 2026

### Commerce Store ✅
Created CC Soccer store via admin UI (content entity, not config — doesn't survive DB import). Order type confirmed pointing to custom checkout flow. Payment gateways confirmed sandbox + manual — intentional for TEST.

### Jersey Notification Opt-in ✅
- Added `field_jersey_notifications` boolean to user entity via `ccsoccer_update_9062`
- Added "Jersey Alerts" checkbox column to `BoardContactPreferencesForm`
- `sendJerseyPurchase()` in `NotificationService` now queries only board members with `field_jersey_notifications = TRUE` instead of all board members
- Respects existing `field_board_email` / `field_board_phone` preferences
- Tested locally via Mailpit — only opted-in members received notification
- **Action needed on TEST:** Go to Board Contact Preferences and check Jersey Alerts for appropriate members

### Completion Pane — Non-registration Orders ✅
- `CompletionPane::buildPaneForm()` now has three branches: season reg, tournament reg, no-reg (jersey-only)
- Jersey-only shows "Order complete!" / "Your order has been confirmed." and redirects to My Account
- Previously showed "You're registered!" for all order types

### Beta Tester Role ✅
- `config/sync/user.role.beta_tester.yml` added — no permissions, weight 4
- `getAllowedContacts()` in `NotificationService` now includes `beta_tester` role alongside `board_member`
- Beta users get their own transactional emails on TEST without board notification access
- At launch: remove role from users — gating disappears automatically when `site_instance = production`
- Add beta user IPs manually to `.htaccess`

### Pre-launch Checklist Updated
- Added three-tier button methodology pass to checklist

---

## Session Work — April 25, 2026

### UPDATE_WORKFLOW.md ✅
New top-level doc covering `composer install` vs `composer update`, conflict resolution, and the producer/consumer split between developers. Reference for the team to avoid simultaneous lockfile bumps.

### Admin Tables — Mobile Scroll Wrapper ✅
- `js/admin-mobile.js` wraps every admin table in a focusable `<div class="ccsoccer-table-scroll-wrapper">` at runtime
- `css/admin-mobile.css` styles the wrapper (`overflow-x: auto`, iOS momentum scrolling, keyboard focus ring, print styles)
- Library `ccsoccer/admin-mobile` attached on admin routes via `ccsoccer_page_attachments()`
- Fixes mobile portrait AND desktop-with-sidebar-open in both Claro and the public theme (board members / tournament directors lack the use admin theme permission and see admin pages in the public theme)
- Wrapper has `tabindex=0`, `role=region`, `aria-label` for keyboard/screen-reader users

### Dashboard — Current Seasons Filter ✅
- `buildSeasonsOverview()` in `AdminController.php` now filters to `end_date >= today` and sorts by `start_date ASC`
- Past seasons hidden; empty-state message differentiates "no seasons created yet" vs "all existing seasons have ended"
- Affects `/admin/ccsoccer/dashboard` and `/admin/ccsoccer/board-dashboard` (shared rendering)

### Welcome Banner Legibility ✅
- "Board Member Tools" / "Tournament Director Tools" headings now render in white on the dark welcome banner
- `.dashboard-section.welcome-section` background defined explicitly with `--color-gray-800`, text with `--color-text-inverse` so the banner looks the same in Claro and the public theme

### Credits Overview Dashboard ✅
- New `CreditsDashboardController::dashboard()` replaces the empty default `EntityListBuilder` rendering at `/admin/ccsoccer/credits`
- Route `entity.credits.collection` repointed to the new controller; menu and quick-link references unchanged (still resolve through the same route name)
- Page surfaces four stat cards (Outstanding active, Issued L30D, Used L30D, Net L30D), Recent Credit Events table (last 10 `SeasonCreditEvent` records, links to `ccsoccer.season_credits`), and Top Player Balances table (top 10 active, unexpired, links to `ccsoccer.admin_user_credits`)
- Direct DB queries with `SUM`/`GROUP BY` for stats; user-reference column resolved via `TableMapping` rather than hardcoded
- Cache: invalidated by `credits` and `season_credit_event` list cache tags + 5-minute `max-age` backstop
- Inline `<style>` block plus a piggyback on the existing `ccsoccer/season-credits` library — no new CSS file or theme hook
- Reachable from admin sidebar, Admin Dashboard quick link, and Board Dashboard quick link (all three already pointed at `entity.credits.collection`)

### Tournament Director Dashboard ✅
- `AdminController::directorDashboard()` no longer renders a "coming soon" stub
- Welcome copy updated; `buildDirectorQuickLinks()` extended with three new links: Tournament Schedule (public, `ccsoccer.tournament_schedule`), Tournament Deposits / Captain Dashboard (`ccsoccer.reports.tournament_deposits`, gated by `access tournament deposits report`), Help Center (`ccsoccer.help_center`)
- New `buildTournamentsOverview()` renders one card per active/upcoming tournament (filter `end_date >= today`, sorted by `start_date ASC`)
- Each card shows: name (link to canonical), status badge with color coding (planned / registration_open / in_progress / completed / cancelled), date range, three stats (Registrations, Teams `count / max_teams`, Captains), and action buttons: Roster, Schedule, Teams, Players, Deposits (Deposits hidden for users without the report permission)
- Empty-state distinguishes "no tournaments created yet" vs "all existing tournaments have ended"; "Add a tournament" CTA only renders for users with `administer ccsoccer` so pure directors don't see a 403 link
- CSS appended to `getDashboardStyles()` — no new files

### Help Center Accuracy Pass ✅
Six articles in `HelpCenterController.php` rewritten to match actual UI behavior — several referenced fields/buttons that do not exist.

### Game Status Added to Admin Quick Links ✅
- Game Status was already a quick link on the Board Member Dashboard but missing from the Admin Dashboard. Added between Credits and Notifications in `buildQuickLinks()`.

### Games Quick Link Removed ✅
- `entity.game.collection` was the same broken-default-EntityListBuilder pattern as the old Credits page. Removed from `buildBoardQuickLinks()` and `ccsoccer.links.menu.yml`. Route itself kept untouched.

### Help Center Card Added to Admin + Board Dashboards ✅
- `buildQuickLinks()` (Admin Dashboard) and `buildBoardQuickLinks()` (Board Member Dashboard) now both include a "Help Center" card.

### Tournament Teams Page — Pure Alphabetical Sort ✅
- `TournamentController::teams()` replaced weight-first sort with pure case-insensitive alphabetical (`strcasecmp`).

### Tournament Edit/Delete Permissions Realigned ✅
- `entity.tournament.edit_form`: `administer ccsoccer` → `manage tournaments`
- `entity.tournament.delete_form`: `manage tournaments` → `administer ccsoccer`

### Tournament Players Page — Search + Sticky Header Fix ✅
- `/admin/ccsoccer/tournament/{id}/players` now has client-side search matching Season Players page
- Sticky-header overlap bug fixed for both Season Players and Tournament Players pages

---

## Session Work — May 6, 2026

### Andrew's PR Deployed ✅
Pulled 10 commits from Andrew including:
- `ccsoccer_update_9063` — belt-and-suspenders removal of `field_credits_balance` from user entity
- New `CreditsDashboardController.php` — credits overview page
- `AdminController.php` additions — director dashboard, current-season filters, quick links
- `HelpCenterController.php` — accuracy pass on 6 articles
- `TournamentController.php` — alphabetical sort fix, player search
- Admin mobile CSS/JS (`admin-mobile.css`, `admin-mobile.js`)
- `season-players.css` / `season-players.js` — extended to cover tournament players
- Full deploy ran: `ccsDeploy && ccsUpdb && ccsCim && ccsCr` — clean

### Waitlist Migration ✅
- Investigated D7 `league_manager_waitlist` table — 279 entries across 18 seasons (March 2024 → Spring 2026)
- All waitlist UIDs confirmed present in D11 (migrated via 5-year login window) except Osman Harac (uid 94531, signed up after April 16 dump)
- Created Osman Harac as D11 user (uid 94531) with full profile from D7 + waitlist entries for seasons 41 and 42
- Ran bulk migration: 239 `ccsoccer_waitlist` entities created, 38 skipped (already registered), 2 skipped (Osman already exists)
- D7 → D11 season map used (D7 product_id → D11 season id, 18 seasons covered)

### Bulk Notification Waitlist Bug Fixed ✅
- **Bug:** `NotificationService::calculateRecipientCount()` and `sendBulkNotification()` were querying `ccsoccer_registration` with `status = 'waitlist'` — a status that doesn't exist in the actual registration flow
- **Root cause:** The entire waitlist flow (join, offer spot, override creation, conversion) is built around the `ccsoccer_waitlist` entity via `WaitlistManagerService`. The notification query was wired to the wrong entity.
- **Fix:** Both methods now query `ccsoccer_waitlist` with `status IN (pending, offered)` via `$this->entityTypeManager->getStorage('ccsoccer_waitlist')`
- **File:** `web/modules/custom/ccsoccer/src/Service/NotificationService.php` — two blocks updated, same pattern

### TEST settings.local.php — D7 Connection Added
- Added `$databases['d7']['default']` pointing at `n6ac4b5_ccsoccer` to TEST's `settings.local.php`
- Required chmod 755 on `sites/default` to edit (via cPanel or SSH), lock back to `555` after
- Needed for migration scripts that read from D7 directly

---

## ⚠️ ANDREW — ACTION REQUIRED: Update Your Local DB

Your local DB has the old 200-user sample with masked emails. You need to replace it with the new production-ready migration DB. **Do not re-run the migration** — just import the finished DB directly. This is much simpler than what Caleb went through.

### Step 1: Download the DB from the server
```bash
scp ccsoccer:/home/n6ac4b5/d11-beta-2026-04-16-clean.sql.gz ~/Sites/ccsoccer-d11/
```
(Assumes your SSH alias is `ccsoccer` — adjust if different)

### Step 2: Import it into DDEV
```bash
cd ~/Sites/ccsoccer-d11
ddev import-db --file=d11-beta-2026-04-16-clean.sql.gz
```
This replaces your local DB content cleanly — no site:install, no cim, no UUID drama.

### Step 3: Pull latest code
```bash
git pull
ddev drush updb -y
ddev drush cim -y
ddev drush cr
```

### Step 4: Update your local settings.local.php D7 connection
Your `settings.local.php` needs a `d7` database entry pointing at the fresh D7 dump so the migration command works if you ever need to run it. Import the D7 dump first:
```bash
ddev mysql -e "CREATE DATABASE IF NOT EXISTS n6ac4b5_ccsoccer;"
gunzip -c /path/to/d7-fresh-2026-04-16.sql.gz | ddev mysql n6ac4b5_ccsoccer
```
(The D7 dump is at `/home/n6ac4b5/d7-fresh-2026-04-16.sql.gz` on the server if you need it)

Your `settings.local.php` should already have:
```php
$databases['d7']['default'] = [
  'database' => 'n6ac4b5_ccsoccer',
  'username' => 'db',
  'password' => 'db',
  'host' => 'db',
  'driver' => 'mysql',
  'prefix' => '',
];
```
If it doesn't, add it.

### Step 5: Log in locally
Admin credentials: username `admin`, password `TJ4XxyYGCd`

---

## Andrew's Recent Features (all merged + on TEST)

### 1. Role-based Help Center
`/admin/ccsoccer/help` — documentation for board members, admins, and tournament directors. Role-gated by existing permissions. Own CSS (`help-center.css`), controller (`HelpCenterController.php`), routing, menu link, and library entry. All hardcoded content in PHP methods.

### 2. Personalized next-game banner
Shows authenticated players their next upcoming game in the site header (team, jersey color, time, field). Moved from module `hook_page_top()` to theme `preprocess_page` + Twig. Cache contexts set to `user` + `max-age = 0`.

### 3. Schedule grid current-week fix
Grid now scrolls to the current/next week on load. Fixed in both `ScheduleGridBuilder::calculateCurrentWeekIndex()` and `schedule-navigation.js`.

### 4. My Teams "registered but not assigned" fix
Players registered for a season with no team yet see "teams haven't been assigned yet, check back soon" instead of the false "not registered" message. Three-state logic in `ContentController::myTeamsPage()`.

### 5. Mobile swipe for schedule
Swipe left/right to navigate weeks on mobile. Feature branch `feature/mobile_swipe_schedule_view` merged to main.

### 6. Login form PHP warning fix
Added `'#parents' => []` to passkey help text element in `ccsoccer.module` — fixes PHP warnings on failed login attempts.

---

## Beta Testing Notes

### Password Reset Flow
- TEST now has real user data — all migrated users have randomized passwords
- Users (including board members) must use "Forgot password" to get in
- Password reset emails are gated by `site_instance = 'test'` — only board members' emails go through
- Caleb confirmed the flow works end-to-end (email went to spam — SPF/DKIM needed before launch)

### Notification Gating on TEST
When `site_instance = 'test'`, `NotificationService` dynamically builds an allowlist from all users with the `board_member` role. Only their emails/phones go through. Everyone else is silently blocked.

### What Players See
- My Account shows current season registration ✅
- No team or schedule shown (none assigned yet — correct) ✅
- Registration page shows available seasons ✅

---

## Pending Decision — Passkey RP ID
Passkeys are domain-bound. Passkeys registered on `test.ccsoccer.com` will NOT work on `ccsoccer.com` at launch unless the RP ID is set to `ccsoccer.com` now.

**Action needed:** Determine if `drupal/wa` supports a custom RP ID. If so, set it to `ccsoccer.com` before users start registering passkeys on TEST.

---

## Next Steps

### 1. Pre-launch checklist
- Set up SPF/DKIM for ccsoccer.com domain (password reset emails going to spam)
- Remove IP whitelist block from `.htaccess` on launch day
- Confirm www vs non-www canonical redirect
- Confirm HTTPS handling
- Enable CSS/JS aggregation
- reCAPTCHA setup
- Set up two alias sets in `~/.bashrc`: `ccsTest*` (pointing to `~/test_ccsoccer_site`) and `ccsProd*` (pointing to `~/public_html`) for Deploy, Cr, Updb, Cim
- Create live `settings.local.php` in `~/public_html/web/sites/default/` with live Authorize.net credentials and `site_instance = production`
- Three-tier button methodology pass (primary solid red = main CTAs, primary-soft light red = navigation actions, white/outlined = informational)

### 2. Post-launch manual actions (cannot be scripted)
- Assign `permanent_override`: Haley, Layne, Julie, Myk, Kyle
- Send password reset email to all migrated users (all have random passwords)
- Verify credit balances for known users against D7
- Check credits/registrations issued in D7 after April 16, 2026 dump date

### 3. Team handling refactor (discuss with Andrew first)
Eager team creation on season save → lazy creation when Roster Builder opens.
Do not implement without consulting Andrew — he built the roster builder.

### 4. Resolve Passkey RP ID decision (see above)

### 5. `slofriendly` role reference
During `cim` a warning appeared: `Config user.role.slofriendly not found`. There's a reference to this old role somewhere in module install hooks. Not breaking, but worth cleaning up before launch.

### 6. Top-level "CC Soccer" menu item — admin landing (discuss with Caleb)
**Open question, no code changed yet.** The top-level `ccsoccer.admin` menu link always routes to `ccsoccer.board_dashboard` — including for administrators. Proposed fix: `hook_menu_links_discovered_alter()` to route admins to `/admin/ccsoccer` instead. See previous SESSION_HANDOFF for full details.

---

## DB Quick Reference

### TEST server DB (live migration data)
- DB: `n6ac4b5_d11prod`
- User: `n6ac4b5_ccsoccer_user`
- Password: `vGL3KWO(K8C;`
- Migration counts: 1,727 users, 581 credits, 5,072 registrations, 42 seasons, 241 waitlist entries

### TEST site app DB (old 200-user sample — no longer in use)
- DB: `n6ac4b5_ccsoccer_test`
- User: `n6ac4b5_ccsoccer_test`
- Password: `Soc8er#Test24!`

### D7 production DB (source of truth)
- DB: `n6ac4b5_ccsoccer`
- User: `n6ac4b5_ccsoccer_user`
- Password: `vGL3KWO(K8C;`
- Fresh dump: `/home/n6ac4b5/d7-fresh-2026-04-16.sql.gz` on server

### Local D11 DB
- Admin: `admin` / `TJ4XxyYGCd`
- D11 beta export: `d11-beta-2026-04-16-clean.sql.gz` in project root (gitignored)

---

## Migration Command Reference

**File:** `web/modules/custom/ccsoccer/src/Drush/Commands/MigrateCommands.php`

```bash
# Full migration (steps 1-4: seasons, users, credits, registrations)
ddev drush ccsoccer:migrate-d7

# Individual steps
ddev drush ccsoccer:migrate-d7 --step=seasons
ddev drush ccsoccer:migrate-d7 --step=users
ddev drush ccsoccer:migrate-d7 --step=credits
ddev drush ccsoccer:migrate-d7 --step=registrations

# Role assignments — run ONCE after migration is finalized (not in 'all')
ddev drush ccsoccer:migrate-d7 --step=roles --dry-run
ddev drush ccsoccer:migrate-d7 --step=roles
```

**Role assignments hardcoded in `assignRoles()`:**
| Username | Roles |
|---|---|
| David.farris | board_member |
| Csalvini | board_member |
| laynesmith | board_member |
| chrisraymer | board_member |
| SoccerHD4 | board_member, tournament_director |
| kaimark | board_member |
| cncross | board_member, tournament_director, administrator |
| abmeade@hotmail.com | board_member, tournament_director, administrator |

Note: `abmeade@hotmail.com` is his D11 username (email-as-username from D7), looked up by `name` field.

---

## Key Facts / Gotchas

### DB import fix for InMotion
DDEV exports views with `DEFINER=\`db\`@\`%\`` which InMotion rejects. Always use the clean version:
```bash
gunzip -c dump.sql.gz | sed 's/DEFINER=[^*]*\*/\*/' | gzip > dump-clean.sql.gz
```

### Fresh DB reset for local dev (if needed again)
If you ever need to wipe and re-run the migration locally:
```bash
ddev drush site:install --account-name=admin --account-pass=TJ4XxyYGCd -y
ddev drush config-set system.site uuid $(grep uuid config/sync/system.site.yml | awk '{print $2}') -y
ddev drush entity:delete shortcut -y
ddev drush entity:delete shortcut_set -y
ddev drush cim -y
ddev drush cr
# Then run migration
ddev drush ccsoccer:migrate-d7
ddev drush ccsoccer:migrate-d7 --step=roles
```
Note: `ddev import-db --file=dump.sql.gz` is much simpler if you have a finished DB export — skip the above entirely.

### Theme vs module rendering
Site-wide header content is rendered by the **theme** (`ccsoccer_theme_preprocess_page` + `page.html.twig`), NOT `hook_page_top()`. New header features go in the theme.

### Two-file CSS sync
Any change to base styles must be applied to both:
- `web/modules/custom/ccsoccer/css/ccsoccer-base.css` (admin/Claro pages)
- `web/themes/custom/ccsoccer_theme/css/base.css` (public pages)

### Email-as-username migration
Some D7 users had email addresses as usernames. These migrate as-is into the D11 `name` field. Their `mail` field gets real email from D7 `mail` column. Look them up by `name`, not `mail`.

### Notification gating on TEST
`NotificationService::getAllowedContacts()` queries all `board_member` role users dynamically. Board member emails/phones go through; all others are blocked. This means as of this session, board members can receive real emails on TEST (password resets confirmed working).

### Waitlist architecture
- **`ccsoccer_waitlist` entity** — the source of truth. All queue management (join, offer spot, override creation, conversion) goes through `WaitlistManagerService` using this entity.
- **`NotificationService` bulk sender** — now correctly queries `ccsoccer_waitlist` with `status IN (pending, offered)`. Previously was incorrectly querying `ccsoccer_registration.status = 'waitlist'` which never gets set anywhere in the codebase.
- **`registration.status = 'waitlist'`** — not used anywhere. Do not use this pattern.

### TEST settings.local.php
- Located at `/home/n6ac4b5/public_html/test_ccsoccer_site/web/sites/default/settings.local.php`
- Requires `chmod 755 sites/default` to edit (via cPanel or SSH), lock back to `555` after
- Now includes D7 connection (`$databases['d7']['default']`) pointing at `n6ac4b5_ccsoccer`

---

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

# Full deploy
ccsDeploy && ccsUpdb && ccsCim && ccsCr

# Individual commands
ccsDeploy   # git pull
ccsUpdb     # drush updb -y
ccsCim      # drush cim -y
ccsCr       # drush cr
```

## Test Server .htaccess (IP Whitelist)
Not in git — protected via `skip-worktree`. If ever lost:
```apache
Require ip 68.249.41.9 35.151.50.130 99.8.107.54 97.84.70.141
<IfModule mod_headers.c>
  Header set X-Robots-Tag "noindex, nofollow, noarchive"
</IfModule>
```
```bash
git update-index --skip-worktree web/.htaccess
```

## Git Workflow
- Always `git pull` before `git push`
- `main` is the primary branch
- `settings.local.php` is NOT in git
- `*.sql.gz` files are gitignored
