# 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:** - `
` 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.