โ† All apps
๐Ÿ€„

games-mahjong

Georgianna's American Mahjong โ€” tutorial + practice + online multiplayer

README

No README.

STATUS

# games-mahjong โ€” Status

## Current state
- Live at **https://mahjong.kenlill.com** on sherpa-server / Coolify. Pushes to
  `main` auto-deploy via GitHub webhook (verified 2026-06-05; see sherpa-server
  project). Old Render service `georgianna-mahjong` is suspended.
- **KenBot Stage 1 (text chat) is integrated and verified** โ€” STOPPED for
  Ken's review before any Stage 2 (voice) work, per INTEGRATION.md.

## KenBot โ€” Stage 1 done 2026-06-11, awaiting Ken's review
Ken's decisions: React island just for KenBot (game stays vanilla JS); new
`ask_kenbot` Supabase Edge Function; help screen only. Model pick was
delegated โ†’ **Claude `claude-opus-4-8`** (the project already had
`ANTHROPIC_API_KEY` in Supabase Edge Function secrets for `ask_claude`, so
no new infrastructure).

What shipped:
- Deps: `react@19`, `react-dom@19`, `sherpa-kenbot` (github dep, builds on
  install). Island in `src/kenbot/island.js`, lazy-loaded by
  `src/screens/kenbot.js` โ€” KenBot+React is its own ~336 kB chunk that only
  downloads when help is first opened; main bundle unchanged.
- Game screen ๐Ÿ’ฌ button (relabeled "Ken") toggles KenBot bottom-right
  (`wander=false`); hidden on exit to menu = "help screen only". This
  **replaces the old AI Help panel** โ€” `src/screens/help.js` and the
  `#helpOverlay` markup/CSS are now dormant dead code (ask Ken before
  deleting).
- New Edge Function **`ask_kenbot`** (deployed, ACTIVE, verify_jwt=true):
  Anthropic proxy with conversation-history support; system prompt =
  ask_claude's mahjong/app knowledge + KenBot persona (speaks as Ken,
  1โ€“3 sentence answers, TTS-friendly). `src/net/kenbot.js` is the client
  bridge (anon session + functions.invoke).
- Verified: `scripts/smoke-kenbot.js` passes (question + history follow-up);
  browser flow verified end-to-end (summon โ†’ greeting โ†’ ask "what's a pung?"
  โ†’ answer in bubble โ†’ toggle preserves chat โ†’ hides on Menu). No console
  errors. `npm test` 10/10; `npm run build` clean.
