# CC Soccer D11 - Session Handoff
**Date:** March 21, 2026 (evening)
**Session:** Passkey (WebAuthn) implementation
**Branch:** `feature/passkey-webauthn` (pushed to GitHub)

---

## Current State

### Passkey authentication is WORKING locally

Installed `drupal/wa` 2.0.0-rc7 on the DDEV local environment. Full passkey flow tested and verified:
- Registration via Touch ID on Mac (Chrome)
- Login via "Sign in with Passkey" button (Touch ID, usernameless)
- Redirect to `/my-account` after passkey login (existing `ccsoccer_user_login` hook works)
- Password login and password reset still work normally
- Passkeys tab visible on user profile (`/user/{uid}/passkeys`)

### Branch status
- `feature/passkey-webauthn` — committed and pushed to GitHub
- Ready to merge to `main` and deploy to test.ccsoccer.com

---

## What Changed This Session

### Composer — drupal/wa added
- `drupal/wa:^2.0@RC` added to `composer.json`
- 18 new packages in `composer.lock` including `web-auth/webauthn-lib` 5.2.4, `brick/math`, `spomky-labs/cbor-php`, `spomky-labs/pki-framework`

### Configuration — 3 files added/updated
- `config/sync/wa.settings.yml` — Passkey settings (new)
- `config/sync/views.view.wa_passkeys.yml` — Admin passkey overview view (new)
- `config/sync/core.extension.yml` — `wa` module added to enabled modules

### wa module settings (in wa.settings.yml)
- **Enable Passkey Login:** true
- **Allowed Roles:** All (authenticated, content_editor, administrator, board_member, tournament_director)
- **Allowed Authenticators:** Generic/Software, Chrome on Mac, iCloud Keychain (Apple Passkeys), Microsoft Authenticator for Android, Google Password Manager, Windows Hello (TPM-backed)
- **User Verification:** preferred
- **Resident Key:** required (enables usernameless login via "Sign in with Passkey" button)
- **Enforce User Handle:** false
- **Notification:** Email sent when a new passkey is added to an account

---

## Deploying to test.ccsoccer.com

After merging `feature/passkey-webauthn` to `main`, SSH in and run the standard deploy sequence plus `composer install` (new packages) and `drush cim` (new config):

```bash
ssh ccsoccer
cd ~/public_html/test_ccsoccer_site
git pull

# Install new composer packages (drupal/wa + 17 dependencies)
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

# Run database updates (creates wa module tables)
PATH=/opt/cpanel/ea-php83/root/usr/bin:$PATH /opt/cpanel/ea-php83/root/usr/bin/php vendor/drush/drush/drush.php -r web updb -y

# Import config (enables wa module, applies passkey settings)
PATH=/opt/cpanel/ea-php83/root/usr/bin:$PATH /opt/cpanel/ea-php83/root/usr/bin/php vendor/drush/drush/drush.php -r web cim -y

# Clear caches
PATH=/opt/cpanel/ea-php83/root/usr/bin:$PATH /opt/cpanel/ea-php83/root/usr/bin/php vendor/drush/drush/drush.php -r web cr
```

### Prerequisites for test.ccsoccer.com
- HTTPS with valid SSL certificate (should already be in place)
- PHP 8.2+ with `ext-openssl` (already PHP 8.3 on InMotion)
- `trusted_host_patterns` should include `^test\.ccsoccer\.com$` in `settings.local.php`

### Important notes
- Passkeys registered on DDEV will NOT work on test.ccsoccer.com (domain-bound by design)
- Users must register new passkeys on each domain
- The RP ID will automatically be `test.ccsoccer.com` — passkeys registered there will NOT carry over to `ccsoccer.com` either (consider setting RP ID to `ccsoccer.com` if wa module supports it, so passkeys are portable to production)

---

## Remaining Work

### UI Polish (future session)
- **Login form spacing:** Need CSS to add margin between the "Log in" button and "Other sign in options" fieldset
- **Passkeys tab discovery:** The tab shows on the user profile page but isn't surfaced from My Account dashboard — consider adding a link/card

