Commit Graph
109 Commits
Author SHA1 Message Date
tiennm99 df1bcdced1 fix(ci): bump JDK to 21 (Capacitor 8 compiles with sourceCompatibility 21) 2026-05-10 02:11:32 +07:00
tiennm99 b2c839c902 fix(ci): bump Node to 22 (Capacitor 8 CLI requires >=22) 2026-05-10 02:05:06 +07:00
tiennm99 7840aa419d feat: migrate to Capacitor wrapper for fully-offline Android
Replace the native Kotlin/Compose port with a Capacitor 8 wrapper around
the upstream loto SvelteKit PWA. Loto is consumed as a git submodule and
its static build is bundled into the APK at sync time. The wrapper has
no INTERNET permission, so fully offline is enforced rather than
conventional.

- loto/ submodule pinned to dfb1c1e
- android/ scaffolded by cap add android; signing wired via env vars
- All 184 voice MP3s + service worker bundled in assets/public/
- CI checkouts submodule, runs npm build, then Gradle inside android/
- Native port preserved in git history at e7fb3d0 and 8b8d46e
2026-05-10 02:03:16 +07:00
tiennm99 0d3d822b36 feat(seo): expand meta tags + PWA manifest for GitHub Pages
- app.html: add format-detection=telephone=no so mobile browsers
  stop auto-linking 2-digit bingo numbers as tel: links.
- app.html: rewrite description to mention Bingo 90, Vietnamese
  voice, offline play; add application-name + apple-mobile-web-app-title.
- app.html: add Open Graph (og:type, locale, site_name, title,
  description, url, image) + Twitter summary card. URLs hardcoded
  to https://tiennm99.github.io/loto/ to match the GH Pages target.
- app.html: add explicit <link rel="icon"> alongside apple-touch-icon.
- manifest.webmanifest: add stable id "/loto/", expanded description,
  and categories ["games", "entertainment"].