- `.env.local` recreated (lost

โ€ฆ(truncated for upload size)

DECISIONS

# Architectural decisions

Append-only running log. Each entry: context, decision, rationale, follow-ups.

---

## 2026-04-23 ยท Phase 1 โ€” Extract engine, keep single-player working

### D-001 ยท Vanilla ES modules, not a framework rewrite
The monolith is ~1200 lines of plain JS/HTML/CSS. Introducing React or Vue
would be a larger rewrite than the multiplayer work itself, and would
separate us from the original monolith we must keep working. We split into
ES modules and keep `renderTile` / `renderGame` as plain functions that
take state in and write DOM out.

### D-002 ยท Pure engine, no DOM or globals
Every module under `src/engine/` is pure. Functions take state + rng +
timestamp as arguments and return new state (or mutate the state object
they were given โ€” we're not chasing immutability, just referential
transparency). This is the hard gate: the engine must run in Deno.

### D-003 ยท Seeded RNG (mulberry32)
Using `Math.random()` inside engine code makes Edge Function behavior
non-reproducible, breaks tests, and prevents client-side speculative
rendering. Mulberry32 is ~8 lines, no dependency, and plenty good enough
for shuffle and AI randomness. The game's `state.seed` is the seed of
record; per-turn rngs are derived from `(seed ^ turn_index ^ seat)`.

### D-004 ยท Preserve original player indexing
The original monolith stores players as `[human(South), East, North, West]`
with turn order `(i+1) % 4`, which yields East โ†’ North โ†’ West โ†’ South
instead of the traditional East โ†’ South โ†’ West โ†’ North. Changing this
now would be a behavioral regression in single-player. We keep the old
indexing. If it bothers us for online play, the Edge Function can
remap seats to their canonical turn order at that boundary; we won't do it
as part of the extraction.

### D-005 ยท Mahjong validation stays `checkSimpleWin`
The monolith shipped a placeholder `matchPattern` (always false) and used
`checkSimpleWin` as the actual win check. That's the current behavior and
we match it. Real NMJL 

โ€ฆ(truncated for upload size)

MEMORY

# games-mahjong โ€” Memory

## What this is
American Mahjong tutorial + practice-vs-AI + online multiplayer, built for
Ken's **wife** Georgianna (NOT his sister โ€” early docs had this wrong).
Vanilla JS + Vite (NO React in the game itself),
Supabase (`nidovyzcsgmcqovukzvy`) for multiplayer via Edge Functions (Deno).
Engine is pure/dom-free in `src/engine/` and is synced into Edge Functions by
`npm run sync-engine`. Tests: `npm test` (node:test, engine only).

## Hosting
- Live at **https://mahjong.kenlill.com** on Ken's self-hosted sherpa-server
  (Coolify; `D:\dev\sherpa-server` project). Pushes to `main` auto-deploy via
  GitHub webhook โ€” **a push IS a production deploy**.
- Edge Functions deploy separately: `npm run deploy-functions` (Supabase CLI)
  or the Supabase MCP `deploy_edge_function` tool (used 2026-06-11 โ€” works,
  pass `ask_kenbot/index.ts` + `_shared/cors.ts` with entrypoint
  `ask_kenbot/index.ts` to preserve the `../_shared/` import).
- Old Render service `georgianna-mahjong` is suspended; render.yaml remains
  in the repo but is inactive.

## Standing facts
- GitHub repo (private) is source of truth; local restored 2026-06-08 to
  `D:\Personal\games-mahjong`.
- `.env.local` is gitignored and was lost in the re-clone; recreated
  2026-06-11 with the **publishable** key (`sb_publishable_โ€ฆ`). Values come
  from the Supabase MCP (`get_project_url` / `get_publishable_keys`).
- The app has TWO Anthropic-backed Edge Functions: `ask_claude` (legacy
  single-shot help, still deployed, no longer wired to UI) and `ask_kenbot`
  (KenBot chat with history, `claude-opus-4-8`). `ANTHROPIC_API_KEY` is set
  in Supabase Edge Function secrets and shared by both.
- **KenBot fully integrated (chat + voice), Ken-approved 2026-06-11** โ€”
  React island pattern, help-screen-only, replaces the old AI Help panel.
  Voice = `kenbot_tts` Edge Function (ElevenLabs proxy, voice ID defaulted
  in code); `ELEVENLABS_API_KEY` secret is set (Ken approved setting it from
  sherpa-kenbo

โ€ฆ(truncated for upload size)

CLAUDE.md

# Georgianna's Mahjong โ€” Project Guide

American Mahjong tutorial, practice vs AI, and online multiplayer. Built for
Ken's wife Georgianna for her first game night.

## Live

- **App:** https://mahjong.kenlill.com (Coolify on sherpa-server โ€” see `D:\dev\sherpa-server`)
- **Repo:** https://github.com/klill6506/games-mahjong (private)
- **Supabase project:** `nidovyzcsgmcqovukzvy` โ€” https://supabase.com/dashboard/project/nidovyzcsgmcqovukzvy
- **Old hosting:** Render service `georgianna-mahjong` is suspended; `render.yaml` remains but is inactive

## Running locally

```bash
npm install
npm run dev        # vite dev server on http://localhost:5173
npm test           # engine unit tests (node:test, no framework)
npm run build      # production build โ†’ dist/
npm run preview    # serve dist/ locally
```

Single-player ("Practice vs AI") works offline with no Supabase credentials.
Multiplayer is wired in Phase 4+ and requires env vars below.

## Where things live

```
index.html                   # shell with every screen div
src/
  main.js                    # entry โ€” wires screens and events
  ui/
    styles.css               # all CSS (lifted from the original monolith)
    render.js                # renderTile + renderGame, called with explicit state
  engine/                    # pure, dom-free, importable from Deno
    constants.js             # SUITS, WINDS, DRAGONS, PATTERNS, seat layout
    rng.js                   # mulberry32 seeded RNG
    tiles.js                 # createDeck, shuffle, tileKey, sortHand
    deal.js                  # newGameState (post-deal, pre-Charleston)
    charleston.js            # applyCharlestonPass, maybeEndCharleston
    turn.js                  # applyDraw, applyDiscard, applyClaim, applyMahjong
    ai.js                    # aiPickDiscard, aiDecideClaim, aiCharlestonPick
    patterns.js              # findBestPattern (used only for hints)
    engine.test.js           # node --test
  screens/
    menu.js, setup.js        # (wiring 

โ€ฆ(truncated for upload size)

Diary mentions

2026-06-08
# 2026-06-08

## sherpa-hub

Shipped the UI v2 redesign โ€” the hub went from plain light-Tailwind to a dark,
polished dashboard matching the mockup I liked, without giving up the
"stays-current-by-itself" guarantee that's the whole point of the rebuild.

### What we did

Full brainstorm โ†’ spec โ†’ plan โ†’ subagent-driven build cycle:
- Brainstormed the look using the visual companion. Looked at three directions
  first (minimal / dense-dark / playful), then I dropped in a reference mockup
  (the sherpa-dashboard-style dark dashboard with sidebar + summary cards + icon
  tiles + two-button cards) and we built around that.
- Key call: **Path 3 (hybrid).** Adopt the mockup's look, but keep status,
  activity buckets, and all counts 100% auto-derived from GitHub + Render. The
  only hand-set data is set-once and non-rotting: per-app icon (glyph + color)
  in `icons.yaml`, and category tags from GitHub repo topics. This preserves the
  thing that matters most to me โ€” it never goes stale on its own.
- Wrote the spec (`docs/superpowers/specs/2026-05-20-ui-redesign-v2-design.md`,
  decisions UI-1..UI-15) and a 10-task TDD plan, then executed subagent-driven
  with spec + code-quality review on the load-bearing tasks.

### What shipped

- Dark theme, violet accent, left sidebar (bucket filters with live counts +
  Diary/Health links)
- 4 summary stat cards: Total / Live / Active / Needs Attention (the last links
  to /health and shows a green โœ“ when nothing's wrong)
- App cards: colored Lucide icon tile, name, description, category tag pills,
  status pill, and two buttons โ€” "Open App โ†—" (jumps to the app's URL) and
  "โŒ Status" (drills into the detail page)
- Client-side search + grid/list toggle (no page reload, no backend)
- `/apps` list view dropped โ€” home IS the apps view now (301 redirect)
- Detail, diary, and health pages all restyled dark

### How the icons work (for future me)

`web/app/icons.yaml` maps each slug to a Lucide icon name + a color. The SVGs are
self-hosted via the `lucide-static` npm package and inlined server-side โ€” no CDN,
no icon flash, glyphs color from the tile via currentColor. Lookup is
case-insensitive (matters for repos like `Pips`/`Tanks` that GitHub stores with
capitals). Add a line per new app; anything missing falls back to a slate box.
`reload_icons()` fires on every sync upload so icon edits show up without a restart.

### Decisions recorded

- D-012: UI v2 dark dashboard, Path 3 hybrid (auto-derived + set-once icons)
- D-013: icons.yaml is a deliberate, bounded exception to D-001 (hub-owns-nothing)
  โ€” an icon is a display preference with no source of truth elsewhere, and it
  doesn't rot, so it doesn't reintroduce the legacy failure mode
- D-014: self-host Lucide via lucide-static, inlined (no CDN, no runtime JS)

### Numbers

- 10 plan tasks, all subagent-driven with review gates
- 99 tests pass (49 web + 50 sync)
- Local full-page smoke render confirmed real Lucide SVGs inline on every page
  (17 on the dashboard) โ€” proved the icon pipeline end-to-end, which the unit
  tests alone couldn't
- Pushed to main; Render auto-deploy in flight

## ideas

- Once the deploy lands, fill in `icons.yaml` for any app still showing the slate
  box, and sprinkle GitHub topics on repos I want category tags for. Both
  set-once.
- Favorites was in the mockup but I cut it โ€” no data source and it'd need upkeep.
  If I ever want it, the cheap version is a `favorites: [slugs]` list in a hub
  config file + a star toggle that's really just a filter. Only if I'll actually
  use it.
- The "Needs Attention" card is a nice nudge โ€” when it's >0 it's red with a count,
  when clean it's a green check. Worth watching whether it actually drives me to
  fix orphans/missing-memory, or whether I just ignore it like the legacy hub.

## notes

- Reviewer caught a good one on the icon module: it suggested lowercasing the
  YAML keys to fix `Pips`/`Tanks`, but that would've BROKEN the match (GitHub
  slugs preserve case, so the cache has `Pips` with a capital). Right fix was
  case-insensitive lookup on both sides. Good reminder to verify review feedback
  against the actual data rather than implementing it blindly.
- Subagent-driven dev keeps paying off: the model-vs-data reasoning above, the
  CVE catch last session, the schema-datetime tightening โ€” all came from the
  review gates, not the first-pass implementation.
- Hub URL unchanged: https://sherpa-hub-6psj.onrender.com (dark UI live after the
  deploy finishes). Still need to eyeball it + run a fresh sync.

---

## sherpa-hub โ€” UI v2.1 polish (later same day)

Same-day follow-up after I looked at the live v2 dashboard. Three quick
refinements, brainstormed with the visual companion then built subagent-driven
(5 tasks, 103 tests green).

- **Buttons toned down.** The big filled-purple "Open App" bar was too much. Now
  both buttons are small outlined; "Open โ†—" keeps a faint violet tint as the
  primary, "Status" is a quiet outline.
- **Light mode.** Added a sidebar Light/Dark toggle (sun-moon). Default dark,
  remembered in the browser, applied before paint so there's no flash on reload.
  The light palette is a token-override block plus explicit light variants for the
  status pills and the open button (their dark translucent colors were invisible
  on white).
- **The Yeti.** Went down a fun rabbit hole on the app icon โ€” the plain Lucide
  "mountain" looked like a pencil. Generated a few yetis; landed on a flat-style
  "Coding Yeti" logo (shades + hoodie + laptop with `</>`). It's now sherpa-hub's
  icon on its card, the sidebar brand, and the favicon. Built a small extension:
  icons.yaml entries can now carry an `image` field that renders a standalone PNG
  instead of a recolored Lucide glyph โ€” bounded one-app exception, every other app
  keeps the clean Lucide tiles.

### Notes / lessons
- The icon detour was a good reminder that "give me an icon" and "here's a
  gorgeous AI illustration" are different things. Detailed full-color mascots are
  hero images, not 24px tiles. The flat two-tone logo (which even shipped with its
  own small-size preview) was the right kind of asset.
- Verified review feedback against real data again: a reviewer wanted me to
  lowercase the icons.yaml keys to "fix" Pips/Tanks, but GitHub slugs preserve
  case so the cache really has `Pips` โ€” case-insensitive lookup was the correct
  fix, not lowercasing the keys.
- Source yeti images saved in D:\dev\sherpa-server\Images\ (yeti-laptop.png is the
  keeper). Crop recipe is in the v2.1 plan.

### Still to do
- Eyeball the live site after deploy: yeti everywhere, smaller buttons, theme
  toggle. Run a fresh sync so the sherpa-hub *card* picks up the yeti.
- If the yeti's dark hoodie is low-contrast on a dark card, there's a ready
  one-line `.icon-tile:has(.icon-img)` plate fix noted in the plan.

---

## sherpa-hub โ€” end of day: two bugs on the live v2.1 site (paused, resume tomorrow)

Looked at the deployed v2.1 site. Light/dark mode works. Two issues; both
root-caused (systematic-debugging), no fixes applied yet โ€” stopped for the day.

**Bug 1 โ€” yeti not showing.** Server side is verified correct: the image returns
200 image/png live, and the render test proves the sidebar HTML includes the
`<img>`. So it's a browser cache (a stale failed-load from the brief window before
the asset deployed). To do: hard-refresh (Ctrl+Shift+R) and confirm. Likely no code
change.

**Bug 2 โ€” "Open" links go to GitHub, not the live app.** Real root cause: only 13
of 43 apps match a Render service. The matcher requires the Render service name to
EXACTLY equal the repo slug, and many don't โ€” georgianna-mahjong=games-mahjong,
ken-half=half-training-app, four CFB* services=cfb-picks, plus ~27 repos not on
Render (some at kenlill.com). All of those fall back to GitHub. The hub can't guess
these โ€” only I know the mappings/custom domains. Plan: a manual per-app `url:`
override in hub config (like icons.yaml), precedence url > render > github. Need to
decide tomorrow: (A) manual override only, or (B) also auto-pull Render custom
domains. Leaning A. Then I supply the real URLs and re-sync.

Also: the deployed cache is stale (last sync Jun 5) โ€” a fresh sync is needed after
the fix regardless.


---

## games-mahjong + sherpa-hub โ€” evening wrap

**Hub link fix shipped.** Built the per-app `url:` override in icons.yaml
(precedence url > Render > GitHub), read at render time so no re-sync needed.
First mapping: games-mahjong โ†’ https://mahjong.kenlill.com (verified live there โ€”
"Georgianna's Mahjong Tutorial"; the old Render service is suspended). 55 web
tests green, pushed. Remaining GitHub-linking apps just need their real URLs
added one line each.

**kenlill.com apex finding:** the root domain is serving a *different, empty*
sherpa-hub instance โ€” no data, and NO auth (open to the world, though only the
empty state shows). The real hub (sherpa-hub-6psj on Render) has the data and
Basic auth. Recommended fix: move the kenlill.com domain to the real hub service.
Decide tomorrow.

**KenBot โ†’ mahjong: investigated, correctly STOPPED at pre-flight.** Ken dropped
`D:\Personal\sherpa-kenbot\INTEGRATION.md` (animated talking help character for
the mahjong help screen). Ran the doc's pre-flight gates; both fail:
1. KenBot is React-19-only; the mahjong app is vanilla JS + Vite (no React).
   Recommended path: a tiny React island just for the bot (package ships built
   ESM, so no JSX tooling needed in the host).
2. The doc requires an existing AI help backend; mahjong has none (Supabase Edge
   Functions for game moves only). Natural fit: a new `ask_kenbot` Edge Function
   calling an LLM with keys in Supabase secrets; the ElevenLabs voice proxy can
   be an Edge Function too (doc's Django proxy doesn't apply).
Two decisions for Ken tomorrow: React-island vs vanilla-build, and which
LLM/knowledge should power the bot's answers.

Also: re-cloned games-mahjong to D:\Personal\games-mahjong (local copy had been
lost in the D: cleanup โ€” it was a repo-no-local orphan on the hub Health page),
and created its missing STATUS.md + MEMORY.md with all of the above. Tomorrow's
agenda: hub URL list + kenlill.com domain move + KenBot decisions.