### Testing Still Needed
- [ ] iPhone testing (Face ID) — requires mkcert CA on iPhone or `ddev share` for local, or test on test.ccsoccer.com directly
- [ ] Multiple passkeys per user (e.g., Mac + iPhone)
- [ ] Delete passkey and verify graceful fallback
- [ ] Commerce checkout with passkey-authenticated user
- [ ] Flood control (repeated failed attempts)
- [ ] Test with non-admin user roles

### Future Decisions (from implementation plan)
- Whether to set RP ID to `ccsoccer.com` on test.ccsoccer.com for passkey portability
- Whether to require passkeys for board member accounts
- User-facing help text about passkeys on My Account page

---

## Architecture Reference

### Decision 6 (ARCHITECTURE_DECISIONS.md)
Passkeys are a **post-launch optional enhancement**. Password login remains for all users. Passkeys sit alongside, not replacing, existing auth. The `drupal/wa` module is not yet under Drupal's security advisory policy — risk is mitigated by the fact that it's purely additive (disable toggle available at `/admin/config/people/wa`).

### Key routes added by wa module
- `/admin/config/people/wa` — Admin settings
- `/admin/people/passkeys` — Admin overview of all registered passkeys
- `/user/{user}/passkeys` — User's passkey management page
- `/wa/register/options`, `/wa/register/verify` — Registration API endpoints
- `/wa/login/options`, `/wa/login/verify` — Login API endpoints

### Rollback
Quick disable (no code change):
```bash
drush config:set wa.settings enable_passkey_login 0 -y
drush cr
```
Full removal:
```bash
drush pm:uninstall wa -y
composer remove drupal/wa
drush cr
```

---

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

# Deploy to test
cd ~/public_html/test_ccsoccer_site
git pull
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
PATH=/opt/cpanel/ea-php83/root/usr/bin:$PATH /opt/cpanel/ea-php83/root/usr/bin/php vendor/drush/drush/drush.php -r web updb -y
PATH=/opt/cpanel/ea-php83/root/usr/bin:$PATH /opt/cpanel/ea-php83/root/usr/bin/php vendor/drush/drush/drush.php -r web cim -y
PATH=/opt/cpanel/ea-php83/root/usr/bin:$PATH /opt/cpanel/ea-php83/root/usr/bin/php vendor/drush/drush/drush.php -r web 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>
```

---

## Session 2: Personalized Banner, Schedule Fix, Login Fix (March 21 evening, continued)

### Summary
Added a personalized "next game" banner in the site header for authenticated players, fixed the schedule grid not scrolling to the current week, and fixed PHP warnings on failed login attempts.

### Changes (NOT YET COMMITTED)
All changes are in the working tree on `main`. Suggested commit message:

```
Add personalized next-game banner for authenticated players

- Show player's next game in the site header (team, jersey color,
  time, field) using registration-based team lookup matching the
  existing ScheduleGridBuilder approach
- Fix jersey color logic to match ScheduleGeneratorService convention
  (home = Red, away = White)
- Fix schedule grid not scrolling to current week (PHP and JS)
- Fix login form #parents warning on failed login attempts
- Clean up dead banner code from ccsoccer.module (hook_page_top
  approach replaced by theme preprocess + Twig template)
- Add per-user cache context to prevent Dynamic Page Cache from
  serving stale personalized content
