Skip to content

Commit 27a5834

Browse files
committed
docs(website): add how-to guides
1 parent 8d86e57 commit 27a5834

4 files changed

Lines changed: 503 additions & 0 deletions

File tree

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
---
2+
title: Adding a Game
3+
description: Step-by-step recipe for adding a new game to GamePulse — seed entry, taste profile, reviews, and verification.
4+
sidebar_position: 2
5+
---
6+
7+
# Adding a Game
8+
9+
This is the canonical recipe for adding a game. It takes about 5 minutes.
10+
11+
## 1. Add the catalog row
12+
13+
Open `lib/db/seeds/games.ts` and append:
14+
15+
```ts
16+
// lib/db/seeds/games.ts
17+
export const games: SeedGame[] = [
18+
// …existing games…
19+
{
20+
slug: "ark-nova",
21+
title: "Ark Nova",
22+
year: 2021,
23+
description:
24+
"Plan and design a modern, scientifically managed zoo. Build enclosures, support conservation projects, and run a successful zoo.",
25+
categories: ["Strategy", "Animals", "Card-Driven"],
26+
mechanics: ["Action Selection", "Tableau Building", "Resource Management"],
27+
min_players: 1,
28+
max_players: 4,
29+
complexity: 3.7,
30+
play_time: 150,
31+
buzz: 88,
32+
rising: 72,
33+
taste_profile: {
34+
strategy: 92, thematic: 78, party: 10, family: 25, solo: 85, conflict: 20,
35+
},
36+
},
37+
];
38+
```
39+
40+
### Taste profile guidelines
41+
42+
Each dimension is **0–100**:
43+
44+
| Dimension | What "high" looks like |
45+
| --- | --- |
46+
| `strategy` | Many decisions, long-term planning, engine building |
47+
| `thematic` | Strong narrative, immersive world, evocative components |
48+
| `party` | Fast, social, group-dynamics-driven |
49+
| `family` | Approachable, kid-friendly, gateway weight |
50+
| `solo` | Robust single-player mode or designed for solo |
51+
| `conflict` | Direct player interaction, take-that, PvP |
52+
53+
Score relative to *the catalog*, not absolutely. A 90 strategy means "among the most strategic games we list."
54+
55+
## 2. Add at least one critic review
56+
57+
Critic reviews live in `lib/db/seeds/critic-reviews.ts`. Reference the game and critic by **slug**:
58+
59+
```ts
60+
// lib/db/seeds/critic-reviews.ts
61+
export const criticReviews: SeedCriticReview[] = [
62+
// …existing reviews…
63+
{
64+
game_slug: "ark-nova",
65+
critic_slug: "tabletop-tara",
66+
score: 92,
67+
verdict: "Buy",
68+
excerpt: "The card synergies are deep without ever feeling fiddly. Ark Nova rewards patient builders.",
69+
source: "YouTube",
70+
content_type: "video-review",
71+
published_at: "2022-08-12T10:00:00Z",
72+
},
73+
];
74+
```
75+
76+
The orchestrator resolves `game_slug` and `critic_slug` to numeric IDs at insert time.
77+
78+
## 3. (Optional) Seed prices and awards
79+
80+
```ts
81+
// lib/db/seeds/prices.ts
82+
{ game_slug: "ark-nova", retailer: "Miniature Market", price: 64.99, shipping: "Free over $99", label: "best" }
83+
84+
// lib/db/seeds/awards.ts
85+
{ game_slug: "ark-nova", award_name: "Spiel des Jahres", award_year: 2022, result: "Recommended" }
86+
```
87+
88+
## 4. (Optional) Seed community ratings
89+
90+
For more realistic personalized predictions, add a few community ratings:
91+
92+
```ts
93+
// lib/db/seeds/community-reviews.ts
94+
{ game_slug: "ark-nova", user_handle: "alex", rating: 9.0, review: "Best card-driven euro of the decade.", created_at: "2024-01-04T12:00:00Z" }
95+
```
96+
97+
## 5. Bump the seed version
98+
99+
```ts
100+
// lib/db/seed.ts
101+
const SEED_VERSION = 5; // was 4
102+
```
103+
104+
## 6. Rebuild and verify
105+
106+
```bash
107+
npm run clean
108+
npm run dev
109+
```
110+
111+
Then check:
112+
113+
- **Browse page** at `/browse` → your game appears, filterable.
114+
- **Game detail** at `/games/ark-nova` → scores render, consensus badge is correct.
115+
- **Dashboard** at `/me` → if matched critics reviewed it, a personalized prediction shows up.
116+
- **Search** in the header → the title autocompletes.
117+
118+
## 7. Run the CI gate
119+
120+
```bash
121+
npm run check
122+
```
123+
124+
This runs `type-check → lint → build`. If types or seeds are inconsistent the build will catch it.
125+
126+
## Common mistakes
127+
128+
| Mistake | Symptom | Fix |
129+
| --- | --- | --- |
130+
| Forgot to bump `SEED_VERSION` | New game doesn't appear locally | Bump it and `npm run clean && npm run dev` |
131+
| Taste profile dimensions don't sum to anything | None — they're independent axes | Treat each axis independently, don't normalize |
132+
| Used `critic_id: 12` instead of `critic_slug` | Build error from the seed type | Always reference by slug |
133+
| Score is 9.2 (a 1–10 rating in a critic review) | Validation throws | Critic scores are 0–100; community ratings are 1–10 |
134+
135+
Next: [Customizing Scoring](./customizing-scoring.md) if you want to tune the algorithms.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
---
2+
title: Customizing Scoring
3+
description: Practical recipes for adjusting consensus thresholds, weighting taste dimensions, and changing personalized prediction behaviour.
4+
sidebar_position: 3
5+
---
6+
7+
# Customizing Scoring
8+
9+
All scoring logic lives in `lib/scoring.ts`. These are the four most common tweaks.
10+
11+
## Tweak the consensus thresholds
12+
13+
```ts
14+
// lib/scoring.ts
15+
export function buildConsensus(criticsScore, communityScore, rising) {
16+
if (Math.abs(criticsScore - communityScore) >= 15) return "Divisive";
17+
if (criticsScore >= 86 && communityScore >= 80) return "Critically Acclaimed";
18+
if (communityScore >= 88) return "Community Favorite";
19+
if (communityScore >= 80 && criticsScore >= 74 && rising >= 60) return "Hidden Gem";
20+
return "On the Rise";
21+
}
22+
```
23+
24+
Want stricter "Critically Acclaimed"? Bump `86 → 90`. Want a tighter "Divisive" gap? Bump `15 → 20`. Re-run `npm run check` to verify no regressions.
25+
26+
:::tip
27+
Run the dev server and sweep the browse page after a change. The mix of badges should still look reasonable across the seeded catalog.
28+
:::
29+
30+
## Weight taste dimensions
31+
32+
By default, cosine similarity treats every dimension equally. If you want *strategy* to count twice as much as *family*:
33+
34+
```ts
35+
// lib/scoring.ts
36+
const WEIGHTS: Record<TasteDimension, number> = {
37+
strategy: 2,
38+
thematic: 1,
39+
party: 1,
40+
family: 0.5,
41+
solo: 1,
42+
conflict: 1,
43+
};
44+
45+
function dot(a: TasteProfile, b: TasteProfile) {
46+
return TASTE_DIMENSIONS.reduce(
47+
(sum, key) => sum + WEIGHTS[key] * a[key] * b[key],
48+
0,
49+
);
50+
}
51+
52+
function magnitude(profile: TasteProfile) {
53+
return Math.sqrt(
54+
TASTE_DIMENSIONS.reduce(
55+
(sum, key) => sum + WEIGHTS[key] * profile[key] ** 2,
56+
0,
57+
),
58+
);
59+
}
60+
```
61+
62+
The change preserves the `[0, 1]` output range as long as `WEIGHTS` are applied symmetrically to both `dot()` and `magnitude()`.
63+
64+
## Change the matched-critic count
65+
66+
The dashboard shows the **top N** matched critics. To change `N`, locate `getMatchedCritics()` in `lib/queries/user.ts` and adjust the `.slice()` at the end. We'd recommend keeping `N` between 3 and 10:
67+
68+
- `N < 3` produces unstable personalized predictions when one critic skips a game.
69+
- `N > 10` dilutes the personalization signal.
70+
71+
## Re-weight personalized predictions
72+
73+
`getPersonalizedScore()` weights each matched critic's score by their match. To bias toward higher-confidence critics:
74+
75+
```ts
76+
// lib/scoring.ts → getPersonalizedScore
77+
const weights = matchedCritics.map((c) => c.matchScore ** 2); // was: c.matchScore
78+
```
79+
80+
Squaring the match score makes a 90%-match critic count ~3× a 60%-match critic, instead of 1.5×. Useful if you find low-match critics dragging predictions toward the global average.
81+
82+
## Add a new consensus label
83+
84+
1. Extend `CONSENSUS_LABELS` in `lib/scoring.ts`:
85+
86+
```ts
87+
export const CONSENSUS_LABELS = [
88+
"Divisive",
89+
"Critically Acclaimed",
90+
"Community Favorite",
91+
"Hidden Gem",
92+
"Cult Classic", // new
93+
"On the Rise",
94+
] as const;
95+
```
96+
97+
2. Add a branch in `buildConsensus()` **before** the `return "On the Rise"` fallback:
98+
99+
```ts
100+
if (criticsScore <= 60 && communityScore >= 85) return "Cult Classic";
101+
```
102+
103+
3. Update `ConsensusBadge` in `components/gamepulse-ui.tsx` to add the new label's icon, color, and tooltip.
104+
105+
4. Run `npm run check`. TypeScript will surface any switch/case that hasn't handled the new value — fix those.
106+
107+
## Verify your changes
108+
109+
Two quick checks any scoring change should pass:
110+
111+
```bash
112+
npm run type-check # types still align
113+
npm run build # all pages render with the new logic
114+
```
115+
116+
Then walk through:
117+
118+
- `/browse` — badges visually plausible across the catalog
119+
- `/games/brass-birmingham` — a Critically Acclaimed staple
120+
- `/games/<a divisive game>` — Divisive triggers correctly
121+
- `/me` — personalized predictions update
122+
123+
Next: [Deploying](./deploying.md) when you're ready to ship.