2026-05-10 00:37:10 +07:00
tiennm99 4d89366deb chore(plans): sweep shipped plan folders and orphaned reports
Apply the policy stated in plans/todo.md ("all prior plan folders
have been deleted") that had drifted: remove three completed plan
folders (auto-call-countdown, both-mode-state-consistency,
switch-deploy-to-github-pages) and the reports tied to them.

Also remove pre-shipping audits that referenced the now-deleted
Cloudflare _headers / CSP setup, plus the just-actioned
cloudflare-legacy cleanup audit. Keep the evergreen Lô Tô rules
researcher report.

Refresh todo.md hand-off header to reflect the GitHub Pages target.
2026-05-10 00:36:54 +07:00
tiennm99 777b2dbaec refactor: drop dead code left over from Cloudflare era
- Remove vietnamese-number.js + test: build-time Python script
  (generate-audio.py) is the sole spec for pre-baked MP3s; runtime
  no longer reads the JS module.
- Remove resetMaster() export: only ever called from tests; inline
  the two-line body at test sites.
- Update Python docstring to drop the JS-mirror reference.
- Rewrite stale "CF Pages root" comment in svelte.config.js.
2026-05-10 00:36:39 +07:00
tiennm99 8fc3ec6ebd ci(deploy): switch from Cloudflare Pages to GitHub Pages
Migrate build and deployment pipeline from Cloudflare Pages to GitHub Pages.
Adds production-ready deploy workflow in deploy-github-pages.yml with proper
artifact handling. Removes Cloudflare-specific tooling: wrangler config, _headers,
_redirects, and CSP hash injection scripts (no longer needed with GitHub Pages
static hosting). Updates package.json build scripts and all project documentation
to reflect new deployment target and simplified architecture.
2026-05-09 23:25:06 +07:00
tiennm99 a06cbd2675 fix(settings): hide "Báo Chờ / Kinh" toggle in master mode
Master-only mode never mounts PlayerBoard, so no Chờ/Kinh detection
fires — the toggle does nothing. Hide both it and its child
"Đọc thêm số đang chờ" sub-toggle in master mode to avoid the dead
control. Visible in player and both modes as before.
2026-04-30 21:53:25 +07:00
tiennm99 74a235a401 revert(player): bring back the gradient cross-slash style
The SVG-line replacement from the gradient-cleanup refactor (commit
635a4c3) drew the slash on the main diagonal (top-left → bottom-right)
and stopped at the inset:6px box edge — visually disconnected from the
cell vertices. The original CSS gradient stripe goes corner-to-corner
of the inset box on the anti-diagonal (the natural slash direction
for Vietnamese lô tô), with a clip-path reveal animation that reads
as "drawing" the slash.

- Restore .cell-crossed / .cell-crossed-win pseudo classes
- Replace the inline <svg> markup with a single <span> + class
- Restore cross-draw keyframes (clip-path inset reveal)
- prefers-reduced-motion: keep slash drawn, drop the reveal animation
2026-04-30 21:48:50 +07:00
tiennm99 dbed8aa664 chore(plans): add waiting-cell research + brainstorm reports 2026-04-30 21:41:50 +07:00
tiennm99 02d492c29c feat(player): center "Chờ N" chip + pulse the awaited cell
The toast above the card was disconnected from where the user actually
looks (the 9×9 grid). Two-part redesign per /ck:research + /ck:brainstorm:

- Chip moved to the vertical center of the card with bg-amber-500/75 +
  backdrop-blur-sm + pointer-events-none on the wrapper. Taps still
  pass through to the cells underneath; auto-hide stays at 5s.
- Persistent 1.6s breathing pulse on the cell holding the awaited
  number — amber inset ring + soft outer glow, dimmed in
  prefers-reduced-motion to a static ring. Material 3-style cadence
  picked over scale-bounce because the grid is dense and bouncing
  cells would smudge neighbours.

Pulses scale to multiple simultaneous waiting rows (Set-keyed by
"row,col"). aria-label gains "đang chờ" so screen readers pick it up.
2026-04-30 21:41:39 +07:00
tiennm99 3d988e1fc4 refactor(active-tab): rename tab-lock to active-tab for clarity
The "lock" framing was misleading — there's no OS-level mutex; this is
a soft coordinator that decides which tab is the active one. Rename
makes the role obvious at the import site.

- file: tab-lock.svelte.js → active-tab.svelte.js (+ test)
- export: tabLock → activeTab
- field: .frozen → .inactive (positive form: this tab is inactive)
- fn: startTabLock → watchActiveTab
- fn: reclaimTab → claimActiveTab
- BroadcastChannel name: loto_tab_lock → loto_active_tab

No behavior change. Banner copy already updated separately.
2026-04-30 21:34:06 +07:00
tiennm99 64e496b777 fix(tab-lock): formalize banner copy
Drop casual loanword 'Tap', use neutral formal Vietnamese for the freeze banner shown to all users (not just internal team).
2026-04-30 21:25:30 +07:00
tiennm99 7a4a4e0450 chore(plans): add voice + multi-tab brainstorm report 2026-04-30 21:22:31 +07:00
tiennm99 2f4efd1cab feat(tab-lock): single-tab guard via BroadcastChannel
Two tabs of the app on the same origin both running auto-call would
double-draw, double-write `loto_master`, and overlap audio. New tab
now broadcasts a claim; old tab freezes itself with a fullscreen
overlay ("Loto đã mở ở tab khác. Tap để chuyển về tab này.") and the
user can tap to take it back — handover broadcasts a fresh claim,
freezing the other tab in turn.

Mounted in +layout.svelte's onMount so the cleanup closes the channel
on HMR / route changes. No-op in legacy iOS Safari (≤15.4) without
BroadcastChannel — silently falls through to the prior behavior.
2026-04-30 21:22:23 +07:00
tiennm99 9e9c68bff6 fix(voice): suppress "Chờ {số}" in both mode
Master is the audio owner in both mode and is already calling numbers
aloud, so the player-side "Chờ N" announcement that fires right after
a master call confused listeners — they couldn't tell which number was
the active draw vs the awaited row.

- voice.js playWaiting: gate `speakNumber` on `mode !== "both"`
- SettingsButton: hide the "Đọc thêm số đang chờ" toggle in both mode

The bare-word "Chờ" still plays so the host knows a row is one cell
away. Setting persists across mode toggles — the gate is at the call
site, not in storage.
2026-04-30 21:22:18 +07:00
tiennm99 dbf93e29dd chore(plans): add both-mode consistency plan + audit reports 2026-04-30 21:10:58 +07:00
tiennm99 64a2f7b515 refactor(state): replace call-bus with shared master-store for both-mode
Single-slot bus carried only the latest draw, so any state event off-bus
(player regen, master "Ván mới", reload, mode toggle, throttled tab)
silently lost history. Symptom the host hit: regenerating the player
board mid-game wiped all prior auto-crosses.

- master-store.svelte.js: lifted {called, remaining} out of MasterPanel
  into shared reactive $state, persisted to loto_master, hydrated once
  in +layout.svelte's onMount so panels mount with consistent state
- player-auto-cross.js: new applyMasterCalls helper using cursor-by-index
  (vs the retired Date.now-based at timestamp), so callers can pass
  lastHandledIndex: 0 to replay master's full history on demand
- PlayerBoard:
  - Reads masterState.called directly; cursor advances strictly
  - manualUnticks Set tracks user-initiated unticks of called numbers,
    suppressing re-cross on replay; persisted to loto_manualUnticks
  - "Tạo bảng mới" replays masterState.called onto fresh grid (in both)
  - "Xoá đánh dấu" clears + immediately replays in both mode
  - Master "Ván mới" detected by called length transitioning >0 → 0,
    force-clears player crossed + manualUnticks (locked product call)
- Killed call-bus.svelte.js + auto-tick.js and their tests; helper
  surface is fully covered by master-store.test.js (9) and
  player-auto-cross.test.js (10), plus 6 new manualUnticks cases in
  game-logic.test.js (134 tests passing, was 123 before this refactor)

Targets findings F1, F2, F4, F6, F7, F8, F10 from the 2026-04-30
both-mode consistency audit. F9 (voice ownership) and #20 (multi-tab)
remain out of scope — separate plans to follow.
2026-04-30 21:10:51 +07:00
tiennm99 2b6a8c91f1 fix(master): break effect_update_depth_exceeded loop in countdown
Two $effects were reading state they also wrote, turning each into its
own dependency:

- AutoCountdown reset effect: `now = tickStart` read tickStart after
  writing it. Use a local snapshot so neither rune is read post-write.
- MasterPanel auto-call effect: `tickCount++` read tickCount. Drop the
  bump here — AutoCountdown's reset effect already re-baselines on the
  rising edge of `running` and on `duration` change, so re-arms remain
  covered. handleDrawNext keeps its per-draw bump (not in effect scope).
2026-04-30 19:47:22 +07:00
tiennm99 1e89137fa6 chore(plans): add countdown indicator plan + review report 2026-04-30 19:32:09 +07:00
tiennm99 7eb96808b0 feat(master): add auto-call countdown indicator
Visible countdown ring + seconds number above the hero token while
auto-call runs. Host now sees exactly when the next number fires
instead of staring at a static caption.

- AutoCountdown.svelte: pure visual component, props-driven, rAF loop,
  SVG ring via stroke-dashoffset, prefers-reduced-motion fallback
- MasterPanel.svelte: tickCount $state bumped per draw + on every
  (re-)arm of the auto-call $effect (covers speed-slider mid-run)
2026-04-30 19:32:02 +07:00
tiennm99 7f5b4bac95 chore: add Apache-2.0 license 2026-04-29 21:33:26 +07:00
tiennm99 04e4cf7a41 chore: add Apache-2.0 license 2026-04-29 21:33:25 +07:00
tiennm99 e80087c339 chore(plans): record 260429-1511 upstream-sync plan + reports
Plan directory and recon/code-review reports for the 6-commit
upstream sync from tiennm99/loto @ 2fb35f2.
2026-04-29 16:26:23 +07:00
tiennm99 593c270211 refactor: KinhModal title color to solid amber (palette refactor)
- Remove gradient brush from "Kinh!" title, use solid BrandAmber600.
- Align with solid-color palette refactor (phase 03).
- Update button and text colors to new palette.

Upstream: a60ea08
2026-04-29 16:25:01 +07:00
tiennm99 f1bc58a6df chore: refactor master/player UI components for consistency
- Minor UI alignment and spacing adjustments across master panel and
  player board screens.
- Improve code consistency and readability in Compose components.
- Update project todo list with implementation notes.

Upstream: a60ea08 (sync completion)
2026-04-29 16:24:49 +07:00
tiennm99 60df1dd7d0 feat: per-mode voice-master hint copy in settings
- Add mode-specific voice hint messages (player vs master mode).
- Update VoiceToggles to show contextual hint based on current mode.
- Improve UX clarity in settings sheet.

Upstream: a60ea08
2026-04-29 16:24:38 +07:00
tiennm99 11825315e0 refactor: solid-color palette refactor (BrandSky, removal of gradients)
- Replace BrandPink/BrandIndigo with BrandSky variants (light/dark).
- Add BrandRose600, BrandAmber600, BrandEmerald600.
- Add BackgroundCream for light mode background.
- Switch light theme background from white to BackgroundCream.
- Keep wordmark gradient (only intentional gradient in brand).

Upstream: a60ea08
2026-04-29 16:24:27 +07:00
tiennm99 793788c78c feat: section "Chờ" ring on label with pulse animation
- Add sectionHasWaiting state to track which sections have rows one cell
  from bingo (in Chờ state).
- Render amber ring + 1.5s pulse on section labels when section has waiting.
- Skip animation if accessibility motion is disabled.
- Add BrandAmberLight color for the ring.

Upstream: a60ea08
2026-04-29 16:24:16 +07:00
tiennm99 ec0a8567dd feat: confetti celebration tier rule and emoji set
- Fire confetti on 2nd bingo OR 1st bingo when another row is in Chờ
  (waiting for one number). Replaces old ≥3 threshold that rarely fired.
- Add Vietnamese hội-chợ emoji: 🥢 🎋 🏮 (chopsticks, bamboo, lantern)
- Per-particle size jitter (1.5–2.4x) for visual interest

Upstream: a60ea08
2026-04-29 16:24:05 +07:00
tiennm99 435a57a02b refactor(ui): drop gradients for clean solid-color palette
Single-accent system: rose-600 (player + UI), amber-600 (host),
emerald-600 (draw), sky-600 (master low-half). Replaces every text
gradient, button gradient, radial bg-glow, hatch-stripe divider, and
the diagonal cell slash (now an SVG <line> with non-scaling stroke).
Updates design-guidelines.md to match.
2026-04-29 14:53:08 +07:00
tiennm99 5b647e76ac chore(plans): delete shipped plan folders, inline residual TODOs
All implemented plan folders removed (current backlog + 8 archived).
PWA verification checklist preserved inline in todo.md as the only
remaining post-deploy work.
2026-04-28 14:11:22 +07:00
tiennm99 0f4c616b41 chore(plans): refresh todo.md after 260428-0927 plan ships
8 of 9 phases shipped (only manual PWA verification remains). Rewrite
the hand-off list to reflect current state: residual UX polish,
upstream-blocked tech debt, parking-lot features, and a "Recently
shipped" tail listing the 8 phases delivered today.
2026-04-28 11:43:43 +07:00
tiennm99 918fb1cda9 chore(pwa): tighten audio cache eviction (30d → 7d, purgeOnQuotaError)
Workbox's CacheFirst rule for /audio/*.mp3 used a 30-day age-based
TTL. Approximate LRU by dropping to 7d — a voice clip stays cached as
long as it's played at least once a week, otherwise it falls out and
re-fetches on next play. Each clip is <200KB and same-origin so the
re-fetch cost is negligible. Also flip purgeOnQuotaError so the
runtime cache yields first under storage pressure.

Default voice precache (vite.config.js additionalManifestEntries)
unaffected — it's a separate workbox flow with its own revision.
2026-04-28 11:10:51 +07:00
tiennm99 3fd02c14a2 ci(csp): replace 'unsafe-inline' with sha256 hash at build time
Postbuild script computes SHA-256 of every inline <script> in
build/index.html and rewrites build/_headers — replacing the
script-src 'unsafe-inline' relaxation with the matching hashes. The
hash regenerates per build (SvelteKit bootstrap embeds a per-build
registration call) so the script must run on every build; chain it
into both `npm run build` and `build:gh`.

verify-build extended to assert build/_headers script-src no longer
contains 'unsafe-inline', so the inject step's output is enforced in
CI. style-src 'unsafe-inline' stays — Svelte's `style:` directives
emit inline attributes that hashes can't cover.
2026-04-28 11:09:32 +07:00
tiennm99 a81695d982 feat(player-board): persistent Chờ ring + livelier confetti
Three player-board polish tweaks:

1. Per-section Chờ indicator. Each section's label band glows amber
   while any of its 3 rows is one cell from bingo. Uses the same amber
   as the toast so colour stays consistent. Animation respects
   prefers-reduced-motion.

2. Confetti threshold drops from 3+ bingos to: 2nd bingo OR 1st bingo
   while another row is in Chờ. The old threshold rarely fired on a
   9-row card so most wins felt under-celebrated.

3. Confetti emoji set adds 🥢 🎋 🏮 (chopsticks, bamboo, lantern) for
   hội-chợ flavour, plus per-piece size jitter (1.5–2.4rem) via a
   --size CSS variable.
2026-04-28 11:05:43 +07:00
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