```

### Files Modified (6 files)

1. **`web/themes/custom/ccsoccer_theme/ccsoccer_theme.theme`** (+205 lines)
   - Added `_ccsoccer_build_player_next_game_data($uid)` function
   - Uses registration-based team lookup (same as `ScheduleGridBuilder::getUserTeamIds()`)
   - Jersey colors: **home = Red, away = White** (matches `ScheduleGeneratorService` line 1205)
   - Added `$variables['#cache']['contexts'][] = 'user'` and `max-age = 0` to prevent Dynamic Page Cache from serving stale content

2. **`web/themes/custom/ccsoccer_theme/templates/page.html.twig`** (+23 lines)
   - `{% if player_next_game %}` block renders below the game status bar
   - Four display states: today+active ("Tonight: Team, Color, Time, Field"), today+cancelled, future+active ("Next game: Date — Team, Color, Time, Field"), future+cancelled

3. **`web/themes/custom/ccsoccer_theme/css/layout.css`** (+32 lines)
   - `.site-header__player-game` with `--on` (green) and `--cancelled` (red) variants
   - Mobile responsive styles at 768px breakpoint
   - `.ccsoccer-game-status-banner { display: none !important }` — hides the module's duplicate `page_top` game status banner

4. **`web/modules/custom/ccsoccer/ccsoccer.module`** (+13 lines net)
   - Removed `ccsoccer_player_next_game_banner` theme hook registration
   - Removed player banner code from `ccsoccer_page_top()`
   - Removed entire `_ccsoccer_build_player_next_game_banner()` function (~220 lines)
   - Added `'#parents' => []` to login form passkey help text element (fixes PHP warnings)

5. **`web/modules/custom/ccsoccer/js/schedule-navigation.js`** (+5/-2 lines)
   - Fixed initial offset: `Math.min(currentWeekIndex, state.lastGame)` so grid scrolls to current/next week instead of always showing week 1

6. **`web/modules/custom/ccsoccer/src/Service/ScheduleGridBuilder.php`** (+10/-5 lines)
   - Fixed `calculateCurrentWeekIndex()`: now finds first week >= today instead of last week <= today

### File Deleted
- `web/modules/custom/ccsoccer/templates/ccsoccer-player-next-game-banner.html.twig` — orphaned template from early iteration, manually deleted by Andrew

### Key Lessons / Gotchas Discovered

1. **Theme rendering vs module rendering:** The site-wide game status banner is rendered by the **theme** (`ccsoccer_theme_preprocess_page` + `page.html.twig`), NOT the module's `hook_page_top()`. The module's `page_top` output is hidden by CSS. New header features must go in the theme, not the module.

2. **Registration-based team lookup is authoritative:** The `Team.players` multi-value field is NOT reliably populated for all users. The `/my-schedule` page and `ScheduleGridBuilder` find teams via `ccsoccer_registration` entities (`player` + `status` + `season` → `team`). The banner function must use the same approach.

3. **Jersey color convention:** Home = **Red**, Away = **White**. Defined in `ScheduleGeneratorService::getTeamSchedule()` line 1205: `'jersey' => $is_home ? 'Red' : 'White'`. Schedule grid renders home team on top row, away team on bottom row.

4. **Cache contexts matter:** Without `$variables['#cache']['contexts'][] = 'user'` in the theme preprocess, Drupal's Dynamic Page Cache serves stale personalized content. The module's `ccsoccer_preprocess_page()` returns early on production without setting cache metadata.

5. **`#after_build` elements need `#parents`:** Elements added to forms via `#after_build` callbacks bypass Drupal's normal form processing that auto-assigns `#parents`. The error handler iterates all children and crashes on elements missing this property.

6. **Schedule grid bug was in commit `05073cb` (Feb 26):** The `lastGame` capping logic collapsed the offset to 0. The security hardening commit was a red herring.

### After Deploying
Run `drush cr` to clear all Drupal caches (theme registry, Twig template cache, render cache, dynamic page cache).

### Testing Verified
- [x] Personalized banner shows for authenticated players (testuser35, testuser2000)
- [x] Banner disappears for anonymous users
- [x] Schedule grid scrolls to current week (Mar 24/26)
- [x] Login form no longer shows PHP warnings on failed attempts
- [x] Jersey colors match schedule grid and My Schedule table

---

## Session 3: Credits Menu Item Investigation (March 21 evening, continued)

