Commit Graph
73 Commits
Author SHA1 Message Date
tiennm99 f1b4c3256c feat(settings): clearer "both" glyph + sticky title/footer on mobile
Two small UX fixes in the settings modal:

1. Mode picker "Cả hai" glyph was two stacked rectangles — read as
   "windows" rather than the two roles being combined. Replace with a
   mini player-grid + mini megaphone side-by-side so the glyph mirrors
   the two single-mode glyphs concretely.

2. On iPhone SE the modal body pushes the title and the action row off
   screen, so the close button needs scrolling-back to reach. Make the
   title block sticky at top and the action row sticky at bottom
   inside the existing scroll container; move the panel padding from
   the outer wrapper into per-section padding so the sticky bands can
   span edge-to-edge with matching backgrounds.
2026-04-28 11:00:23 +07:00
tiennm99 a6c10aa67f ci: add inline-script guard for built index.html
SvelteKit emits one inline bootstrap <script> in build/index.html and
the CSP in static/_headers is relaxed to `script-src 'unsafe-inline'`
to admit it. If a SvelteKit upgrade adds another inline block, the
relaxation no longer matches reality and the new block could ship
unhashed.

`npm run verify:build` reads build/index.html, counts inline scripts
(no `src=`), and fails when count > EXPECTED_INLINE (1). New GH
Actions workflow runs test + build + verify on push/PR to main.

Mutation-tested locally: setting EXPECTED_INLINE=0 fails as expected,
restored to 1 passes.
2026-04-28 10:55:43 +07:00
tiennm99 36142de6c5 chore(plans): add todo-backlog implementation plan
9-phase plan covering the highest-leverage items from `plans/todo.md`
plus tech-debt and UX polish. YAGNI cuts: GhostBoardPreview (rule of
three not met), upstream-blocked override removals, parking-lot
features. Phase 1 (auto-tick test) marked completed.
2026-04-28 10:03:54 +07:00
tiennm99 e745631ea7 refactor(player-board): extract auto-tick into pure helper + tests
Pull the bus-driven auto-tick effect body out of PlayerBoard.svelte
into `src/lib/auto-tick.js` so the dedup-by-`at` invariant — the one
that already caught a P0 — is unit-testable without mounting Svelte.
The effect is now a thin wrapper that calls `processAutoTick()` and
applies the returned `{crossed, lastHandledAt, changed}`.

8 vitest cases cover NEW draw, dedup on same `at`, re-cross after
manual untick, mode=master/player ignored (timestamp still advances),
off-board number, null lastDraw, and null grid.
2026-04-28 10:03:47 +07:00
tiennm99 aabd1373a2 chore(plans): archive completed plans + add next-session todo
- Move all 8 completed plans (260426-* and 260427-*) into
  plans/archive/ to keep the active plans/ dir uncluttered.
- Add plans/todo.md as a hand-off list for next session: highest
  leverage items first (auto-tick integration test, CI inline-script
  smoke check, PWA install verification), then UX polish queued from
  pass-2 reviews, tech-debt items, and a parking-lot of new features.
2026-04-27 21:13:33 +07:00
tiennm99 8f7546c20a fix: address pass-2 review findings (PWA + CSP + copy)
P0:
- CSP `script-src` was 'self' only, but SvelteKit's static export
  emits a small inline bootstrap script. Without 'unsafe-inline' the
  entire app silently fails under Cloudflare Pages CSP enforcement.
  Verified by inspecting the built index.html.
