Files
loto/web/docs/system-architecture.md

197 lines
7.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# System Architecture
## Page Flow
```
Entry (+layout.svelte)
├─ onMount: loadSettings() — restore all 9 settings keys from loto_settings,
│ apply CSS vars, toggle <html class="dark"> on theme/OS pref, setup auto-call effect
│
└─ / (single page)
├─ [if settings.mode !== "master"]
│ ├─ Load loto_grid, loto_crossed from localStorage
│ ├─ Display 9×9 PlayerBoard (empty cells use --empty-cell-bg from settings)
│ ├─ Listen to call-bus for auto-tick when master draws
│ ├─ Generate new grid on button click
│ ├─ Mark/unmark cells on click
│ └─ Show bingo popup + "Chờ X" toasts
├─ [if settings.mode !== "player"]
│ └─ Mount MasterPanel (controls + draw history, publishes to call-bus)
└─ PageFooter (tagline + miti99 link)
```
## State Model
### Player Card (`storagePrefix="loto"`)
```
grid: number[][] // 9×9 numbers (0 = empty)
crossed: boolean[][] // 9×9 marked state
```
Each row has exactly 5 non-zero numbers AND each column has exactly 5
(constraint-aware picker — no slack). Numbers within a column are sorted
ascending top-to-bottom (lô tô hội chợ Tân Tân convention).
### Settings (`loto_settings`)
```
theme: "auto" | "light" | "dark" // Display mode
mode: "player" | "master" | "both" // Panel visibility (replaces masterMode)
autoCallEnabled: boolean // Enable auto-call timer
autoCallSpeed: number (1–10) // Speed in seconds
emptyCellColor: "#rrggbb" // Hex color (default #7030A0 Excel Purple)
voiceEnabledMaster: boolean // Speak called numbers (master view)
voiceEnabledPlayer: boolean // Speak "Chờ"/"Kinh" (player events)
voiceWaitingNumber: boolean // Include number after "Chờ"
voice: string // Voice ID from audio manifest
```
### Host State (`storagePrefix="loto_master"`)
```
called: number[] // [5, 23, 67, ...] — drawn in order
remaining: number[] // [1, 2, 3, ...] minus called — shuffled initially
autoRunning: boolean // Auto-call timer active
```
### Host's Card (`storagePrefix="loto_master_card"`)
Same as player card; isolated by prefix to allow host to play.
## localStorage Keys
| Prefix | Grid Key | Crossed Key |
|--------|----------|-------------|
| `"loto"` | `loto_grid` | `loto_crossed` |
| `"loto_master_card"` | `loto_master_card_grid` | `loto_master_card_crossed` |
(Special) `loto_master` stores `{ called, remaining }` for the master state.
All keys are JSON stringified. Corruption is silent (returns null).
## basePath & Asset Resolution
### Production (GitHub Pages, canonical)
```
npm run build:gh
basePath="/loto"
Output: build/index.html with /loto prefix
Deploy: https://tiennm99.github.io/loto/
Workflow: .github/workflows/ci.yml (build "gh" -> deploy-pages)
```
### Development (Local)
```
npm run dev
basePath="" (empty)
Dev server: http://localhost:5173
```
### Development (Code-Server)
```
npm run dev:codeserver
.env.local: CODESERVER_HOST + CODESERVER_PORT
basePath="/absproxy/{PORT}"
Access: https://<proxy>/absproxy/{PORT}/
```
**Note**: Use `/absproxy/{port}` — `/proxy/{port}` strips the path prefix and breaks the SvelteKit base path.
## Client-Only Architecture
All pages are client-only (no SSR) because:
- `ssr: false` in `+layout.js` disables server-side rendering
- localStorage is unavailable on server
- State initialization (grid, crossed) must run in browser
Files that are client-only:
- `src/routes/+page.svelte` (single page)
- `src/lib/PlayerBoard.svelte` (player card component)
- `src/lib/MasterPanel.svelte` (host panel, mounted when `settings.masterMode`)
## Data Flow: Master Draw (mode: "both")
```
1. Master taps "Xổ số" button in MasterPanel
2. Draw logic removes number from remaining
3. broadcastDraw(num) publishes to call-bus
4. PlayerBoard's $effect listens to bus.lastDrawn
5. Auto-marks cell in player's grid (if it exists)
6. If row completes → bingo popup + auto "Kinh" voice
7. MasterPanel displays hero token, appends to history
```
## Data Flow: Mark a Cell (Player)
```
1. User clicks button in PlayerBoard
2. handleCellClick(row, col) fires
3. crossed = crossed.map(...) toggled state
4. $effect listens to crossed → saveCrossedState()
5. localStorage updated with new crossed state
6. $derived updates rowCompleteness matrix
7. If row complete → bingo popup; if waiting → "Chờ X" toast
```
## Data Flow: Initial Load
```
1. Component mounts
2. $effect runs initial loadGrid(storagePrefix)
3. If found, load crossed state (or initialize as all false)
4. Pre-populate celebratedRows + notifiedWaitingRows sets
5. Pass grid + crossed to UI render
```
## Animations
| Name | Duration | Use |
|------|----------|-----|
| fade-in | 0.2s | Modal background entry |
| pop-in | 0.4s | Bingo popup scale + scale-back |
| bounce-slow | 1.5s infinite | Emoji on bingo popup |
| spin-slow | 3s infinite | ✨ on bingo popup |
| spin-slow-reverse | 3s infinite reverse | 🎊 on bingo popup |
| toast | 5s forwards | "Chờ X" notification fade in/build |
| cell-crossed::after | instant | Red diagonal line in marked cells |
## Call Bus (Master ↔ Player Wiring)
In `mode: "both"`, MasterPanel and PlayerBoard communicate via `call-bus.svelte.js`:
```
call-bus.svelte.js:
├─ bus.lastDrawn = { num, at } // Published by master draw
├─ broadcastDraw(num) // Called by MasterPanel on each draw
└─ resetBus() // Called on new game
PlayerBoard:
└─ $effect(() => { bus.lastDrawn }) // Listens for draw broadcast
└─ Auto-marks cell if number exists on board
└─ Triggers bingo popup if row completes
```
Each draw creates a new object (even repeat numbers) to ensure reactive re-fire.
## Offline Capability & PWA
**Static Storage**: All game state in localStorage. No API calls. Fully functional offline.
**Service Worker**: vite-plugin-pwa generates `/sw.js` (wired into the SvelteKit build in `vite.config.js`) with:
- App shell precache (~353 KB): HTML, CSS, JS bundles for instant load
- Audio runtime caching: Voice clips cached on-demand (CacheFirst strategy)
- Navigation: NetworkFirst (try network, fallback to cache)
- Auto-update: Detects new versions, shows reload toast, skipWaiting on user action
**Manifest**: `/manifest.webmanifest` enables install prompt on Android/iOS:
- Display: `standalone` (fullscreen app-like mode, no browser chrome)
- Theme colors: Indigo primary, white background
- Icons: 192px + 512px PNG (auto-generated)
- Start URL: `/` with basePath awareness
**Installation Flow**:
1. App loads, registers SW (production only, not dev)
2. Browser detects manifest → shows install prompt (native, no custom banner)
3. User installs → app appears on home screen, launches in standalone mode
4. Updates: SW checks for new version; if found, toast appears; user can reload
Last reviewed: 2026-05-09
Last synced: 2026-05-09 (Cloudflare Pages removed; GitHub Pages is the sole production target)