### Problem
The **CC Soccer > Credits** menu item (`/admin/ccsoccer/credits`) renders a blank page with just an "Operations" header and no data, even though credit entities clearly exist in the database (visible on season credit pages and individual player credit pages).

This page is also visible to **board members** via the `view reports` permission — they see the same blank page from their CC Soccer sidebar menu.

### Root Cause
The `Credits` entity (in `src/Entity/Credits.php`) uses Drupal's **default** `EntityListBuilder`:

```php
"list_builder" = "Drupal\Core\Entity\EntityListBuilder",
```

The default list builder only renders an "Operations" column with no data columns (no user, amount, status, etc.). Every other entity in the module that has a meaningful list page (Season, Registration, Team, Tournament, League) has a **custom** list builder class. Credits never got one.

The entity and data are fine — the Season Credits pages (`/admin/ccsoccer/season/{id}/credits`) use `SeasonController::credits()` with custom queries, and the player credits pages (`/user/{uid}/ccsoccer-credits`) use `AdminCreditsController::userCredits()`. Both work correctly. Only the global entity collection route is broken.

### How the Credits System Actually Works

There are **two entity types** working together:

1. **`SeasonCreditEvent`** — bulk operation records. When admin issues a rainout credit, one `SeasonCreditEvent` is created (e.g., "$8 × 124 players = $992"). Lives at `/admin/ccsoccer/season/{id}/credits`.

2. **`Credits`** — individual per-player records. The `SeasonCreditEvent` form fans out one `Credits` entity per registered player. These are what appear on player profiles and the season drill-down at `/admin/ccsoccer/season/{id}/credits/players`.

The `/admin/ccsoccer/credits` page is supposed to be a **global, cross-season view** of all individual `Credits` entities — but it was never built out.

### Scalability Concern
Each rainout credit event creates ~100-124 individual `Credits` entities. With 3-4 rainouts per season and 3 seasons per year, the credits table grows by ~1,500 records/year. After a few years, a flat list would be 5,000-10,000+ rows — too many for an unfiltered entity list page.

### Options for Discussion (Andrew + Caleb)

