P0: - PlayerBoard auto-tick: track lastDrawn.at non-reactively so the effect re-firing on crossed/grid changes (manual untick, clear, regen) no longer re-marks the latest drawn number. - Toast moved above the grid — was overlaying middle cells for 5s. - Bingo modal Escape now uses a window listener (matches settings modal pattern); the inline onkeydown rarely fired. - Dark winning-row contrast: bg-emerald-900/60 + text-emerald-200 to clear WCAG AA. New .cell-crossed-win class flips the slash to emerald so completed rows read as "win" not "marked off". - master-only mode: keep "Quản trò" h2 visible so the page has context when no player board sits above. P1: - Reset bus on PlayerBoard handleClear/handleGenerate. - Master mode picker now shows a per-mode hint line. - Hide "Quản trò đọc số" toggle entirely in mode=player (no effect there). Indent auto-call slider with the same nested-border treatment as voice waiting. - Hero number scales smaller on ≤375px (w-32 / border-6). - aria-live=polite on hero (was assertive — informational not urgent). - MasterPanel auto-stop early-return without redundant write. - Drop in-card "Made by miti99" — duplicates the page footer. - Replace biased Math.random sort shuffle with Fisher-Yates in randomNumbersInCol. - Reduced-motion gates: vibrate, smooth scroll, all keyframe animations short-circuit when prefers-reduced-motion: reduce. - History pill border-2 (was border-3, cramped 88s). Security: - static/_headers: CSP, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, X-Frame-Options for Cloudflare Pages. - safeParse / loadSettings / MasterPanel.loadState: payload-size cap before JSON.parse + reviver strips __proto__ and constructor keys as defense-in-depth. - voice.clipUrl: encodeURIComponent on voice id and clip name. Tests: +findUncrossedCell unit tests (covers the auto-tick path that exposed the P0), +voice.js cancellation/chaining tests (playWaiting honours voiceWaitingNumber). 115/115 pass. Skipped (need new assets / deeper scope): - Vietnamese-supporting condensed @font-face (needs font upload). - Full master empty-state hero illustration. - Confetti emoji variety, color picker redesign, mode picker icons, brand-mood overhaul of the header subline.
Lô tô
Bàn số của trò chơi "Lô tô" — SvelteKit app.
Single page (/). The host enables "Chế độ quản trò" in settings to
reveal the master panel inline below their player card; called numbers
get spoken aloud in Vietnamese using bundled MP3 clips (no runtime TTS).
See docs/ for architecture, code standards, and deployment.
Development
npm install
npm run dev
Inside code-server (reverse proxy)
cp .env.example .env.local
# edit .env.local: set CODESERVER_HOST and CODESERVER_PORT
npm run dev:codeserver
Open https://<CODESERVER_HOST>/absproxy/<CODESERVER_PORT>/.
Use /absproxy/{port}/, not /proxy/{port}/ — the latter strips the
path prefix and breaks the SvelteKit base path.
Build
npm run build # default — root basePath, for Cloudflare Pages
npm run build:gh # /loto basePath, for tiennm99.github.io/loto manual export
Static export to build/. Deployed to Cloudflare Pages from main
(set up via the CF dashboard — see docs/deployment-guide.md).
Regenerating audio
Vietnamese voice clips live in static/audio/{voiceId}/. Generated
once with edge-tts (free, no API
key) — runtime never calls TTS. To regenerate (e.g., to add a new voice
that Microsoft ships, or to change wording):
pip install edge-tts
python3 scripts/generate-audio.py
The script auto-discovers every vi-* voice and writes a manifest the
app reads on next reload.