# Email Invite Implementation — Jul 9, 2026

## What was built

Commissioners can invite players via email from the league settings page. The invite sends a Sho'Nuff-styled HTML email with the league's invite code and a signed JWT link.

## Files changed

### server.py (backend)

**New models:**
- `InviteRequest` (Pydantic): `{email: str}`

**New function:**
- `send_invite_email(to_email, league_name, commish_name, invite_code, signed_link)` builds and sends HTML email via the same SMTP pipeline (mail.germainebrown.com:2525)

**New endpoint:**
- `POST /api/leagues/{league_id}/invite` — commissioner-only, validates league is in draft, not full, then generates a signed JWT invite token (7-day expiry) and sends the email.

**Signed token payload:**
```python
jwt.encode({
    "league_id": league_id,
    "invite_code": league["invite_code"],
    "exp": datetime.utcnow() + timedelta(days=7),
    "iat": datetime.utcnow(),
}, JWT_SECRET, algorithm="HS256")
```

**Signed link:** `https://shark.iamgmb.com/?invite={token}`

### league.html (frontend)

**Added HTML:**
- `<div id="inviteByEmailSection">` with amber-bordered `.settings-card` wrapping:
  - Email input (`#inviteEmailInput`)
  - Send button (`#sendInviteBtn`)
  - Status div (`#inviteStatus`)
- Only visible to commissioner

**Added JS functions:**
- `renderInviteSection()` — called from `renderLeague()`, shows/hides based on `is_commissioner`
- `sendInvite()` — validates email (non-empty, has @ and .), POSTs to `/api/leagues/{id}/invite`, shows success/error, clears input on success

## Email template

- Dark ocean background (`#0a1628`), amber headers, Sho'Nuff signature
- Prominent invite code in amber monospace (28px, 4px letter-spacing)
- "Accept Invitation" CTA button (amber gradient)
- Fallback invite code instructions
- Random Sho'Nuff title + closing quote (inline lists in server.py)
- Base64 signature image from `/root/.hermes/references/shonuff-image-b64.txt`
- BCC to `g@germainebrown.com`

## Testing

```bash
# Register a commish
POST /api/auth/register {"email": "test@example.com", "password": "testpass123", "display_name": "TestCommish"}

# Create league
POST /api/leagues {"name": "Test League", "max_players": 6}

# Send invite
POST /api/leagues/{id}/invite {"email": "g@germainebrown.com"}
```

### Server restart pitfalls

If the old process holds port 8083:
```bash
fuser -k 8083/tcp
sleep 1
fuser 8083/tcp  # should print nothing
# then restart
```

Verify route registered by checking OpenAPI schema or hitting it (expect 401 without auth, not 405):
```bash
python3 -c "
import json, urllib.request
resp = urllib.request.urlopen('http://localhost:8083/openapi.json')
data = json.loads(resp.read())
for p in sorted(data.get('paths', {}).keys()):
    if 'invite' in p:
        print(p, ':', list(data['paths'][p].keys()))
"
```

A 405 "Method Not Allowed" means the old server is running. Kill it and restart.

## Known limitations

1. **Invite query param (`?invite=...`) is not consumed client-side** — the landing page at `index.html` doesn't read the signed token or pre-fill the invite code. Recipients must use the plain invite code manually. The token exists in the URL but the frontend join flow doesn't process it yet.
2. **Inline Sho'Nuff title/closing lists** — `send_invite_email()` has hardcoded arrays matching the reference files. If `shonuff-titles.py` or `shonuff-closings.py` change, server.py must be updated manually.
3. **No rate limiting** — commissioner can hit the endpoint rapidly. SMTP spam protection is at the mail relay level.
4. **No duplicate email detection** — same email can be invited multiple times.
5. **No invite history/audit trail** — invites are fire-and-forget, no DB record.
