# CC Soccer D11 - Session Handoff
**Date:** February 23, 2026
**Session:** Player Picture Improvements — Upload, Validation, Hover, Rotate
**Branch:** main

## Last Updated
2026-02-23

## Completed This Session

### 1. Consolidate Picture Fields — `field_player_picture` Is Now Canonical
Resolved the duplicate picture field issue. All code now reads from `field_player_picture` instead of Drupal's built-in `user_picture`. The `user_picture` field is hidden from ALL users (admins included) on the edit form.

**Files modified:**
- `src/Controller/SeasonController.php` — switched from `user_picture` to `field_player_picture`
- `src/Controller/TournamentController.php` — same
- `src/Controller/PlayerAdminController.php` — same
- `ccsoccer.module` — `ccsoccer_form_user_form_alter()` now hides `user_picture` for all users, not just non-admins

### 2. Upload Limit Increased + Auto-Resize on Upload
Increased `field_player_picture` upload limit from 1MB to 10MB to support modern phone cameras. Added `ccsoccer_user_presave()` hook that auto-resizes images to 1024px max on the longest side after upload, keeping stored files small.

**Files modified:**
- `config/sync/field.field.user.user.field_player_picture.yml` — `max_filesize: '10 MB'`
- `ccsoccer.module` — new `ccsoccer_user_presave()` hook (auto-resize + EXIF orientation fix)

**Note:** Config change requires `drush cim -y` on deploy.

### 3. EXIF Orientation Auto-Fix on Upload
Phone photos with EXIF orientation metadata (e.g., upside-down, rotated) are now automatically corrected in `ccsoccer_user_presave()` using GD library. The EXIF orientation tag is read, the image pixels are transformed to match, and the file is saved without the EXIF tag so all viewers display it consistently.

**File:** `ccsoccer.module` — `ccsoccer_user_presave()`

### 4. Client-Side Face Detection (Warning Only)
Added face detection on player picture uploads using face-api.js (vladmandic fork, TinyFaceDetector model ~190KB). Runs entirely in-browser — no third-party cloud services for privacy. Shows a yellow warning if no face is detected but does NOT block form submission.

**Key decisions:**
- Warning only, not blocking — admins can review and delete bad photos
- Uses `URL.createObjectURL()` on the full-size File object for reliable detection across desktop and mobile
- Warning placed outside Drupal's AJAX replacement zone so it survives file widget re-renders
- `inputSize: 416`, `scoreThreshold: 0.35` for good balance of speed and accuracy

**Files modified:**
- `ccsoccer.libraries.yml` — new `face-detection` library (external face-api.js CDN + local JS)
- `js/face-detection.js` — new file, face detection behavior
- `ccsoccer.module` — library attached in both `ccsoccer_form_user_register_form_alter()` and `ccsoccer_form_user_form_alter()`

### 5. Hover-to-Enlarge on Player List Pages
Hovering over a 50x50 thumbnail on season/tournament/all-players pages shows a 300x300 popup of the full-size photo. Uses a `position: fixed` popup appended to `<body>` (avoids clipping by table overflow). Hover events bound to `.player-picture-wrapper` so the popup stays visible when moving to the rotate button.

**Files modified:**
- `js/season-players.js` — `playerPictureEnlarge` behavior
- `css/season-players.css` — wrapper, popup, and enlarged-image styles
- `src/Controller/SeasonController.php` — picture markup includes wrapper + enlarged img
- `src/Controller/TournamentController.php` — same
- `src/Controller/PlayerAdminController.php` — same

### 6. Admin Photo Rotation (90° Clockwise)
Admins can rotate player photos on both:
- **Player list pages** (season/tournament/all-players) — small ↻ button appears on hover over thumbnail
- **User edit page** (`/user/{id}/edit`) — "↻ Rotate Photo 90°" button below the picture field

Rotation is server-side (GD library), modifying the actual file on disk. EXIF orientation is normalized before rotation so the result matches what the browser displays. Image style derivatives are flushed after rotation. Cache-busted URLs (`?v=filemtime`) ensure browsers show the updated image.

