Skip to content

URL: https://mkdocs.justinsforge.com/memory/general/reference_justinkrystal_games_hub/

justinkrystal.com Games Hub + Michael 23rd Birthday Page

Built 2026-07-02. A Vite + Phaser 3 + TypeScript arcade that builds to static files served by the existing nginx container on media-server (CT102). First game: "Michael The Angler" (a.k.a. Set the Hook), a reaction fishing game (bass +3 / birthday cake +5 / rabid squirrel -2). Plan: justinkrystal-games-hub-2026-07-02.

Layout

  • Game source (build only, not served): sites/justinkrystal.com/games-src/ (Vite project). vite.config.ts sets base: '/games/' and outputs to ../landing/games/. Entries: index.html (hub) + fishing.html (the game). Pixel art is generated procedurally in Phaser (src/fishing/pixel.ts), no external assets. Fishing scene src/fishing/FishingScene.ts; leaderboard client src/fishing/leaderboard.ts; name-entry + board overlay src/fishing/overlay.ts.
  • Built output (served): sites/justinkrystal.com/landing/games/ (committed; deploy rebuilds it).
  • Birthday page: sites/justinkrystal.com/landing/michaels23rd/index.html at justinkrystal.com/michaels23rd. Announcement banner (top, not sticky), floating hero (hero.png), Immich photo gallery (photos/01..06.jpg, POS map pins per-photo crop e.g. football photo 03.jpg to center top), and the game via an auto-sizing iframe (fitGameFrame() reads the same-origin game height so the leaderboard below the cabinet is never clipped; scrolling="no"; cabinet fills the 552px embed = 15% bigger than the 480px base). OG/iMessage share card share.jpg (1200x630) wired via og:image/twitter:card. Home-page tile in the Misc section of landing/index.html.
  • Source images: originals live at /mnt/workspace/MichaelsBirthday/ (Main Pic.png hero, immich-*/ gallery, share pic/ OG). Processed into the page folder with ImageMagick convert (gallery capped 1400px q82; hero 560px; share fit to 1200x630 on #0d0d0f).

Leaderboard backend (extends hub-api Flask)

sites/justinkrystal.com/hub-api/app.py (Flask, already the family-hub API). sqlite at /data/leaderboard.db in the hub-data volume. - POST /api/games/score : body {game, name, score, sig}. Anti-cheat: per-IP rate limit (RATE_LIMIT_PER_MINUTE=10), score sanity cap, name clamp, HMAC-sha256 sig with HUB_SECRET_KEY verified via hmac.compare_digest. KNOWN_GAMES = {"fishing"}. - GET /api/games/leaderboard?game=fishing&limit=10 : top N {name, score, ts} + total. Unknown game -> 400. - In prod nginx serves /api same-origin; in Vite dev, vite.config.ts proxies /api to http://127.0.0.1:8085.

Dev workflow (watch live)

cd sites/justinkrystal.com/games-src && npm install && npm run dev -> Vite on 0.0.0.0:5180 with HMR. Watch at http://192.168.86.50:5180/fishing.html (Console LAN). Birthday page preview: python3 -m http.server 8091 in landing/ -> http://192.168.86.50:8091/michaels23rd. npm run build = tsc --noEmit && vite build.

Deploy

sites/justinkrystal.com/deploy.sh (unchanged pipeline + a prepended games build). It runs npm ci && npm run build in games-src (skips with a loud warn if npm absent), then tars landing/ + hub-api/, scp to finn, pct push to CT102, docker compose up -d. tar extract uses --no-same-owner --no-same-permissions; a couple of harmless utime/mode warnings on the bind-mount top dir remain (extraction still succeeds, chain still returns 0). Verify on the origin, not the public URL: http://192.168.86.74:8085/michaels23rd/ (Cloudflare 403s scripted/bot requests to justinkrystal.com; real browsers are fine).

Adding another game

New <name>.html entry in games-src/, add it to rollupOptions.input, build a scene under src/<name>/, add a tile to games-src/index.html, register the game in KNOWN_GAMES if it uses the leaderboard.