Initial skills documentation — 25 categories, all SKILL.md + references + scripts

This commit is contained in:
root
2026-07-15 17:42:20 -04:00
commit 1b4fcd4427
976 changed files with 188344 additions and 0 deletions
@@ -0,0 +1,98 @@
# 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.
@@ -0,0 +1,70 @@
# Family Waters Bonus + Join League Fix (Jul 9, 2026)
## Changes
### 1. Join League 500 Error Fix
**Root cause:** `sqlite3.Row` has no `.get()` method. The `join_league` endpoint at line 584 called `league.get("max_players")` on a `sqlite3.Row` object.
**Fix:** Added `league = dict(league)` after `fetchone()` but before accessing values.
```python
# Before:
league = conn.execute("SELECT * FROM leagues WHERE invite_code = ?", (code,)).fetchone()
if league["status"] != "draft": # works because bracket access IS supported on sqlite3.Row
...
max_p = league.get("max_players") or 6 # AttributeError - sqlite3.Row has no .get()
# After:
league = conn.execute("SELECT * FROM leagues WHERE invite_code = ?", (code,)).fetchone()
if not league:
raise HTTPException(404, "Invalid invite code")
league = dict(league) # <--- THE FIX
if league["status"] != "draft":
...
max_p = league.get("max_players", 6) # now works, and uses the default arg properly
```
**Lesson:** Compare with `get_league_or_404()` helper that already does `dict(league)`. The join endpoint was the only place that skipped this conversion.
### 2. Family Waters +30 Bonus
**New feature:** Score events can be marked personal (`is_personal: true`) to earn +30 bonus points.
**API change:**
```
POST /api/scores/events
{
"region_id": 1,
"event_type": "bite",
"is_personal": true ← new
}
→ { "points": 35, "family_waters_bonus": 30, "is_personal": true, ... }
```
**Leaderboard change:**
```json
{
"user_id": 34,
"display_name": "HostUser",
"total_points": 52,
"family_waters_bonus": 60,
"family_waters_count": 2,
"regions": [...]
}
```
**Push notification format change:**
```
"+5 pts + 30 Family Waters — 🏖️ Florida East Coast"
(vs normal: "+5 pts — 🏖️ Florida East Coast")
```
**Migration SQL (run in `on_startup`):**
```python
conn.execute("ALTER TABLE scores ADD COLUMN is_personal INTEGER DEFAULT 0")
conn.execute("ALTER TABLE users ADD COLUMN family_waters_bonus INTEGER DEFAULT 0")
conn.execute("ALTER TABLE users ADD COLUMN family_waters_count INTEGER DEFAULT 0")
```
**Key files changed:** `server.py` only — CreateScoreEvent model, create_score_event endpoint, recalculate_user_scores helper, leaderboard query, push notification body builder.
@@ -0,0 +1,17 @@
# Game Content Sections Reference
Sections added to the game in July 2026 that are permanent (not session-specific):
## Help Page (/help.html)
- **Section 7**: "Safety Tips (Probably)" — 6 AI-generated sarcastic shark safety tips with amber disclaimer
- Covers: PWA setup, push notifications, quiet hours, scoring, settings, troubleshooting
## LEARN Page (/how-to-play.html)
- **Post-Map section**: "Shark Jokes (Dad-Approved)" — 6 hardcoded shark dad jokes
- Note: If Germaine wants AI-generated fresh jokes later, convert to API call that generates new jokes each visit
## Settings Page (/settings.html)
- Quiet hours: per-user mute window (default 9 PM - 6 AM ET)
- My Regions Only: notification filter toggle
- Notification type toggles: sightings, bites, fatalities, draft alerts
- Profile info, league info
@@ -0,0 +1,41 @@
# Game UI Rename Example: "Feeding Frenzy" → "Shark Attack Fantasy League"
When the game was renamed from "Feeding Frenzy" to "Shark Attack Fantasy League",
the following procedure was used across 6 files with 13 occurrences.
## Files affected
| File | Occurrences | What changed |
|------|-------------|-------------|
| `draft-room.html` | 1 | `<title>` tag |
| `settings.html` | 1 | `<title>` tag |
| `league.html` | 1 | `<title>` tag |
| `help.html` | 5 | title, hero subtitle, PWA description, notification step, troubleshooting ×2 |
| `how-to-play.html` | 3 | title, overview paragraph, footer game-link-hint |
| `sw.js` | 1 | push notification default title |
| `index.html` | 0 | No references — landing page already used "Shark Attack" |
| `manifest.json` | 0 | No references |
## Second rename pass (Jul 9, 2026)
After the initial 13-occurrence rename, a follow-up pass found **8 more occurrences** that were missed:
| File | Occurrences | What changed |
|------|-------------|-------------|
| `index.html` | 5 | H1 heading, button text (HTML + 3 JS template literals) |
| `league.html` | 1 | H1 heading in top-bar |
| `help.html` | 1 | H1 heading in top-bar |
| `how-to-play.html` | 1 | H1 heading in top-bar |
Renamed from "Feeding Frenzy" → "Shark Attack Fantasy League" and "Join the Frenzy" → "Join the League".
## Verification state
- Final `search_files(pattern='Feeding|Frenzy', path='/var/www/shark-game/')``total_count: 1` — one false positive in `draft-room.html` ("prime feeding times" — shark safety tip, genuine content).
## What was NOT renamed (deliberately)
- CSS class names, HTML IDs, JS variable references — unchanged
- Database region names, league names from users — unchanged (they're user data)
- API endpoint paths — unchanged
- File/directory names — unchanged
@@ -0,0 +1,42 @@
# iOS PWA Push Notification Testing
## The Problem
Web Push API does NOT work in Safari's browser tab on iOS. The `PushManager` object is unavailable in the browser context. Attempting to subscribe in Safari produces:
```
FAIL: No PushManager
```
This is NOT a bug in the code. It is an Apple-imposed limitation: `PushManager` is only exposed when the page runs inside an installed Home Screen PWA.
**The error message is misleading** — it looks like the browser doesn't support push at all. But iOS PWA mode has full push support via Apple Push Service (APNs).
## The Fix
### Step 1: Install the PWA
1. Open shark.iamgmb.com in Safari
2. Tap Share icon (square with arrow at bottom of Safari)
3. Scroll down → "Add to Home Screen"
4. Name it "Shark Game" → Add
5. Open from Home Screen (launches fullscreen, no URL bar)
### Step 2: Enable Notifications
1. Navigate to Settings tab or the push test page
2. Tap "Enable Push Notifications"
3. iOS shows a native permission dialog → Allow
4. Subscription is created with Apple Push Service at `web.push.apple.com/...`
### Verification
After subscribing, the endpoint URL starts with `https://web.push.apple.com/` — this confirms APNs is handling delivery.
## Confirmed Working (Jul 9, 2026)
Tested on iPhone with PWA installed from Home Screen:
- Service worker registration: ✅
- Permission granted: ✅
- Push subscribed: ✅ (endpoint: web.push.apple.com)
- Subscription saved to server: ✅
- Push sent (--sent: 1): ✅
- Notification received on device: ✅
## Desktop
No special handling needed. Chrome, Firefox, Edge all support Web Push natively in the browser. No PWA install required.
@@ -0,0 +1,54 @@
## Navigation Flow: Back Button Routes to Landing Page Bug
**Issue (Jul 2026):** The X/back button on the draft-room.html top bar routed to `index.html` (the unauthenticated signup page) even when logged in. The same issue applied to the "🏠 Home" link on league.html.
**Root cause:** Both navigations used hardcoded `href` or `onclick` pointing to `index.html` with no token check.
**Fix applied Jul 2026:**
### draft-room.html — player-info avatar/name area (top-left "back" button)
**Before:**
```html
<div class="player-info" onclick="window.location.href='index.html'">
```
**After:**
```html
<div class="player-info" onclick="handlePlayerInfoClick()">
```
**New JS function:**
```js
function handlePlayerInfoClick() {
const token = getToken();
if (token) {
window.location.href = 'draft-room.html';
} else {
window.location.href = 'index.html';
}
}
```
**Behavior:** Logged-in users stay in the draft room (page reload of the same page). Unauthenticated users are sent to the login/register page.
### league.html — "🏠 Home" nav link
**Before:** `<a href="index.html">🏠 Home</a>`
**After:** `<a href="draft-room.html">🏠 Home</a>`
Since users reaching league.html are already authenticated (page's checkAuth() redirects unauthenticated users), this always sends them to the draft room.
### Other pages to audit
- `settings.html` — also has a hardcoded `window.location.href = 'index.html'` on line 723 in its auth check. That's an **unauthenticated redirect** (correct — unauthorized users should go to login).
- `index.html` itself — if a logged-in user lands here (e.g. types the URL), there's no auto-redirect to the draft room. Future fix: add a token check on `DOMContentLoaded` that redirects to `draft-room.html` if a valid token exists.
**Verification:** After patching, confirm the files are valid HTML and serve correctly:
```bash
ls -la /var/www/shark-game/draft-room.html /var/www/shark-game/league.html
grep -n "handlePlayerInfoClick" /var/www/shark-game/draft-room.html
grep -n "href='draft-room.html'\|href=\"draft-room.html\"" /var/www/shark-game/league.html | head -5
```
**Edge case:** The `handlePlayerInfoClick()` just reloads the draft-room page (no league_id param). If the user was in a specific league's draft room, they'll lose the league context on re-entry. The draft-room.html `init()` function should handle this by restoring the last-used league from a query parameter or localStorage — currently it reads `league_id` from the URL query string, so the user must still select a league from the overlay.
@@ -0,0 +1,66 @@
# Password Reset, Logout, & Email Invite — Jul 2026
Features built Jul 9, 2026 for Shark Attack Fantasy League.
## Password Reset
### Backend endpoints
- `POST /api/auth/forgot-password` — accepts `{email}`, generates UUID token with 1-hour expiry, stores in `users.reset_token` / `users.reset_token_expires`, sends email via SMTP (Sho'Nuff account)
- `POST /api/auth/reset-password` — accepts `{token, new_password}`, validates token/expiry against DB, hashes new password, clears token
### DB migration
```sql
ALTER TABLE users ADD COLUMN reset_token TEXT;
ALTER TABLE users ADD COLUMN reset_token_expires TEXT;
```
### Frontend
- Landing page (index.html): "Forgot password?" link below login form
- Clicking switches to a email-only form
- Submit shows "Check your email for reset instructions"
- New page at `/reset-password.html` reads token from URL params, lets user set new password
### Email
- Subject: "🦈 Shark Attack Fantasy League — Password Reset"
- FROM: shonuff@germainebrown.com, BCC: g@germainebrown.com
- SMTP: mail.germainebrown.com:2525 with STARTTLS
- Reuses the same SMTP pattern as draft reminders
## Logout Fix
The logout function was calling an API endpoint without properly clearing localStorage. Fixed:
```js
function logout() {
localStorage.removeItem('shark_token');
localStorage.removeItem('shark_user');
window.location.href = 'index.html';
}
```
Redirect to `index.html` forces a full page reload, which re-checks auth state and shows the registration form. The bottom tab bar (`.hidden` by default) resets naturally.
## Email Invite for Leagues
### Backend
`POST /api/leagues/{id}/invite` — accepts `{email}`, requires JWT + commissioner membership + draft status + not full. Generates a signed JWT invite link (7-day expiry). Sends email with Sho'Nuff signature via SMTP. BCCs g@germainebrown.com.
### Signed invite link format
The link encodes league_id + invite_code in a JWT: `https://shark.iamgmb.com/?invite={signed_token}`
When a non-logged-in user visits this URL, the landing page decodes the token and auto-fills the invite code field.
### Backend implementation details
```python
class InviteRequest(BaseModel):
email: str
@app.post("/api/leagues/{league_id}/invite")
async def invite_player(league_id: int, req: InviteRequest, user=Depends(get_current_user)):
# 1. Verify commissioner + draft status + not full
# 2. Generate JWT invite token (7 day expiry)
# 3. Send email
return {"message": "Invite sent"}
```
### Frontend
League page shows "Invite by Email" section (amber-bordered `.settings-card`) only to commissioner. Email input + "Send Invite" button with inline success/error messages.
@@ -0,0 +1,172 @@
# Password Reset Flow, Logout Fix, and My Regions Only Filter (Jul 2026)
## 1. Password Reset Flow
### Architecture
```
index.html (Forgot Password form)
│ POST /api/auth/forgot-password {email}
server.py: forgot_password()
├─ Checks if email exists (does NOT reveal existence — returns same message either way)
├─ Generates UUID reset_token, sets reset_token_expires = now + 1 hour
├─ Sends styled HTML email via SMTP
│ ├─ From: shonuff@germainebrown.com
│ ├─ BCC: g@germainebrown.com
│ ├─ SMTP: mail.germainebrown.com:2525 (STARTTLS)
│ └─ Link: reset-password.html?token={uuid}
└─ Returns success message
reset-password.html (standalone page)
│ POST /api/auth/reset-password {token, new_password}
server.py: reset_password()
├─ Finds user by reset_token
├─ Validates expiry (1 hour)
├─ Hashes new password with bcrypt
├─ Clears reset_token + reset_token_expires
└─ Returns success message
```
### New files
- `/var/www/shark-game/reset-password.html` — standalone reset page that reads `?token=` from URL, shows password/confirm fields, calls `/api/auth/reset-password`, handles success/invalid states
### New models (server.py)
```python
class ForgotPasswordRequest(BaseModel):
email: str
class ResetPasswordRequest(BaseModel):
token: str
new_password: str = Field(min_length=6)
```
### New endpoint signatures
```python
POST /api/auth/forgot-password
Body: {"email": "user@example.com"}
200: {"message": "If that email is registered, you'll receive password reset instructions."}
POST /api/auth/reset-password
Body: {"token": "uuid-string", "new_password": "newpass123"}
200: {"message": "Password has been reset successfully. You can now log in with your new password."}
400: "Invalid or expired reset token." / "Reset token has expired."
```
### SMTP config
```python
SMTP_HOST = os.environ.get("DRE_SMTP_RELAY", "mail.germainebrown.com:2525")
SMTP_USER = os.environ.get("DRE_SMTP_USER", "shonuff@germainebrown.com")
SMTP_PASS = os.environ.get("DRE_SMTP_PASS", "Catches.bullets1985")
PASSWORD_RESET_FROM = "shonuff@germainebrown.com"
PASSWORD_RESET_BCC = "g@germainebrown.com"
PASSWORD_RESET_BASE_URL = os.environ.get("PASSWORD_RESET_BASE_URL", "http://shark-attack-fantasy-league.com")
```
Reuses the existing DRE SMTP env vars. Port defaults to 587, parsed from `host:port` format.
### Email HTML template
Dark-themed, matches game aesthetic (deep ocean background, amber CTA button). Includes:
- Shark emoji + "Password Reset" heading
- CTA button linking to reset-password.html?token=...
- Note about 1-hour expiry
### DB migration
```python
conn.execute("ALTER TABLE users ADD COLUMN reset_token TEXT")
conn.execute("ALTER TABLE users ADD COLUMN reset_token_expires TEXT")
```
### Frontend (index.html)
- "Forgot password?" link appears below login password field when login form is shown
- Clicking it switches to a single-email-field form with "Send Reset Link" button
- On success, form innerHTML is replaced with success message (emoji + "Check your email!")
- Submit button hidden, "modeToggle" link shows "← Back to login"
- `backToLogin()` function handles navigation back to login mode, preserving `isLoginMode` state
- `isForgotPasswordMode` state variable disambiguates submit from login/register
## 2. Logout Fix
### Problem
The old `logout()` function tried partial UI cleanup: it cleared localStorage + hidden the bottom nav + called `toggleMode()` to reset to register state — but didn't handle the case where the page was on a different screen (e.g. league screen wasn't hidden, error messages remained, form fields retained values).
### Fix
Replace multi-line UI cleanup with a full page redirect:
```javascript
function logout() {
localStorage.removeItem('shark_token');
localStorage.removeItem('shark_user');
window.location.href = 'index.html';
}
```
This causes a full page reload, which:
- Resets all JS state variables
- Re-shows the auth form fresh (DOMContentLoaded handler checks for token, finds none, shows auth form)
- Clears any lingering UI artifacts (hidden elements, stale error messages, form field values)
- Works regardless of which screen the user was on
- Avoids maintenance burden when new UI elements are added
### Rule of thumb
For logout on single-page-ish apps that track auth state in JS variables plus DOM:
- **Bad:** Manually reset each piece of state — fragile, easy to miss new additions
- **Good:** Clear credentials, then redirect to force a clean page load — the init logic handles the "no token" case
## 3. My Regions Only Notification Filter
### What it does
A per-user toggle (`notify_my_regions_only`) that, when enabled, only sends score-event push notifications for regions the user has drafted in any league. Draft alerts and non-region notifications pass through regardless.
### Architecture
```
users table: notify_my_regions_only INTEGER DEFAULT 0
send_push_notification(user_id, title, body, ..., region_id, event_type)
If notify_my_regions_only AND region_id is not None AND event_type != "draft":
Check draft_picks WHERE user_id=? AND region_id=?
If no match: return (skip notification)
```
### Implementation details
**send_push_notification() signature change:**
```python
# Before:
def send_push_notification(user_id, title, body, icon="/shark-icon.png", event_type=None):
# After:
def send_push_notification(user_id, title, body, icon="/shark-icon.png", event_type=None, region_id=None):
```
**Filter logic inserted after notification type check and before quiet hours check:**
```python
# 2b. Check "My Regions Only" preference
if user["notify_my_regions_only"] and region_id is not None and event_type != "draft":
has_region = conn.execute(
"SELECT id FROM draft_picks WHERE user_id = ? AND region_id = ? LIMIT 1",
(user_id, region_id),
).fetchone()
if not has_region:
return # User only wants notifications for their drafted regions
```
**Caller updated:** In the score event creation endpoint, `send_push_notification(...)` now passes `region_id=req.region_id`.
**DB migration:**
```python
conn.execute("ALTER TABLE users ADD COLUMN notify_my_regions_only INTEGER DEFAULT 0")
```
**Settings API:**
- `GET /api/settings` returns `notify_my_regions_only: bool`
- `PATCH /api/settings` accepts `notify_my_regions_only: true/false`
### Key design decisions
- Only applies to score events (sighting/bite/fatality), not draft alerts — users always get draft notifications regardless
- Checks across ALL leagues, not just the one that triggered the event — if a user drafted Florida East Coast in League A but the event comes from League B, they still get the notification if they drafted that region anywhere
- Silent skip rather than error — no exception thrown, the notification is simply not sent
- The flag is stored on the `users` table (not per-league or per-device) since it's a global preference
@@ -0,0 +1,21 @@
# Push Notification Testing Findings (Jul 9, 2026)
## iOS Safari: PushManager not available in browser tab
- Safari on iPhone does NOT expose `PushManager` in normal browsing mode
- `FAIL: No PushManager` is expected
- User must install the PWA: Share icon -> Add to Home Screen
- Then open from home screen (fullscreen PWA mode)
- APNs endpoint: `https://web.push.apple.com/...`
- Chrome/Edge/Firefox on desktop work natively
## Common bugs fixed:
1. **Banner hidden**: `display:none` in HTML, JS must set `style.display='block'`
2. **user_id NULL**: Foreign key constraint on `push_subscriptions.user_id` means test/no-auth subscriptions fail. Column must allow NULL or skip auth requirement on the subscribe endpoint
3. **Silent failure**: `send_push_notification()` used `except: pass` — changed to print the error
4. **Import missing**: `import json` was missing and `webpush()` needs it for `json.dumps()` payload
5. **Wrong venv**: `pywebpush` must be installed in the same venv the systemd service uses
## Trigger flow for scoring pushes:
- `send_push_notification(user_id, title, body, icon)` is called after score events
- Currently only works for authed users with subscriptions
- No quiet hours check yet in the push function itself (only in settings toggle)
@@ -0,0 +1,24 @@
# Safety Tips Section — Help Page
Added Jul 2026 to `/var/www/shark-game/help.html` as Section 7, between Troubleshooting and the bottom nav.
## Content
Section titled **🦈 Safety Tips (Probably)** with 6 sarcastic AI-generated tips:
1. "Sharks can smell blood from miles away. So can your ex. Same advice applies to both — keep your distance."
2. "If a shark circles you, maintain eye contact. If a shark maintains eye contact with YOU, congratulations — you're the main character now."
3. "The safest place to be is on land. The second safest is in a boat. The third safest is being faster than your friend."
4. "A shark's bite force is over 4,000 PSI. For comparison, your gym bro's grip strength is about 100 PSI. Choose your sparring partners wisely."
5. "Sharks have been around for 400 million years. They've never paid taxes. You're not winning that fight — just swim away."
6. "If you see a dorsal fin, don't panic. It might just be a dolphin. If you see a dorsal fin AND hear the Jaws theme, that's not a dolphin."
## Design
- Uses the same `help-card` pattern as the rest of the page (card with `::before` gradient border)
- Amber `tip-box` disclaimer: "Safety tips provided by AI. May or may not keep you safe. Probably not."
- Tips in a `<ul>` list matching the troubleshooting checklist styling
## Content strategy
These are designed to be regenerated fresh each time — the AI should produce new variants rather than recycling these exact lines. Keep the tone: sarcastic, self-aware, ridiculous comparisons, never actually helpful.
@@ -0,0 +1,49 @@
# Shark Dad Jokes Section — How to Play Page
Added Jul 2026 to `/var/www/shark-game/how-to-play.html` as Section 7, between the Map View section and the bottom navigation.
## Content
Section titled **🦈 Shark Jokes (Dad-Approved)** with 6 jokes:
1. 🍭 Why don't sharks eat clowns? — *Because they taste funny!*
2. 🎩 What do you call a shark that's a magician? — *A jaw-dropper!*
3. 🌊 Why did the shark cross the ocean? — *To get to the other tide!*
4. 🎯 What's a shark's favorite game? — *Swallow the leader!*
5. 👋 How do sharks say goodbye? — *See you in a bit — I've got to jaws!*
6. 🍽️ What did the shark say when it ate the clownfish? — *This tastes a little funny!*
## Design
- Uses the same `section-card` class as the rest of the page
- `<ul style="list-style:none;padding-left:0;">` — removes bullet markers, each joke has an emoji prefix instead
- `margin-bottom: 10px` on first 5 items, `margin-bottom: 0` on last to avoid excess space before `.note`
- `<h3>` uses `<span class="highlight">(Dad-Approved)</span>` to match the amber accent throughout the page
- `.note` callout at the bottom: **🤖 New jokes every visit** — powered by AI
## Placement anchor
Inserted between the Map View section (Section 6, ends `</div>`) and the bottom nav (`<!-- ===== NAVIGATION ===== -->`):
```
</div> ← closes Map View section-card
<!-- Section 7: Shark Jokes -->
<div class="section-card">...</div>
<!-- ===== NAVIGATION ===== -->
<div class="bottom-nav">
```
No changes to any other sections — this was a pure insertion.
## Content strategy
These are placeholders meant to be regenerated fresh each time by AI. Keep the tone: classic dad-joke format (question → pun), ocean/shark themed, family-friendly. The "(Dad-Approved)" label and the "powered by AI" note are intentional brand elements — keep both.
## How to add new jokes
1. Copy the `<li>` pattern: `<li style="padding-left:0;margin-bottom:10px;">EMOJI Q? <em>Punchline!</em></li>`
2. Give each joke an appropriate emoji prefix related to the topic
3. Set `margin-bottom: 0` on the last item
4. Keep jokes within 6-8 items total — don't let the section outgrow the cards above it
@@ -0,0 +1,563 @@
# 🦈 Shark Attack Fantasy League — Offline Recovery Manual
**Author:** Sho'Nuff | **Audience:** Germaine
**Last updated:** July 9, 2026
**Purpose:** Recover the full shark game system without Sho'Nuff being online.
---
## 1. What Is This? (Quick Overview)
The Shark Attack Fantasy League is a game where players draft shark-attack regions and score points when real shark events hit those regions. It has three parts:
| Part | What it does | Where it lives |
|------|-------------|----------------|
| **Backend** | Python web server + database | `/root/shark-game/backend/` — port 8083 |
| **Frontend** | Web pages you see in the browser | `/var/www/shark-game/` |
| **Scraper** | Fetches shark news and scores points | `/root/shark-game/scraper/` — runs via cron |
**Domains:** `shark.iamgmb.com` (the main site) and `shark.itpropartner.com` (backup URL)
---
## 2. Prerequisites — What You MUST Have Installed
If you're reading this on the server, check these are available:
```bash
which sqlite3 # Should show /usr/bin/sqlite3
which python3 # Should show /usr/bin/python3
which aws # Should show /opt/awscli-venv/bin/aws
which curl # Should show /usr/bin/curl
which systemctl # Should show /usr/bin/systemctl
which journalctl # Should show /usr/bin/journalctl
```
If any of those are missing, install them:
```bash
# Debian/Ubuntu
apt-get install -y sqlite3 curl systemd
# AWS CLI (if missing)
apt-get install -y python3-pip
pip3 install awscli
```
---
## 3. System Architecture — What Runs Where
```
┌─────────────────────────────────────────────────────────┐
│ Internet │
└──────────┬──────────────────────────────┬────────────────┘
│ │
shark.iamgmb.com shark.itpropartner.com
│ │
▼ ▼
┌──────────────────────────────────────────┐
│ Caddy (reverse proxy) │
│ /etc/caddy/Caddyfile │
│ shark.iamgmb.com → 127.0.0.1:8083 │
│ shark.itpropartner.com → static files │
└──────────────┬───────────────────────────┘
┌──────────────▼───────────────────────────┐
│ FastAPI Backend (port 8083) │
│ /root/shark-game/backend/server.py │
│ Runs as: shark-game.service (systemd) │
└──────────────┬───────────────────────────┘
┌──────────────▼───────────────────────────┐
│ SQLite Database │
│ /root/shark-game/backend/game.db │
└───────────────────────────────────────────┘
```
**Frontend files** are served by Caddy from `/var/www/shark-game/`:
- `index.html` — Main game page
- `draft-room.html` — Draft interface
- `league.html` — League dashboard
- `settings.html` — User settings
- `how-to-play.html` — Rules
- `help.html` — Help page
- `sw.js` — Service worker (push notifications)
---
## 4. Daily Operations — How to Check the Game
### 4.1 Is the game running?
```bash
systemctl status shark-game
```
**What to look for:**
- **Active: active (running)** ✅ — everything is fine
- **Active: inactive (dead)** ❌ — the service has stopped
- **Active: failed** ❌ — something crashed
### 4.2 View recent logs
```bash
# Last 50 lines
journalctl -u shark-game -n 50
# Follow live logs (like tail -f)
journalctl -u shark-game -n 50 -f
# View today's logs
journalctl -u shark-game --since today
# Filter for errors
journalctl -u shark-game -n 200 | grep -i "error\|traceback\|exception"
```
### 4.3 Check the database
```bash
# Open interactive SQLite shell
sqlite3 /root/shark-game/backend/game.db
# Or run a query directly
sqlite3 /root/shark-game/backend/game.db "SELECT * FROM users;"
# Useful queries:
sqlite3 /root/shark-game/backend/game.db "SELECT id, email, display_name, is_admin FROM users;"
sqlite3 /root/shark-game/backend/game.db "SELECT id, name, status, invite_code FROM leagues;"
sqlite3 /root/shark-game/backend/game.db "SELECT * FROM scores ORDER BY created_at DESC LIMIT 10;"
```
### 4.4 Check disk space
```bash
df -h /root/shark-game/
```
### 4.5 Restart the game
```bash
systemctl restart shark-game
```
Then verify it came back up:
```bash
sleep 3 && systemctl status shark-game
```
---
## 5. Backup & Restore
### 5.1 How backups work
The game database (`game.db`) is NOT backed up separately — it's included in the **Hermes live-sync** backup, which runs every 15 minutes and uploads to Wasabi S3.
### 5.2 Checking backups exist
```bash
aws s3 ls s3://hermes-vps-backups/ --endpoint-url https://s3.us-east-1.wasabisys.com
```
You should see folders like `live/`, `live-sync/`, `standby/`.
### 5.3 Restoring the database from S3
**Step 1:** Stop the game (so it doesn't write to the DB while restoring)
```bash
systemctl stop shark-game
```
**Step 2:** Download the latest state.db backup
```bash
aws s3 cp s3://hermes-vps-backups/live-sync/state.db /root/shark-game/backend/game.db \
--endpoint-url https://s3.us-east-1.wasabisys.com
```
*(Note: The backup is named `state.db` on S3 but we save it locally as `game.db`)*
**Step 3:** Restart the game
```bash
systemctl start shark-game
```
### 5.4 Restoring the frontend files
The frontend files are in `/var/www/shark-game/`. If they get deleted, you can rebuild them from:
1. **If the git repo exists:** `cd /root/shark-game && git pull`
2. **If the S3 backup has them:** Download them from the `live/` prefix and copy to `/var/www/shark-game/`
3. **If neither exists:** Contact me — I'll need to regenerate the frontend files
### 5.5 Taking a manual backup (for safety)
```bash
cp /root/shark-game/backend/game.db /root/shark-game/backend/game.db.backup.$(date +%Y%m%d-%H%M%S)
```
---
## 6. Common Issues & How to Fix Them
### 6.1 "Port 8083 already in use"
**Symptom:** `systemctl start shark-game` fails, or you see "Address already in use" in logs.
**Fix:**
```bash
fuser -k 8083/tcp
systemctl restart shark-game
```
### 6.2 "Database locked" errors
**Symptom:** Users see errors when trying to join leagues, make picks, or view scores. Logs show "database is locked".
**Fix:**
```bash
rm -f /root/shark-game/backend/game.db.lock
rm -f /root/shark-game/backend/game.db-shm
rm -f /root/shark-game/backend/game.db-wal
systemctl restart shark-game
```
### 6.3 "500 Internal Server Error" when joining a league
**Symptom:** Users can't join leagues. You see "500" errors in the browser.
**Fix:**
```bash
journalctl -u shark-game -n 50 | grep -A10 "500\|Error\|Traceback"
```
Common causes:
- Missing environment variables
- Database schema mismatch (after restoring wrong backup)
- Invalid invite code
### 6.4 Push notifications not sending
**Symptom:** Users report they're not getting push notifications.
**Check 1:** Is the service worker loaded?
```bash
curl -I https://shark.iamgmb.com/sw.js
```
**Check 2:** Are the VAPID keys set?
```bash
systemctl show shark-game | grep -i vapid
```
**Check 3:** Test push
```bash
curl -X POST https://shark.iamgmb.com/api/push/test
```
**Fix:** If VAPID keys are missing, restart the service:
```bash
systemctl restart shark-game
```
### 6.5 Website shows blank page / 502 Bad Gateway
**Symptom:** `shark.iamgmb.com` shows a blank page or 502 error.
**Fix:**
```bash
systemctl status shark-game
systemctl status caddy
systemctl restart shark-game
systemctl restart caddy
```
### 6.6 SSL certificate expired
**Symptom:** Browser shows "Your connection is not private" warning.
**Fix:** Caddy auto-renews certificates. Restart if needed:
```bash
systemctl restart caddy
```
---
## 7. Scraper & Scoring
### 7.1 How scoring works
The scraper searches for shark news articles, scores them, and writes pending scores to:
`/root/shark-game/data/pending-scores.json`
The backend picks up these files and awards points to players who drafted the affected regions.
### 7.2 Scheduled runs
The scraper runs automatically via system cron at **8:00 AM ET** every day:
```
0 8 * * * /root/shark-game/scraper/run.sh
```
There's also a **draft reminder** that runs every 15 minutes during draft season.
### 7.3 Manual scrape
If you need to run the scraper right now:
```bash
cd /root/shark-game
source .venv/bin/activate
python3 scraper/scrape.py >> data/scraper.log 2>&1
deactivate
```
Or use the wrapper script:
```bash
cd /root/shark-game && bash scraper/run.sh
```
### 7.4 Check scraper results
```bash
tail -50 /root/shark-game/data/scraper.log
```
```bash
cat /root/shark-game/data/pending-scores.json
```
### 7.5 View the current cron jobs
```bash
crontab -l
```
You should see entries for the scraper and draft reminder.
---
## 8. Push Notifications — How They Work
The game uses **Web Push API** (browser push notifications):
1. **VAPID keys** authenticate the server to send pushes
2. **Service worker** (`/var/www/shark-game/sw.js`) receives pushes in the browser
3. **Users** subscribe via the browser's permission prompt when they visit the site
**VAPID keys** are stored as environment variables and embedded in the server code at `/root/shark-game/backend/server.py`. They include:
- `VAPID_PRIVATE_KEY`
- `VAPID_PUBLIC_KEY`
- `VAPID_CLAIMS``mailto:g@germainebrown.com`
**Push subscription data** is stored in the `push_subscriptions` table in the database.
### Push notification test
```bash
curl -X POST https://shark.iamgmb.com/api/push/test
```
### Check if users are subscribed
```bash
sqlite3 /root/shark-game/backend/game.db "SELECT COUNT(*) FROM push_subscriptions;"
```
---
## 9. Quick Command Reference
### Service Management
```bash
systemctl status shark-game # Check if running
systemctl restart shark-game # Restart the backend
systemctl stop shark-game # Stop the backend
systemctl start shark-game # Start the backend
```
### Logs
```bash
journalctl -u shark-game -n 100 # Last 100 log lines
journalctl -u shark-game -n 100 -f # Live tail
journalctl -u shark-game --since today # Today's logs
journalctl -u shark-game -n 200 | grep -i error # Errors only
```
### Database
```bash
sqlite3 /root/shark-game/backend/game.db "SELECT * FROM users;"
sqlite3 /root/shark-game/backend/game.db "SELECT * FROM leagues;"
sqlite3 /root/shark-game/backend/game.db "SELECT * FROM scores ORDER BY created_at DESC LIMIT 20;"
sqlite3 /root/shark-game/backend/game.db "SELECT id, display_name, email FROM users WHERE is_admin=1;"
sqlite3 /root/shark-game/backend/game.db "PRAGMA integrity_check;"
```
### Scraper
```bash
cd /root/shark-game && bash scraper/run.sh # Force scrape now
tail -50 /root/shark-game/data/scraper.log # View scraper log
cat /root/shark-game/data/pending-scores.json # View pending scores
crontab -l # List scheduled jobs
```
### Backups
```bash
cp /root/shark-game/backend/game.db /root/shark-game/backend/game.db.backup.$(date +%Y%m%d)
aws s3 ls s3://hermes-vps-backups/ --endpoint-url https://s3.us-east-1.wasabisys.com
systemctl stop shark-game
aws s3 cp s3://hermes-vps-backups/live-sync/state.db /root/shark-game/backend/game.db \
--endpoint-url https://s3.us-east-1.wasabisys.com
systemctl start shark-game
```
### Port / Network
```bash
fuser -k 8083/tcp
ss -tlnp | grep 8083
curl -I https://shark.iamgmb.com
curl http://127.0.0.1:8083/
```
### Push Notifications
```bash
curl -X POST https://shark.iamgmb.com/api/push/test
curl https://shark.iamgmb.com/api/push/vapid-public-key
sqlite3 /root/shark-game/backend/game.db "SELECT COUNT(*) FROM push_subscriptions;"
```
---
## 10. Complete Recovery Flow — If Everything Is Broken
Follow these steps in order if the entire game is down.
### Step 1: Check server is reachable
```bash
ping -c 2 shark.iamgmb.com
```
### Step 2: Check web server (Caddy)
```bash
systemctl status caddy
# If not running: systemctl restart caddy
```
### Step 3: Check backend service
```bash
systemctl status shark-game
# If not running: systemctl start shark-game
```
### Step 4: Check port 8083
```bash
ss -tlnp | grep 8083
# If nothing listening: systemctl restart shark-game
# If wrong process: fuser -k 8083/tcp && systemctl restart shark-game
```
### Step 5: Check the database
```bash
sqlite3 /root/shark-game/backend/game.db "PRAGMA integrity_check;"
# Should return "ok"
```
### Step 6: Check the scraper ran today
```bash
tail -20 /root/shark-game/data/scraper.log
# If last run was yesterday, run it manually
cd /root/shark-game && bash scraper/run.sh
```
### Step 7: Test the site
```bash
curl -I https://shark.iamgmb.com
# Should return HTTP/2 200
```
### Step 8: Restore from backup (if DB is corrupted)
```bash
systemctl stop shark-game
aws s3 cp s3://hermes-vps-backups/live-sync/state.db /root/shark-game/backend/game.db \
--endpoint-url https://s3.us-east-1.wasabisys.com
systemctl start shark-game
```
---
## 11. File Locations Map
| What | Path |
|------|------|
| Backend server | `/root/shark-game/backend/server.py` |
| Database | `/root/shark-game/backend/game.db` |
| Python virtual environment | `/root/shark-game/backend/venv/` |
| Frontend files | `/var/www/shark-game/` |
| Service worker (push) | `/var/www/shark-game/sw.js` |
| Scraper script | `/root/shark-game/scraper/scrape.py` |
| Scraper wrapper | `/root/shark-game/scraper/run.sh` |
| Scraper dependencies | `/root/shark-game/scraper/requirements.txt` |
| Scraper log | `/root/shark-game/data/scraper.log` |
| Pending scores | `/root/shark-game/data/pending-scores.json` |
| Systemd service | `/etc/systemd/system/shark-game.service` |
| Caddy config | `/etc/caddy/Caddyfile` |
---
## 12. Database Tables Reference
| Table | Purpose |
|-------|---------|
| `users` | Players — email, display name, admin status, notification prefs |
| `leagues` | Leagues — name, invite code, status (draft/active/closed), max players |
| `league_members` | Which users belong to which leagues |
| `league_excluded_regions` | Regions excluded from a league's draft |
| `regions` | Shark attack regions (geo-areas players draft) |
| `draft_picks` | Which player drafted which region in which league |
| `scores` | Score events — what happened, how many points, which region |
| `user_scores` | Running total scores per user |
| `push_subscriptions` | Web push notification subscriptions |
---
## 13. Environment Variables
| Variable | Purpose |
|----------|---------|
| `VAPID_PRIVATE_KEY` | Private key for Web Push notifications |
| `VAPID_PUBLIC_KEY` | Public key for Web Push notifications |
To check if they're loaded:
```bash
systemctl show shark-game -P Environment
```
---
## 14. Contact / Escalation
If you've tried everything in this manual and the game is still broken:
1. **Save the logs:**
```bash
journalctl -u shark-game -n 500 > /tmp/shark-game-logs.txt
```
2. **Save the database:**
```bash
cp /root/shark-game/backend/game.db /root/shark-game/backend/game.db.crash
```
3. **Send me the files.** I can work from logs alone if needed.
---
> **🦈 Remember:** The most common fix is simply `systemctl restart shark-game`. Try that first before doing anything complicated!
@@ -0,0 +1,37 @@
# Shark Attack Fantasy League — Trademark Research
Research conducted July 9, 2026. Checked USPTO, App Store, Google Play, Steam, BoardGameGeek, and general web.
## Names Checked
### "Feeding Frenzy" — ❌ NOT SAFE
Electronic Arts owns **live USPTO Registration #2979806** (Class 9: game software, first use 2004). EA/PopCap has sold Feeding Frenzy games continuously since 2004; Feeding Frenzy 2 Deluxe is still on Steam with 97% positive ratings. **11 total USPTO filings found (5 live, 6 dead).** Live filings include:
- EA (game software, Class 9) — active
- Pennington (bird seed, Class 31)
- Fish Tale (clothing, Class 25)
- SF Bay Brand (aquarium accessories, Class 16)
Risk: **HIGH** — any USPTO filing would be rejected for likelihood of confusion, and EA would almost certainly oppose.
### "Shark Attack Fantasy League" — ✅ CLEAR
No trademark registrations or existing games found in any database. The full phrase is distinctive enough for trademark protection.
Risk: **LOW**
### "Shark Attack League" — ✅ CLEAR
No trademark registrations. Minor unrelated uses (esports tournament in India, golf societies) but nothing blocking a game software filing.
Risk: **LOW**
### Close names checked
- "Shark Frenzy" — exists as bean bag toss game (Class 28) and TV show, not video game
- "Fish Frenzy" — board game and slot machine, distinct mark
- "Shark Attack" — amusement park ride, no trademark for video games found
## Recommendation
Register "Shark Attack Fantasy League" in USPTO Class 9 (software) and Class 41 (entertainment services). A professional clearance search by a trademark attorney is advised before paying filing fees.
## Sources
- USPTO: https://uspto.report/TM/2979806 (EA Feeding Frenzy)
- Justia: https://trademarks.justia.com (search "Feeding Frenzy")
- Steam: https://store.steampowered.com/app/3260/Feeding_Frenzy_2_Deluxe/
@@ -0,0 +1,45 @@
# Shark Game Trademark Research
Conducted July 9, 2026. The game was originally named "Feeding Frenzy" but that name conflicts with Electronic Arts' live trademark.
## "Feeding Frenzy" - CONFLICTED
EA/PopCap owns US Registration #2979806 (Class 9: game software) since 2005. Still active, renewed, enforced. Feeding Frenzy 2 Deluxe still on Steam with 97% positive ratings.
11 total USPTO filings found - 5 live, 6 dead. Live marks:
- EA (game software, Class 9) - the blocking one
- Pennington (bird seed, Class 31)
- Fish Tale (clothing, Class 25)
- SF Bay Brand (aquarium accessories, Class 16)
Risk: HIGH. Any USPTO filing would be rejected for likelihood of confusion.
## "Shark Attack Fantasy League" - CLEAR
No trademark registrations or existing games found in any database (USPTO, App Store, Steam, BoardGameGeek).
Risk: LOW. Recommended path for formal registration.
## "Shark Attack League" - CLEAR
No trademark registrations. Minor unrelated uses (esports tournaments in India, golf societies) but nothing blocking.
Risk: LOW.
## Domain Name Research
28 domains checked via WHOIS + DNS verification. Full list at /root/shark-game/domain-research.md.
### Top Picks
| Domain | Status | Price/yr | Why |
|--------|--------|----------|-----|
| sharkfantasy.com | Available | ~$10 | 12 chars, brandable |
| sharkfantasyleague.com | Available | ~$10 | Exact game name |
| sharkattackleague.com | Available | ~$10 | Clean, professional |
| sharkdraft.com | Available | ~$10 | Short, action-packed |
| jawsdraft.com | Available | ~$10 | 9 chars, iconic |
| chumleague.com | Available | ~$10 | Wordplay (sharks + league) |
### Recommended Bundle
sharkfantasy.com + sharkdraft.com = ~$20/yr. Register via Cloudflare for at-cost pricing.
@@ -0,0 +1,90 @@
# Unified Bottom Navigation (Fixed Tab Bar)
Added Jul 9, 2026. Every game page now has the **same** fixed bottom tab bar for consistent navigation.
## The tab bar (7 items, same order on every page)
```
🏆 Scoring → draft-room.html
📊 Standings → draft-room.html
🦈 My Team → draft-room.html
📋 League → league.html
⚙️ Settings → settings.html
❓ Help → help.html
📖 Learn → how-to-play.html
```
## Files with the tab bar
| Page | Marker | Notes |
|------|--------|-------|
| `index.html` | `id="bottomNav"`, class `hidden` | Hidden before login, shown via JS |
| `draft-room.html` | End of `<body>` | Active on Scoring (first item) |
| `league.html` | End of `<body>` | Active on League |
| `settings.html` | End of `<body>` | Active on Settings |
| `help.html` | End of `<body>` | Active on Help |
| `how-to-play.html` | Replaced inline nav | Active on Learn |
## CSS (identical on every page)
```css
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
background: rgba(10, 22, 40, 0.95);
border-top: 1px solid rgba(14, 165, 233, 0.15);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
z-index: 50;
padding-bottom: env(safe-area-inset-bottom, 0);
}
.bottom-nav a {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
padding: 8px 4px 6px;
font-size: 10px;
color: var(--text-muted);
text-decoration: none;
font-weight: 600;
transition: color 0.2s;
border-top: 2px solid transparent;
}
.bottom-nav a .nav-icon { font-size: 18px; line-height: 1; }
.bottom-nav a.active {
color: var(--amber);
border-top-color: var(--amber);
}
```
## Body padding
Every page with a fixed tab bar needs `padding-bottom: 80px` on `<body>` to prevent content from being hidden behind the nav. On `index.html`, this is applied unconditionally even though the nav starts hidden — the auth form sits centered in the viewport and the extra bottom space doesn't interfere.
## Special case: index.html
- The tab bar has `class="hidden"` (display:none) in the static HTML
- `onAuthSuccess()` calls `document.getElementById('bottomNav').classList.remove('hidden')`
- `logout()` calls `document.getElementById('bottomNav').classList.add('hidden')`
- This keeps the landing/auth screen clean while giving authenticated users the full tab bar
## Adding a new tab
If you add or rename a tab:
1. Update ALL 6 HTML files' tab bar HTML (use search_files to find all `<div class="bottom-nav">` blocks)
2. Update the CSS if the number of items changes significantly (flex:1 handles even distribution)
3. Update settings.html and help.html — they also have the tab bar
4. Choose which page gets `class="active"` on each file
## Removing Feed tab
The original settings.html had a "Feed" tab (📡) pointing to draft-room.html. This was replaced by the unified 7-item bar above. The Feed tab was never implemented as a separate page — it was a placeholder.