**Key technical note:** Drupal's `#markup` render element strips `<button>` tags via `Xss::filterAdmin()`. Fixed by using `Markup::create()` in controllers and `#type => 'inline_template'` in form alters.

**Files modified:**
- `ccsoccer.routing.yml` — new route `ccsoccer.player_picture_rotate` (POST, `manage seasons` permission)
- `src/Controller/PlayerAdminController.php` — new `rotatePicture()` method with EXIF normalization, GD rotation, image style flush, cache tag invalidation
- `js/season-players.js` — `playerPictureRotate` behavior (list pages) + `playerPictureRotateForm` behavior (edit form)
- `css/season-players.css` — rotate button styles (list pages + edit form)
- `ccsoccer.module` — `ccsoccer_form_user_form_alter()` adds rotate button + season-players library for admins

### 7. Render Cache Fix for Season Players Page
Season players page had no `#cache` metadata, so Drupal's Dynamic Page Cache served stale HTML after photo rotation. Added `#cache => max-age => 0` and file modification timestamps on all image URLs across all three controllers.

**Files modified:**
- `src/Controller/SeasonController.php` — `#cache` + `?v=mtime` on image URLs
- `src/Controller/TournamentController.php` — `?v=mtime` on image URLs
- `src/Controller/PlayerAdminController.php` — `?v=mtime` on image URLs

---

## Files Modified This Session
| File | Changes |
|------|---------|
| `config/sync/field.field.user.user.field_player_picture.yml` | Upload limit 1MB → 10MB |
| `ccsoccer.libraries.yml` | New `face-detection` library |
| `ccsoccer.module` | `user_presave` hook (resize + EXIF), face-detection attachment, hide `user_picture` for all, rotate button on edit form |
| `ccsoccer.routing.yml` | `ccsoccer.player_picture_rotate` route |
| `css/season-players.css` | Hover-enlarge, rotate button, edit-form rotate styles |
| `js/season-players.js` | Enlarge, rotate (list), rotate (edit form) behaviors |
| `js/face-detection.js` | New — client-side face detection |
| `src/Controller/PlayerAdminController.php` | `field_player_picture`, `rotatePicture()`, cache-bust URLs, `Markup::create()` |
| `src/Controller/SeasonController.php` | `field_player_picture`, cache-bust URLs, `Markup::create()`, `#cache` |
| `src/Controller/TournamentController.php` | `field_player_picture`, cache-bust URLs, `Markup::create()` |

---

## TODOs (Carried Forward + New)

**Photo Validation — First Pass Only (DISCUSS WITH CALEB):**
Face detection is currently a warning only — does not block upload. Need to decide:
- Should upload FAIL if no face is detected? Or keep as warning with admin cleanup?
- Need more testing with a variety of photos (sunglasses, hats, group shots, non-face images)
- Photo field is currently NOT required — must make it required before go-live

**DOB Mobile Display:**
Date of Birth field shows as a tiny empty box on mobile phones instead of "mm/dd/yyyy" like desktop. Multiple CSS and PHP approaches were tried and all reverted. Needs a different approach — possibly a custom Twig template override for the datetime widget on mobile, or a JS-based date picker.

**End-to-End Testing:**
Full registration flow testing covering age enforcement, age overrides, waitlist overrides, checkout, and notification delivery.

**Season Overrides — Extend/Nudge/Revoke redirect:**
Those actions currently redirect to the global overrides page. Can add `destination` query param later if needed.

**iCal Subscription Feed:**
Token-based system exists. Still needs testing in Google Calendar on InMotion.

**Data Migration:**
Pending board decision on user pruning cutoff (2yr/3yr/5yr lookback).

**Jersey Reorder Report (future):**
Separate report showing jersey size totals across all purchases for reorder purposes. Not urgent — notification covers the operational need for now.

---

## Previous Session Archive
Previous handoff archived to: `archive/SESSION_HANDOFF_2026_02_22_menu_jersey.md`

---

**Session Status:** COMPLETE — Committed
