28 Commits
Author SHA1 Message Date
tiennm99 1c1bab8733 build: move web/ from pnpm to npm
Replace pnpm-lock.yaml with package-lock.json and drop pnpm-workspace.yaml.
The three security overrides move to package.json#overrides, which npm reads
natively; allowBuilds for esbuild is unnecessary because npm runs dependency
build scripts by default.

android/build:web drove the web build through corepack pnpm, so it would have
broken once web/pnpm-lock.yaml was gone. It now uses npm --prefix.

Fresh resolution picks up patched versions the old lockfile had pinned below,
so npm audit reports no advisories where pnpm audit reported eight.
2026-08-17 11:54:24 +07:00
tiennm99 c6c839fd95 ci: consolidate web and android workflows into one pipeline
Both subprojects ship from the same commit, so ordinary CI is now a single
ci.yml; only the tag-driven release stands apart. The web app is built twice
per run — once per base path — and every consumer downloads the artifact
instead of rebuilding, replacing three redundant base-"" builds on main.

Nothing deploys unless the test job is green, and android-release runs the
suite before signing (ci.yml does not fire on tags, so it was the only gap).

Shared toolchain setup moves into composite actions, which puts the web build
and the APK on the same Node version for the first time. The Firebase PR path
was still on npm ci against a stale web/package-lock.json that could resolve a
different tree than pnpm-lock.yaml; drop the lockfile and the npm path with it.

Also: least-privilege permissions widened per job, persist-credentials off on
every checkout, concurrency groups that cancel superseded PRs but never a live
deploy, npm caching for android, and the Firebase action pinned by commit SHA
to match how the release actions were already pinned.
2026-08-09 15:21:23 +07:00
tiennm99 cf7690e46f docs: describe the web/ + android/ layout at the repository root
Adds a root README covering both subprojects and the CI matrix, folds the two
identical Apache-2.0 copies into a single root LICENSE, and updates the web
docs that named workflow files by their pre-move paths.
2026-08-03 18:06:49 +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 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 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 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 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 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 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 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 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
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 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 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
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 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 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 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 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 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 ee48e162d2 refactor: rewrite from Next.js + React to SvelteKit + Svelte 5
Full stack swap to enable future extension (more pages / load functions /
backend) while keeping JSDoc-only code style.

Stack:
- SvelteKit 2 + adapter-static
- Svelte 5 runes ($state, $derived, $effect, $props)
- Vite 7 + @sveltejs/vite-plugin-svelte 6
- Tailwind 4 (Vite plugin)
- ESLint 9 (flat) + eslint-plugin-svelte
- Pure JS + JSDoc, no TypeScript

Source moves:
- app/page.jsx              → src/routes/+page.svelte
- app/master/page.jsx       → src/routes/master/+page.svelte
- app/layout.jsx            → src/routes/+layout.svelte (+ +layout.js)
- components/player-board.jsx → src/lib/PlayerBoard.svelte
- lib/game-logic.js         → src/lib/game-logic.js (verbatim)
- next.config.mjs           → svelte.config.js + vite.config.js
- app/globals.css           → src/app.css
- (new)                     → src/app.html

Behavior preserved: PlayerBoard with bingo "Kinh!" popup + waiting "Chờ X"
toast, master 9x10 tracking board with shuffled draw, host's own player
card via storagePrefix="loto_master_card", localStorage prefix model
(loto_*, loto_master, loto_master_card_*), basePath dual mode (CF default
empty, BUILD_PROFILE=gh → /loto, codeserver dev → /absproxy/{port}).

A11y kept from prior hardening: role-correct buttons, aria-pressed on
cells, role=dialog modal with Escape, role=status toast.

Plans: ts-to-jsdoc plan marked completed; sveltekit-refactor plan tracks
the work above. Docs under ./docs/ rewritten by docs-manager subagent to
match the SvelteKit terminology.
2026-04-26 21:03:41 +07:00
tiennm99 95545c9308 refactor: convert from TypeScript to JavaScript with JSDoc
Author types in JSDoc comments. jsconfig.json keeps checkJs: true so
the editor's bundled TS server still validates them; CI can validate
with npx -p typescript tsc --noEmit on demand.

Renames (history preserved via git mv):
- next.config.ts        -> next.config.mjs
- app/layout.tsx        -> app/layout.jsx
- app/page.tsx          -> app/page.jsx
- app/master/page.tsx   -> app/master/page.jsx
- components/player-board.tsx -> components/player-board.jsx
- lib/game-logic.ts     -> lib/game-logic.js
- tsconfig.json         -> jsconfig.json (same @/* alias)

Drops typescript and @types/node, @types/react, @types/react-dom
from devDependencies. Removes vendored next-env.d.ts. eslint config
no longer pulls in eslint-config-next/typescript.

Behavior unchanged. Build, lint, and dev profiles verified.
2026-04-26 19:45:36 +07:00
tiennm99 254a6dc39a refactor: move shared modules out of app/
`app/` should hold route segments only. Game logic and the player-card
component are imported by both routes, so they belong outside:

  app/loto-game-logic.ts   -> lib/game-logic.ts
  app/loto-player-board.tsx -> components/player-board.tsx

Imports use the existing @/* alias. Drops the redundant `loto-` prefix.
Also fixes the package name from the scaffold default.
2026-04-26 19:34:37 +07:00
tiennm99 dd64aa6ed3 docs: initialize project documentation
Adds the standard ./docs/ structure (overview, codebase summary,
architecture, code standards, design guidelines, deployment guide,
roadmap) and the code-review report under ./plans/reports/.
README now points at the docs and covers the codeserver dev profile.
2026-04-26 19:27:18 +07:00