**Option A: Remove the menu item entirely**
- The existing Season Credits and Player Credits views already cover every real admin workflow
- Season Credits = bulk operations view (issue/revoke credits for a whole season)
- Player Credits = individual audit view (drill into any player's balance and history)
- The global list doesn't add value that these two don't already provide
- Remove from both admin and board member menus
- Lowest effort, no maintenance burden

**Option B: Repurpose as a credits dashboard/report**
- Replace the entity list with a custom controller showing summary stats: total active credits outstanding, credits by season, top player balances, recently issued events
- More useful than a raw table for board members who want a quick overview
- Medium effort

**Option C: Build a proper list builder with filters and pagination**
- Create `CreditsListBuilder.php` with columns (user, amount, type, status, reason, season, date)
- **Default filter to "Active" status only** — this is the most useful default since admins typically care about outstanding credits, not historical used/expired ones
- Add filter controls for status (Active, Used, Expired, Revoked), season, source type, date range, and player search
- Add pagination (e.g., 50 per page like `RegistrationListBuilder`)
- Follows the pattern of existing list builders (`TeamListBuilder`, `RegistrationListBuilder`)
- Highest effort, but provides a complete audit trail if needed

**Option D: Hybrid — dashboard for board members, filtered list for admins**
- Board members see a summary dashboard (Option B) via their `view reports` permission
- Admins get the full filtered list (Option C) for detailed audit work
- Most complete but most effort

### Board Member View Note
The Credits menu item is also visible to users with the `board_member` role (via `view reports` permission). The board member sidebar shows it under CC Soccer menu. Whatever decision is made should also consider whether to remove or replace it in the board member view. The board member currently sees the same blank page at `/admin/ccsoccer/credits` — just styled differently (no admin toolbar, standard site chrome).

### No Code Changes This Session
This was research/discussion only. No files were modified.

---

## Session 4: My Teams — "Not Registered" Fix for Unassigned Players (March 22 evening, continued)

### Problem
When a player is registered for a season but the admin has not yet created teams / assigned rosters, the **My Team** page (`/my-teams?season={id}`) displays:

> "You are not registered for this season."

This is misleading — the player **is** registered (visible on `/my-registrations` with a "Manage Group" button), they just don't have a team yet.

**Example:** testuser11 is registered for Men's 35+ Spring 2026, but teams haven't been generated for that season. Clicking the "Men's 35+ Spring 2026" tab on My Team shows the false "not registered" message with a "Register for a Season" button.

### Root Cause
`ContentController::myTeamsPage()` (line ~670) builds a map of `season_id => team` by iterating the user's registrations and only keeping those with a non-empty `team` field:

```php
if (!$reg->get('team')->isEmpty()) {
    // ... add to $user_season_teams
}
```

Registrations without a team are silently skipped. The display logic at line ~771 then checks `if (!isset($user_season_teams[$active_season_id]))` — which is true for registered-but-unassigned players — and shows the "not registered" message.

### Fix Applied
**File:** `web/modules/custom/ccsoccer/src/Controller/ContentController.php` (+67/-22 lines)

**Branch:** `feature/registered_but_not_assigned_to_a_team`

Three changes:

1. **New `$user_season_registrations` map** — Tracks ALL seasons the user has a paid/active registration for, regardless of team assignment. Built alongside the existing `$user_season_teams` map in the same loop.

2. **Three-state display logic** instead of the previous binary check:
   - **Has team** → show roster (unchanged)
   - **Registered, no team yet** → *"You're registered for [Season Name], but teams haven't been assigned yet. Check back soon!"*
   - **Not registered** → *"You are not registered for this season."* + Register button (unchanged)

3. **Smarter default tab selection** — Priority chain expanded from 4 to 6 levels. Seasons where the user is registered but unassigned (priorities 3-4) now rank higher than seasons the user has no connection to (priorities 5-6):
   1. Coed season with team assigned
   2. Any season with team assigned
   3. Coed season registered but no team yet
   4. Any season registered but no team yet
   5. First coed season (not registered)
   6. First season overall

### Suggested Commit Message
```
Fix My Teams page showing "not registered" for players awaiting team assignment

Players registered for a season but not yet assigned to a team were
incorrectly told "You are not registered for this season." The page
only checked for team assignments, not registrations. Now distinguishes
three states: has team (show roster), registered but no team yet
("teams haven't been assigned yet"), and not registered (register
button). Also updates default tab selection to prefer seasons the
user is registered for, even without a team.
```

### Testing
- [ ] As testuser11, visit `/my-teams?season=6` (Men's 35+ Spring 2026, no teams yet) — should show "registered but teams haven't been assigned" message
- [ ] As testuser11, visit `/my-teams?season=4` (Men's 35+ Winter 2026, has team) — should show roster as before
- [ ] As testuser11, visit `/my-teams` with no season param — default tab should land on a season the user is connected to
- [ ] As a user with no registrations at all, visit `/my-teams` — should still show "not registered" + Register button
- [ ] Run `drush cr` after deploying to clear caches

### Also Noted This Session

**Schedule Builder Workbench styling** — The Season schedule builder (`/admin/ccsoccer/season/{id}/schedule`) has a workbench area below the grid where dragged teams appear as full-width blue bars with a large empty gap above them. This is because the Season builder uses class `schedule-workbench` with no matching CSS, while the Tournament builder uses class `workbench-panel` inside a `schedule-builder-content` flex wrapper, which has full sidebar styling. Not a regression — the Season builder was built first with a simpler below-grid layout and never got the sidebar treatment. Low priority polish item for a future session.

---

## Git Workflow
- Always `git pull` before `git push` — Andrew may have pushed changes
- `main` is the primary branch
- `feature/passkey-webauthn` branch contains passkey work — merge to main before deploying
- `settings.local.php` is NOT in git — never commit it
- `*.sql.gz` files are gitignored — never commit DB dumps
