Mahjong night tracker — public dashboard + tap-to-record admin (mj.weew.ee)
  • TypeScript 98.2%
  • CSS 1.3%
  • Dockerfile 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jeron Wong df00197d96
All checks were successful
publish / image (push) Successful in 3m59s
放槍 / 自摸 / 流局 per hand, their stats, and a What's-new changelog popup
Every hand is now a rounds row with two nullable columns: winner_id NULL = 流局;
discarder_id NULL = unknown (imported/pre-feature), = winner_id = 自摸, else the
player who 放槍. Existing DBs are migrated in place by a guarded, transactional
table rebuild (SQLite has no ALTER COLUMN) that preserves ids and the
AUTOINCREMENT sequence.

- Admin: two-step tap entry — pick the winner (local state, no request), then
  who 放槍 or 自摸; a 流局 button in the idle state. Undo/delete/attendance
  guards cover the new columns; removing a player who only 放槍'd is blocked.
- Stats: dealtIn/selfDraws/dealtInRate per player, a feed matrix (diagonal =
  自摸), 流局 count/rate/most-in-a-night, hands-played denominators. Outcome
  stats gate per hand on a known outcome, never on the imported flag.
- Dashboard: 放槍 column, 放槍 & 流局 section with the matrix, Biggest 放槍
  tile, 流局 series on the per-night chart.
- Changelog: native <dialog> fed by the repo's Forgejo Releases (anonymous;
  in-process 1h cache, fail-soft), auto-opens once per new release.
- Tests: migration against the old schema in :memory:, new stats cases, the
  markdown-subset renderer and fetch fail-soft.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 22:02:09 -07:00
.claude Phase 2: shared-password auth (HMAC cookie, proxy gate, login page) 2026-07-13 00:39:05 -07:00
.forgejo/workflows CI: push image with a write:package PAT, not the auto Actions token 2026-07-13 20:54:55 -07:00
app 放槍 / 自摸 / 流局 per hand, their stats, and a What's-new changelog popup 2026-09-20 22:02:09 -07:00
lib 放槍 / 自摸 / 流局 per hand, their stats, and a What's-new changelog popup 2026-09-20 22:02:09 -07:00
public Initial commit from Create Next App 2026-07-13 00:28:52 -07:00
.dockerignore Phase 6: Dockerfile, Forgejo publish workflow, README 2026-07-13 19:56:12 -07:00
.gitignore Phase 1: scaffold + logic core (db, csv, stats) with tests 2026-07-13 00:35:19 -07:00
AGENTS.md Initial commit from Create Next App 2026-07-13 00:28:52 -07:00
CLAUDE.md Initial commit from Create Next App 2026-07-13 00:28:52 -07:00
Dockerfile Phase 6: Dockerfile, Forgejo publish workflow, README 2026-07-13 19:56:12 -07:00
next.config.ts Phase 1: scaffold + logic core (db, csv, stats) with tests 2026-07-13 00:35:19 -07:00
package-lock.json Phase 5: public dashboard — leaderboard, race + per-night charts, records 2026-07-13 00:59:09 -07:00
package.json Phase 5: public dashboard — leaderboard, race + per-night charts, records 2026-07-13 00:59:09 -07:00
postcss.config.mjs Initial commit from Create Next App 2026-07-13 00:28:52 -07:00
proxy.ts Phase 2: shared-password auth (HMAC cookie, proxy gate, login page) 2026-07-13 00:39:05 -07:00
README.md 放槍 / 自摸 / 流局 per hand, their stats, and a What's-new changelog popup 2026-09-20 22:02:09 -07:00
tsconfig.json Phase 1: scaffold + logic core (db, csv, stats) with tests 2026-07-13 00:35:19 -07:00

mjdash 🀄

Mahjong night tracker for the four of us: a public dashboard (leaderboard, cumulative race, night-by-night charts, records) and a password-gated admin where the record keeper taps the winner round by round at the table.

Stack

Next.js (App Router) · SQLite via built-in node:sqlite (no external services, no ORM) · Recharts · Tailwind. One process, one file of state (data/mjdash.db).

Dev

npm install
npm run dev      # http://localhost:3000 — admin creds in .env.local
npm test         # stats + csv parser tests (node --test)

.env.local needs ADMIN_PASSWORD and SESSION_SECRET (dev-only values on the dev machine — never committed).

Recording

  • Live nights: admin → New night → for each hand, tap the winner, then tap who 放槍 (dealt in) or 自摸 (self-draw); tap 流局 for a drawn hand. Undo/delete/attendance edits included. Every hand is a rounds row: winner_id NULL = 流局; discarder_id NULL = outcome unknown, = winner_id = 自摸, else the player who 放槍.
  • History import: admin → Import. CSV with header date,player,wins, one row per player per night; wins=0 records attendance. Imported nights carry an imported flag: their round order is fabricated, so round-streak stats only count live-recorded nights — and their 放槍/流局 are unknown, so those stats only count hands recorded live.

Release

git tag v3 && git push origin v3

The publish workflow builds the image and pushes jgit.weew.ee/jeron1/mjdash:v3 (+:latest) to the Forgejo registry. Then create a Forgejo Release for the tag with notes (bullet list; **bold** and bare links render) — the dashboard's "What's new" popup is fed by GET /api/v1/repos/jeron1/mjdash/releases (override with CHANGELOG_URL), cached in-process for an hour and fail-soft if jgit is unreachable.

Deploy (homelab)

Runs on the Proxmox fleet as CT mjdash behind Traefik at https://mj.weew.ee — config lives in the homelab repo (ansible/files/mjdash/docker-compose.yml.j2, secrets from the ansible vault). The SQLite file lives on the LXC at /opt/mjdash-data/ (excluded from git/ansible; PBS backs it up). Reference compose:

services:
  mjdash:
    image: jgit.weew.ee/jeron1/mjdash:v2
    container_name: mjdash
    restart: unless-stopped
    ports: ["3000:3000"]
    environment:
      TZ: America/Los_Angeles
      ADMIN_PASSWORD: "…vault…"
      SESSION_SECRET: "…vault…"
    volumes:
      - /opt/mjdash-data:/data