website/docs/guides/deploying.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
---
2+
title: Deploying
3+
description: How to deploy GamePulse with Docker, on a single VM, or behind a managed Node host — and the SQLite gotchas that matter.
4+
sidebar_position: 4
5+
---
6+
7+
# Deploying
8+
9+
GamePulse is a **single Next.js server + a SQLite file**. That makes deployment simple, but there are two things to get right: **persistence** and **single-writer SQLite**.
10+
11+
## TL;DR
12+
13+
| Target | Difficulty | Use when |
14+
| --- | --- | --- |
15+
| Docker on any VM | ⭐ Easy | You want full control and persistence |
16+
| Fly.io with a volume | ⭐ Easy | You want managed infra + a persistent disk |
17+
| Railway / Render with a volume | ⭐ Easy | You want one-click deploys |
18+
| Vercel / serverless | ⛔ Don't | Serverless runtimes can't hold a persistent SQLite file |
19+
| Kubernetes | ⭐⭐ Medium | Pin to a single replica with a `PersistentVolume` |
20+
21+
The non-negotiable rule: **the process must write to a persistent disk, and only one process can write at a time.** SQLite is single-writer.
22+
23+
## Docker
24+
25+
The repo ships a production-ready `Dockerfile`:
26+
27+
```bash
28+
docker build -t gamepulse .
29+
docker run -p 3000:3000 -v gamepulse-data:/app/data gamepulse
30+
```
31+
32+
The `-v gamepulse-data:/app/data` mount keeps `gamepulse.db` across container restarts.
33+
34+
## Fly.io
35+
36+
1. Install the Fly CLI and `fly launch` from the repo root.
37+
2. When asked about a volume, **yes** — create one named `data` of at least 1 GB, mounted at `/app/data`.
38+
3. Set runtime env:
39+
40+
```bash
41+
fly secrets set NEXT_PUBLIC_BASE_URL=https://gamepulse.fly.dev
42+
```
43+
44+
4. Deploy:
45+
46+
```bash
47+
fly deploy
48+
```
49+
50+
Make sure your `fly.toml` declares `min_machines_running = 1` and a single region — multi-region with SQLite needs LiteFS, which is out of scope here.
51+
52+
## Behind a reverse proxy
53+
54+
GamePulse listens on port `3000` and has no internal HTTPS. Front it with **Caddy**, **Nginx**, **Traefik**, or your platform's edge.
55+
56+
### Caddy snippet
57+
58+
```
59+
gamepulse.example.com {
60+
reverse_proxy 127.0.0.1:3000
61+
}
62+
```
63+
64+
That's it. Caddy handles TLS automatically.
65+
66+
## Environment variables
67+
68+
The complete list lives in [Reference → Configuration](../reference/configuration.md). The two you'll most likely set:
69+
70+
| Variable | Example | Why |
71+
| --- | --- | --- |
72+
| `NEXT_PUBLIC_BASE_URL` | `https://gamepulse.example.com` | Used for sitemap, Open Graph, and canonical links |
73+
| `NODE_ENV` | `production` | Enables Next.js production optimizations and blocks accidental reseeds |
74+
75+
## Database persistence
76+
77+
The SQLite file lives at `data/gamepulse.db`. The WAL and SHM sidecars (`gamepulse.db-wal`, `gamepulse.db-shm`) live next to it.
78+
79+
**Back up all three together** while the server is stopped, or use `sqlite3 .backup` for a hot copy:
80+
81+
```bash
82+
sqlite3 data/gamepulse.db ".backup data/backups/gamepulse-$(date +%F).db"
83+
```
84+
85+
## Production reseed guard
86+
87+
In production, the seeder **refuses to wipe existing data**. To force a reseed (e.g. on a staging environment), set:
88+
89+
```bash
90+
GAMEPULSE_ENABLE_PRODUCTION_RESEED=1
91+
```
92+
93+
This is intentional — running `npm run start` on a production box should never destroy user data.
94+
95+
## Health checks
96+
97+
The app exposes `GET /api/health`:
98+
99+
```json
100+
{ "status": "ok", "version": "0.1.0" }
101+
```
102+
103+
Wire it into your platform's health check. The endpoint returns `200` if the database is reachable and the process is up.
104+
105+
## Scaling beyond one box
106+
107+
You can scale Next.js horizontally, but not SQLite. Two practical options:
108+
109+
1. **Vertical only** — give one box more CPU/RAM. For the seed-scale workload (60 games, 200 reviews, dozens of QPS), this carries you a long way.
110+
2. **Migrate to Postgres** — replace `lib/db/connection.ts` and the parameterized SQL in `lib/queries/*` with `pg`. The schema is portable; only the driver changes.
111+
112+
See [Configuration](../reference/configuration.md) for the full env-variable reference and [Troubleshooting](../troubleshooting.md) for common deployment errors.

0 commit comments

Comments
 (0)