- TypeScript 98.2%
- CSS 1.3%
- Dockerfile 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
publish / image (push) Successful in 3m59s
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> |
||
| .claude | ||
| .forgejo/workflows | ||
| app | ||
| lib | ||
| public | ||
| .dockerignore | ||
| .gitignore | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| Dockerfile | ||
| next.config.ts | ||
| package-lock.json | ||
| package.json | ||
| postcss.config.mjs | ||
| proxy.ts | ||
| README.md | ||
| tsconfig.json | ||
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
roundsrow:winner_id NULL= 流局;discarder_idNULL = 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=0records attendance. Imported nights carry animportedflag: 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