Initial skills documentation — 25 categories, all SKILL.md + references + scripts
This commit is contained in:
+98
@@ -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.
|
||||
+70
@@ -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.
|
||||
+17
@@ -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
|
||||
+41
@@ -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.
|
||||
+54
@@ -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.
|
||||
+66
@@ -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.
|
||||
+172
@@ -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
|
||||
+21
@@ -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.
|
||||
+49
@@ -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
|
||||
+563
@@ -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!
|
||||
+37
@@ -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/
|
||||
+45
@@ -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.
|
||||
+90
@@ -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.
|
||||
Reference in New Issue
Block a user