- manifest `background_color` was the dark base (#0a0f1f); for the
  ~50% of users on light mode that gave a dark splash flash on every
  install/launch. Switch to #f8fafc to match the default light theme.
- <title> bare "Lô tô" mismatched manifest name "Lô tô — Hội chợ TN1";
  align both to the same string so OS install prompt + browser tab
  match.

Medium:
- Audio runtime cache `cacheableResponse.statuses` was [0, 200].
  Audio is same-origin, so opaque (0) responses can never legitimately
  appear; tightening to [200] removes a CDN-poisoning replay window.
- Voice hint copy: "Đọc số đã xổ + báo Chờ/Kinh khi ở Cả hai" was
  shown in master-only mode too, where the hint is wrong (no player
  board → no Chờ/Kinh). Split copy per mode.

Cosmetic:
- Drop `includeAssets: ["icons/*.png", "audio/**/*.mp3"]` — both are
  already in static/, so the option was a no-op.
- Replace `defaultVoiceId` fallback `"hoai-my"` with a hard read; the
  manifest is committed and authoritative — duplicate fallbacks just
  invite drift if the manifest ever rotates.

Verified: npm test 115/115; npm run build clean (305 precache entries,
no glob warnings); npm audit 0 vulnerabilities.

Reports: plans/reports/{code-reviewer,ui-ux-designer,security}-260427-2047-pass2-full.md
2026-04-27 20:53:53 +07:00
tiennm99 87eed879ef chore(deps): override transitive vulns — serialize-javascript + cookie
`@vite-pwa/sveltekit` pulled in `serialize-javascript@6.0.2` via
workbox-build → @rollup/plugin-terser, which is flagged for High RCE
(GHSA-5c6j-r48x-rmvq) and Medium DoS (GHSA-qj8w-gfj5-8c6v). Both fix
to >=7.0.5.

Also opportunistically bump `cookie` past 0.7.0 (Low GHSA-pxg6-pf52-xh8x)
via @sveltejs/kit — was the last-remaining Dependabot Low.

Build-time tooling only — neither package ships to the browser. Pinned
via npm `overrides` rather than `audit fix --force` because the latter
would downgrade @vite-pwa/sveltekit to 0.0.1 (breaking).

npm audit: 0 vulnerabilities. Tests 115/115. Build clean.
2026-04-27 20:43:32 +07:00
tiennm99 f899b09631 feat: UI polish v2 + installable PWA with offline audio
Phase 1 — Vietnamese-safe font + master empty state:
- Add @fontsource/roboto-condensed (700 weight, all subsets including
  Vietnamese). font-display:swap. tan-tan-num now resolves the bundled
  face on Android instead of system Arial Narrow.
- New MasterEmptyState.svelte: ghost 11×9 grid + "Chế độ Quản trò"
  pill + readiness microcopy. Replaces the bare line of text in
  MasterPanel's no-game state.

Phase 2 — Mode picker icons, color picker layout, header polish:
- Inline SVG glyphs above each mode button (player card / megaphone /
  two stacked cards). Communicates role at a glance.
- Color picker wrapped in a single bordered card with "Tuỳ chỉnh"
  and "Mẫu sẵn" sub-headers.
- "Mặc định" → "Đặt lại" with a bordered chip style — clearer
  affordance than the previous near-invisible footer link.
- Header subline: drop tracking-[0.28em] all-caps SaaS look; replace
  with dash-flanked lantern band ("— 🏮 Hội chợ TN1 —") for
  fairground mood.

Phase 3 — Installable PWA + offline audio:
- @vite-pwa/sveltekit with autoUpdate. Precache app shell (~353 KB
  → 213 entries) PLUS the default voice's 92 clips so first-install
  is fully offline-capable. Alternate voices fall through to a
  CacheFirst runtime rule (cached on first play, 30-day TTL,
  maxEntries: 400 for future-proofing).
- New static/manifest.webmanifest (Lô tô — Hội chợ TN1, theme #1565c0,
  background #0a0f1f, standalone, vi).
- Icons: 192/512 standard + 512 maskable, generated from a single
  rose-amber-gradient SVG source.
- app.html: manifest link, dual theme-color meta (light + dark),
  apple-touch-icon, apple-mobile-web-app-capable for proper
  standalone launch on iOS Safari.
- _headers: add manifest-src + worker-src to CSP; no-cache on /sw.js
  and /manifest.webmanifest so deploys propagate.

Tests: 115/115 pass. Build clean (305 precache entries, 0 glob
warnings).

Reviewer concerns (addressed):
- maxEntries bumped 200 → 400 (was barely enough for 2 voices).
- Default voice precached so offline-first promise holds without
  requiring users to play every clip online first.
- "do NOT add skipWaiting" comment added next to autoUpdate.
2026-04-27 20:35:59 +07:00
miti99 f7cbb6e1c7 docs: add post-implementation todo list
Tracks build verification, missing tests, polish items, distribution
prep, and future-feature backlog. Replaces the implemented phase docs
which were deleted post-shipping.
2026-04-27 20:21:37 +07:00
miti99 90a9dd1d1a feat: initial Android port of tiennm99/loto
Native Android port of the SvelteKit Lô tô hội chợ Tân Tân web app.

Stack:
- Kotlin 2.1 + Jetpack Compose + Material 3
- Single Activity, single Gradle module (:app)
- Audio: AndroidX Media3 ExoPlayer with bundled MP3 voice clips
- Settings: DataStore Preferences with legacy masterMode migration
- minSdk 24, targetSdk 35, JDK 17

Game logic:
- 9x9 player card with exactly 5 numbers per row AND per column
- Soft constraint: no 3 consecutive filled columns (rejection-sampled)
- 11x9 ones-digit-aligned master tracking board
- Bingo / Cho ("waiting") state machine ported from PlayerBoard.svelte
- Forward-only auto-tick from MasterPanel via app-scoped CallBus

Audio:
- 184 MP3s pre-generated with Microsoft Edge TTS (Hoai My, Nam Minh)
- ExoPlayer playlist for cho + N gapless sequence
- Token-based cancellation matching the web voice.js semantics

CI:
- build-debug.yml: lint + test + assembleDebug on push/PR
- release.yml: signed AAB+APK on v* tag, env-driven keystore
2026-04-27 19:33:16 +07:00
tiennm99 36f2f41986 fix: address full-project review findings (P0/P1/P2 + security)
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.
2026-04-27 18:57:14 +07:00
tiennm99 218c31d3c0 feat(ui): readability polish and voice settings refinement
- Improve section accent/divider color theming in app.css
- Enhance dark mode glow and body→html background layering
- Increase footer text size for better mobile legibility (text-xs → text-sm)
- Gate voiceWaitingNumber announcements behind new setting flag
2026-04-27 10:51:12 +07:00
tiennm99 d08f25bd96 feat: three-mode display (player/master/both) with master auto-tick
- Add `mode` setting to replace legacy `masterMode` with migration path
- Implement 3-button display mode picker (player-only, master-only, both)
- Auto-increment master when card drawn via call-bus event system
- Add voice hint on settings button, gate auto-call on mode changes
- Broadcast draw events from MasterPanel, reset call-bus on new game
- Broadened announce condition to cover both modes
- New call-bus.svelte.js module for event coordination
- Update settings-store tests to cover mode migration
- Update codebase docs for mode setting and call-bus architecture
2026-04-27 10:51:08 +07:00
tiennm99 7d1bb5e68a fix(ui): four small bugs surfaced after the UI/UX merge
1. Settings gear was unclickable because H1's `drop-shadow` filter
   created a stacking context that painted the H1 over the absolutely-
   positioned trigger. Bump gear wrapper to z-10.
   (src/routes/+page.svelte)

2. Switch rows in settings had a dead zone — `<label>` wrapped a
   `<span role=switch>` (not a labelable element), so taps on the
   text fell through. Move role/click/keydown to the wrapping div
   so the whole row toggles. Pill stays decorative.
   (src/lib/SettingsButton.svelte)

3. Empty cells used `dark:[filter:brightness(...)]` which creates a
   per-cell stacking context — same hazard class as bug #1. Replace
   with a pointer-events-none overlay span (`hidden dark:block`).
   (src/lib/PlayerBoard.svelte)

4. Audio cache never evicted on voice swap, leaking ~92 Audio
   elements per change over a session. New `clearAudioCache()` in
   voice.js, called from `pickVoice` when the id actually changes.
   (src/lib/voice.js, src/lib/SettingsButton.svelte)

Tests 98/98 pass; build clean.
2026-04-27 09:51:49 +07:00
tiennm99 7a6a19f242 feat(ui): mobile legibility, brand marquee, master focal, dark polish
Six small UI/UX phases shipped together:

1. Mobile legibility — cell aspect 3:4 on mobile (was 1:1), text-lg
   number, active:scale-90 press, navigator.vibrate(10) on tap,
   200ms cross-draw clip-path animation on cell mark.
2. Brand — H1 italic rose-amber gradient with drop-shadow + "Hội chợ
   Tân Tân" subtitle. First-run state shows a faded preview card +
   warm welcome line instead of a gray placeholder.
3. Master focal point — "Số vừa xổ" hero scaled w-40/56 with
   text-7xl/8xl, role=status aria-live=assertive, scrollIntoView on
   each user-driven draw (gated by scrollOnNextDraw flag so reload
   doesn't yank the page). Master section uses transition:slide.
4. Settings polish — modal max-w-md on sm+; switchRow snippet
   replaces 4 boolean buttons with role=switch UI (focus ring,
   keyboard space/enter); empty cells get
   dark:[filter:brightness(0.85)_saturate(0.9)] so user's chosen
   purple isn't neon in dark mode.
5. Celebration tiering — 3rd+ bingo per card triggers a 12-emoji
   CSS confetti rain at z-[60] above the popup.
6. Docs sync — codebase-summary + pdr.

Reviewer fixes applied: confetti z-index above popup; scroll-on-load
guarded by interaction flag; switch focus ring.

Tests 98/98 pass; build clean.
2026-04-27 09:38:18 +07:00
tiennm99 bc0622a0dd chore(audio): generate Vietnamese clips for both voices
- Run scripts/generate-audio.py to produce 184 MP3 clips (92 each
  for vi-VN-HoaiMyNeural and vi-VN-NamMinhNeural), ~2.2 MB total.
- Cap edge-tts concurrency at 4 with 4-attempt exponential retry on
  NoAudioReceived — earlier all-at-once gather() hit the upstream
  rate limit and bailed mid-voice.
- .gitignore: add .venv/ + __pycache__/ for the local generator venv.
2026-04-27 09:11:13 +07:00
tiennm99 69edd30529 feat(voice): bundled Vietnamese voice calls (master + player)
Speak the called number on master draw, "Chờ N" when a row is one
away, and "Kinh" on bingo. No runtime TTS API — clips are
pre-generated by `scripts/generate-audio.py` (free edge-tts) and
shipped as static MP3s under `static/audio/{voiceId}/`.

- src/lib/vietnamese-number.js + test (40 cases): tonal exceptions
  mười lăm / hai mươi mốt / hai mươi lăm
- src/lib/voice.js: lazy <audio> cache, token-based cancellation,
  cho+number sequencer, on-unmount cleanup
- src/lib/audio-manifest.js: re-exports static/audio/manifest.json
- scripts/generate-audio.py: discovers every vi-* edge-tts voice,
  writes 92 clips per voice + manifest.json
- static/audio/manifest.json: placeholder until user runs the script
- src/lib/settings-store: +voiceEnabledMaster/voiceEnabledPlayer/voice
  with per-key validators
- src/lib/SettingsButton: new "Âm thanh" fieldset (toggles + voice
  picker rendered from manifest)
- MasterPanel.handleDrawNext: playNumber(next) + cancel on new game
- PlayerBoard $effect: playWaiting/playBingo beside toast/popup;
  cancel on regenerate / clear

To materialize the MP3s on first install:
    pip install edge-tts
    python3 scripts/generate-audio.py

Tests: 98 pass (40 number + 31 settings + 27 game-logic).
2026-04-27 09:06:48 +07:00
tiennm99 35578d00fe feat(card): avoid 3 consecutive filled columns in any row
Soft visual constraint: no row has cols n, n+1, n+2 all filled.
Implementation = constraint-aware per-row picker (uniformly samples
triple-free completions of the forced+candidate set) + whole-grid
rejection sampling (up to 200 attempts). Hard invariants (5 per row,
5 per col, ascending column values) are never sacrificed; if the soft
constraint can't be met, the generator returns the best attempt.

- src/lib/game-logic.js: hasThreeInARow, combinations,
  pickFilledColsOnce, pickFilledCols rejection wrapper
- src/lib/game-logic.test.js: 300-trial strict assertion
- docs/codebase-summary.md, project-overview-pdr.md: note the rule
2026-04-27 07:57:10 +07:00
tiennm99 4a84245b39 feat(player): add "Xoá đánh dấu" button to clear marks without regen
Lets the player wipe all crossed cells on the current card without
generating a new grid. Shown next to "Tạo bảng mới" only when a grid
exists. Confirms before clearing if any cell is marked.

- src/lib/PlayerBoard.svelte: handleClear + secondary outline button
- docs/codebase-summary.md, project-overview-pdr.md: reflect new action
2026-04-27 07:52:05 +07:00
dependabot[bot] 2221ab9af8 chore(deps-dev): bump postcss from 8.5.8 to 8.5.12 (#2)
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.8 to 8.5.12.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.8...8.5.12)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.11
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 07:48:10 +07:00
tiennm99 e2c116abe2 chore: sync docs with single-page app, fix jsconfig
- docs: drop stale /master route refs (system-architecture page flow,
  client-only file list, basePath profiles; pdr architecture section
  + acceptance items; codebase-summary mounted-on notes; code-standards
  link example; deployment-guide redirect explanation)
- roadmap: drop "Currently Implemented Features" section (lives in git
  log + plans + pdr/codebase-summary)
- jsconfig: extend .svelte-kit/tsconfig.json (clears SvelteKit warning,
  picks up generated paths/aliases)
2026-04-27 07:44:19 +07:00
tiennm99 31125443e9 fix(deploy): break the / -> / redirect loop on Cloudflare Pages
The previous rule "/* / 301" matched / itself even though docs say
static assets take precedence — empirically CF still bounced and
caused ERR_TOO_MANY_REDIRECTS on loto.miti99.com.

Replace with two safer rules:
  /master*  /             301   (true URL redirect for the known orphan)
  /*        /index.html   200   (SPA fallback — URL stays, homepage shows)

The 200 rewrite cannot create a redirect loop. Static assets at /,
/_app/*, /favicon.* are served before rules fire.
2026-04-27 01:35:51 +07:00
tiennm99 9933fb49e2 refactor: single-page app — restore master tracking grid, drop /master route
Previous commit misread "remove master board" — restore the 11x9
ones-digit-aligned tracking grid (with circular tokens + draw-order
overlay) inside MasterPanel; remove the host's own player card
("Bảng của quản trò") instead. The host can use the player board
above it; no need for a duplicate card.

Delete /master route entirely (single-page app now). Cloudflare
Pages redirect added via static/_redirects: any unknown path
301-redirects to /, so old /master bookmarks land on the homepage.
Static assets are served first so the rule only fires on misses.

Storage keys loto_master_card_grid / loto_master_card_crossed are
no longer written but old saved data stays in users' localStorage
(harmless leftover).
2026-04-27 01:31:26 +07:00
tiennm99 0bc27f2897 feat(ui): big bundle — settings (theme/master/auto), purple default, mobile fit, master extraction
Settings expansion: 4 new keys (theme auto/light/dark, masterMode,
autoCallEnabled, autoCallSpeed 1-10) with per-key validators that
preserve old saved data. Default empty-cell color flipped from Tân
Tân blue to Excel Standard Purple #7030A0; preset palette swapped
to Office's 10 standard colors (5x2 grid). 15 new tests, 53 total.

Theme system: Tailwind v4 @variant dark (.dark *); applyTheme()
toggles <html class="dark"> based on settings.theme; auto mode
mirrors prefers-color-scheme via matchMedia listener (cleanly torn
down when switching modes). Existing dark-mode CSS converted from
@media to :where(.dark) selectors.

Mobile fit: PlayerBoard cells aspect-square on mobile, sm:aspect-[3/5]
desktop. Number text scales text-base sm:text-2xl md:text-3xl. Page
padding tightened (px-2 py-4 sm:px-3 sm:py-12). Container bumped
max-w-lg to max-w-2xl.

Header / footer: removed instructions toggle and "Trang quản trò"
link from player page. New PageFooter.svelte (tagline + Made by
miti99 with [SVG heart] link); also duplicated in PlayerBoard's
closing section-label band per request. Heart is inline SVG (red),
not emoji.

Master mode: extracted everything from /master route into reusable
MasterPanel.svelte. /master route slimmed to header + MasterPanel
+ footer. / mounts MasterPanel conditionally when settings.masterMode.
Storage prefixes unchanged.

Master tracking grid removed: per request, the 11x9 ones-digit
master board is gone. Host still gets controls, "Số vừa xổ" hero,
draw history list, and their own player card. Player card is enough.

Auto-call: single $effect lifecycle keyed on (autoRunning,
settings.autoCallSpeed, settings.autoCallEnabled). Setup / clear
setInterval cleanly across speed changes, master-mode toggle off,
component unmount, and "user disabled auto" mid-run. Button toggles
"Xổ số" -> "Bắt đầu / Dừng". Speed slider in Settings (only visible
when master mode is on). aria-label / aria-valuetext on slider.

Code-review nice-to-fixes applied: folded the two MasterPanel $effects
into one, removed muddled onkeydown on the SettingsButton modal
backdrop, added slider a11y attrs.

Docs: PDR / codebase-summary / system-architecture / development-roadmap
/ code-standards all synced to the new state.
2026-04-27 01:26:21 +07:00
tiennm99 96a652f381 style(player): move "Made with ❤️ by miti99" into the footer section label
The attribution lives inside the closing section-label band now (with
hatch flanks on both sides) instead of as a separate page footer.
Link to miti99.com kept clickable, with hover styling to differentiate
it from surrounding label text. Drops the duplicated standalone
<footer> from / and /master.
2026-04-27 00:50:10 +07:00
tiennm99 d30abc0717 style(player): cross-hatch ✚✚✚ flanks around section label text
Each label now renders as: [hatch fill] [padding] [LABEL] [padding]
[hatch fill]. Flex layout with ::before/::after pseudo-elements
filling the remaining width. Matches the BAMBOORAFT physical card's
✚✚✚✚ TÂN TÂN ✚✚✚✚ band; 12px horizontal padding around the text
keeps the label readable.
2026-04-27 00:48:07 +07:00
tiennm99 8bb71f8587 style(player): drop top/bottom hatch frames, add footer section label
Card now framed only on left + right by the cross-hatch. Top edge is
the first "Tân Tân" section label and the bottom edge gets a closing
"Tân Tân tốt nhất" label, mirroring the physical sheet's printed
header / footer slogans.
2026-04-27 00:47:03 +07:00
tiennm99 e760ecd180 style(player): drop cross-hatch dividers between sections
Section labels alone separate the 3 mini-cards now. The cross-hatch
frame stays only around the whole card perimeter — cleaner, less busy.
2026-04-27 00:44:30 +07:00
tiennm99 f8dcd8fc29 style(player): bring back per-cell borders so the grid actually reads
Each cell now has a thin slate border on all 4 sides (slate-400/50 in
light, slate-600/40 in dark). Adjacent cells double up to a slightly
heavier 1-2px line — matches the visible grid lines on the physical
BAMBOORAFT card.
2026-04-27 00:43:06 +07:00
tiennm99 09bb48c275 style(player): wrap card in cross-hatch frame on all 4 sides
The BAMBOORAFT physical sheet has the ✚✚✚ blue border running
around the entire card edge, not just between mini-cards. Adds
matching top/bottom/left/right frames using the existing
.section-divider plus a new .section-divider-vertical. Drops the
thin slate outer border (replaced by the hatch frame) and the
per-cell border-r/border-b lines (the white-on-blue contrast already
delineates cells, matching the flat physical-paper look). Outer
wrapper rounded-md instead of rounded-2xl to feel more like
a printed card edge.
2026-04-27 00:41:18 +07:00
tiennm99 5ba5ceddee style(master): number text matches ring color
All three places where called numbers appear (hero, tracking grid
token, history list) — digit color now follows the ring: pink for
1–49, emerald for 50–90. Drops the rose-700 fallback that was
mismatched against both rings.
2026-04-27 00:39:02 +07:00
tiennm99 428161dc21 style(master): bump tracking grid + history tokens to font-black
Match the hero "Số vừa xổ" weight. font-bold reads thin against the
cream-fill / red-text token; font-black gives the same chunky look
across all three places where called numbers appear.
2026-04-27 00:37:52 +07:00
tiennm99 3211a737cc style(master): drop outer red ring on "Số vừa xổ" — already focal
The hero already stands alone with a thick colored border and a large
digit; the extra red ring-offset accent was overkill. Restored
dark:border-* on the colored border so it adapts cleanly in dark mode.
2026-04-27 00:36:29 +07:00
tiennm99 6df2e261b8 style(master): "Số vừa xổ" outer ring keeps single color (no dark variant)
Drop dark:border-pink-400 / dark:border-emerald-400 on the outer ring.
The hero is already a focal point — single-mode colored border reads
the same in light and dark and avoids a lighter wash in dark mode.
Restore dark:text-rose-800 on the digit (light cream fill stays in dark).
2026-04-27 00:34:28 +07:00
tiennm99 6fabcd9970 style(master): drop dark:text-rose on "Số vừa xổ" — already a focal point
The cream fill stays light in dark mode, so the dark variant added no
contrast benefit. One color for the hero digit, simpler.
2026-04-27 00:32:54 +07:00
tiennm99 d9e6140363 style(master): "Số vừa xổ" big-number uses circular token style
Replaces the rounded-square orange→red gradient with the same circular
token treatment as the tracking grid: cream-yellow fill, thick ring
colored by value (pink ≤49 / green ≥50), dark rose digit. Keeps a
red ring-offset accent so the "just drawn" hero still stands out from
the same-style grid behind it. Bumped to w-24/sm:w-28 with text-5xl/
sm:text-6xl since the digit is the focal point.
2026-04-27 00:30:20 +07:00
tiennm99 b5414bdb43 style(player): tân tân BAMBOORAFT card look — tall cells, condensed black numbers, blue paper
Match the BAMBOORAFT physical sheet:
- Cell aspect changed from square to 3:5 (taller than wide), matching
  the printed paper proportions
- Number cells rendered with .tan-tan-num: condensed system-font stack
  (Arial Narrow / Avenir Next Condensed / Roboto Condensed), font-weight
  900, negative letter-spacing for tight fall-back on platforms without
  a true condensed face. Sized text-2xl / sm:text-3xl to fill the
  taller cells. Black number on white cell.
- Default empty-cell color flipped from Minh Tân brown (#7a4a2b) to
  BAMBOORAFT blue (#1e88e5). Settings store + test updated. Users who
  picked a different color in settings keep their choice.
- Section dividers and labels recolored from orange to matching blue.
- Section labels relabelled to the BAMBOORAFT trio:
  "Tân Tân" / "An khang thịnh vượng" / "Tân Tân tốt nhất"
2026-04-27 00:29:15 +07:00
tiennm99 3391e04a64 style(master): circular tokens for "Thứ tự đã xổ" history list too
Same pink/green-by-value treatment as the tracking grid: rounded-full,
3px ring (pink ≤49, green ≥50), cream fill, dark rose number. Bumped
size to w-8/9 + text-sm/base so two digits fit cleanly inside the ring.
2026-04-27 00:22:39 +07:00
tiennm99 e04fd116e9 fix(master): bigger token text + gray ring for uncalled numbers
- Bump number text to text-lg / sm:text-xl so digits read clearly
  inside the circle
- Pink/green rings now indicate *called* state — uncalled numbers get
  a neutral slate-gray ring instead, so the board's call progression
  is the dominant signal at a glance
2026-04-27 00:22:01 +07:00
tiennm99 d54ace7f56 feat(master): circular token style with pink/green ring per value range
Replaces the orange/red filled-square cells on the master tracking
board with a circular token treatment inspired by the Hội chợ "Các
số đã ra" panel. Each numbered cell now renders a centered circle
with a colored ring — pink for 1–49, green for 50–90 — over a
cream-yellow fill when the number has been called, or dimmed/empty
when it has not. The most recent draw keeps its red accent via a
ring-offset highlight + slight scale, so the host can still find
"vừa xổ" at a glance. Draw-order superscript stays at the cell's
top-right corner, now in muted slate (visible against both the
cream-filled and dim-empty token states).
2026-04-27 00:17:21 +07:00
tiennm99 5665663533 test: vitest setup + 38 tests for game-logic and settings-store
Adds vitest + happy-dom as devDependencies and npm scripts test
(one-shot) and test:watch. No SvelteKit/vite reconfig needed —
vitest auto-picks up the existing vite plugin chain.

game-logic.test.js (26 tests): generateGrid shape invariants
(9x9, exact 5/row, exact 5/col, no duplicates) over 200 random
trials; per-column number ranges (col 0 = 1-9, col 8 = 80-90);
ascending-within-column rule; isRowComplete/getWaitingNumber edge
cases; full save/load roundtrip for the persistence layer
including corrupt-JSON and wrong-shape rejection.

settings-store.test.js (12 tests): defaults frozen, load with
valid/invalid/corrupt payloads, hex regex rejects shorthand,
save persists to localStorage and pushes CSS var, reset returns
to brown default. Uses happy-dom env for localStorage and
documentElement.

code-standards.md: documents the rune globals declaration and the
.svelte.js convention for rune-using non-component modules.
2026-04-27 00:13:58 +07:00
tiennm99 8b9468a701 feat(settings): empty-cell color picker with persisted store
Adds a gear button + modal on /, /master with a native color picker
and 8 preset swatches for the empty-cell background. The selected
color repaints both the player card AND the master tracking grid via
the --empty-cell-bg CSS variable. Default brown matches the physical
Minh Tân paper card.

settings-store.svelte.js: Svelte 5 rune-based reactive store, state
hydrated on layout mount, persisted to localStorage key loto_settings.
Validates hex (#rrggbb regex) before applying — keeps the CSS sink
safe and ignores corrupt or shorthand-3-digit payloads.

SettingsButton.svelte: gear icon, modal with picker + presets + reset
+ close, escape-to-close via window keydown listener (the trigger
button retains focus on open, so a dialog-scoped handler would miss).

eslint.config.mjs: declares Svelte 5 rune globals so .svelte.js stores
lint clean.

Docs synced: PDR, codebase-summary, system-architecture, roadmap.
2026-04-27 00:13:39 +07:00
tiennm99 37502a4e28 feat(ui): tân tân 3-section player card layout
Render the 9x9 player card as 3 stacked 3x9 mini-cards mirroring a
physical Minh Tân paper sheet, with traditional separator labels:
"Minh Tân", "Loại đặc biệt", "Tấn tài tấn lộc". Pure visual change —
underlying 9x9 data, generator, click handlers, win detection all
unchanged. Sectioned render uses absolute row index (startRow + r) so
crossed-state and rowCompleteness lookups stay correct.

Adds .section-divider (cross-hatch repeating gradient) and .section-label
styles to app.css. Defines --empty-cell-bg CSS variable with a brown
default matching the paper card; the runtime override lands with the
settings feature in a follow-up commit.
2026-04-27 00:13:27 +07:00
tiennm99 de6bdf7eec docs: lock scope to lô tô hội chợ tân tân variant
- pdr: explicit in-scope (9x9, 5/row + 5/col, ascending cols, single-row
  Kinh, "Chờ N", continue-after-win) vs out-of-scope (3x9 european
  bingo 90, two-line / full-house tiers, custom number ranges)
- update codebase-summary, system-architecture, development-roadmap to
  reflect 11x9 master board, draw-order overlay, exact 5/col picker
- add researcher report comparing tân tân vs european bingo 90
2026-04-26 23:40:07 +07:00
tiennm99 189ae3d4e5 feat(game): tân tân lô tô — align master, 5/col, ascending, draw-order
- master board: 11x9 last-digit aligned (col 0 = 1-9, col 8 = 80-90);
  fixes missing ones-digit-9 row and aligns 1/11/21/.../81 horizontally
- master board: each called cell shows 1-based draw order so host can
  glance across a winning row to verify "Kinh!"
- player card: constraint-aware picker guarantees exactly 5 numbers per
  row AND per column (was loose weighted random allowing 4-6 per col)
- player card: numbers within each column now placed ascending
  top-to-bottom per lô tô hội chợ tân tân convention
2026-04-26 23:39:54 +07:00
tiennm99 ebae5371b4 fix(deploy): add wrangler.toml so CF Pages reads output dir from repo
CF Pages reported: Error: Output directory 'public' not found. The
dashboard had defaulted to 'public' but SvelteKit adapter-static writes
to its own output dir.

wrangler.toml with pages_build_output_dir makes the project reproducible
from git — no manual dashboard tweak required for the output path.

The build command in the CF dashboard still controls how the build
runs; ensure it is set to `npm run b u i l d`.
2026-04-26 21:27:08 +07:00
tiennm99 222cc97fa8 fix(deps): pin lightningcss-linux-x64-gnu so CF Pages build resolves
Cloudflare Pages build failed with:
  Error: Cannot find module '../lightningcss.linux-x64-gnu.node'

Tailwind 4 → @tailwindcss/vite → lightningcss has per-platform native
binaries shipped as optional npm packages. The lockfile generated on
this dev box (linux-arm64) only carried the arm64 binaries, so CF on
linux-x64 was missing the matching platform-specific package.

Adding lightningcss-linux-x64-gnu to optionalDependencies pins it into
the lockfile. On non-x64-linux machines npm skips it silently due to
the platform mismatch; CF Pages on linux-x64 will install it.

Pinned to 1.32.0 to match the parent dep already resolved.
2026-04-26 21:21:37 +07:00
tiennm99 c743d1cbfe ci: GitHub Pages now serves a redirect to loto.miti99.com
Replace the full build+publish flow with a tiny inline shell step that
emits two static HTML pages (out/index.html and out/master/index.html).
Each one carries:

  <meta http-equiv="refresh" content="0; url=https://loto.miti99.com/">
  <script>location.replace("https://loto.miti99.com" + ...)</script>

The script preserves path / query / hash, so
  /loto/                  → loto.miti99.com/
  /loto/master            → loto.miti99.com/master
  /loto/?x=1              → loto.miti99.com/?x=1

Cloudflare Pages stays canonical; this change just stops GH Pages from
serving a stale duplicate of the app and points old links at the live
domain instead.

The build:gh npm script is kept as a manual escape hatch for the rare
case where someone wants to deploy a real GH Pages copy by hand.
2026-04-26 21:19:40 +07:00
tiennm99 76ea82a649 fix(dev): read .env.local in vite + svelte configs
Symptom: 'Blocked request. This host (codeserver.sg.miti99.com) is not
allowed.' — the configs were reading process.env.CODESERVER_HOST, which
is empty at config-eval time because Vite reads .env.local at runtime,
not into process.env.

Switch both vite.config.js and svelte.config.js to use loadEnv() from
vite, which reads .env.local explicitly. Also lift allowedHosts out of
the codeserver profile so plain `npm run dev` is also reachable from
the codeserver proxy when CODESERVER_HOST is set in .env.local.
2026-04-26 21:15:59 +07:00
tiennm99 6dd7ce86f6 ci: bring back GitHub Pages auto-deploy
.github/workflows/deploy-github-pages.yml runs `npm run build:gh` on push
to main and uploads build/ via actions/upload-pages-artifact@v3 +
actions/deploy-pages@v4.

The build:gh script sets BUILD_PROFILE=gh, which switches svelte.config.js
basePath to /loto so assets resolve under tiennm99.github.io/loto.

Cloudflare Pages keeps deploying in parallel via the CF dashboard
(npm run build, root basePath, loto.miti99.com). Two URLs, no conflict.
2026-04-26 21:13:07 +07:00