world-2026-hub/.agents/project-memory.md

211 lines
19 KiB
Markdown
Raw 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.

# Project Memory — World Cup 2026 Hub
Persistent memory for this project. Read this before any significant change.
---
## Context
Static web app showing the FIFA World Cup 2026 (Mexico/USA/Canada, 48 teams) — schedule, group standings, interactive knockout bracket with user simulation, stadiums. Hosted on GitHub Pages, all content from `data/*.json`. Started 2026-06-11 from two spec documents; built step-by-step with user approval between steps.
### What this project is
- A **personal/portfolio** piece — visual polish (glassmorphism, animations) is a primary goal, not a nice-to-have.
- A **static SPA**: one `index.html`, ES-module vanilla JS, JSON as the only "database".
- Maintained by editing JSON only — code should never need touching to update scores/teams.
### What this project is **not**
- No backend, no database, no build step, no bundler, no CDN dependencies, no frameworks.
- No automated tests, no linter (explicit spec constraint).
- Not real-data-complete: ships with mock data (fictional teams) to be replaced later.
---
## Priority objectives
1. **Spec compliance** — both spec files define scope; `complement-spec-worldcup2026-en.md` wins on conflict.
2. **Visual quality** — FIFA/UCL/Apple-inspired, glassmorphism, smooth animations; portfolio-grade.
3. **Interactive bracket** — hover path highlight, zoom, drag, simulation mode; the centerpiece feature.
4. **Easy maintenance** — real data drop-in via JSON; `bracket-config.json` is the only file edited after group stage.
5. **Performance/accessibility** — Lighthouse > 90, first render < 2s, JS < 300KB, ARIA + keyboard nav.
---
## Technical decisions and rationale
### Stack
- **Vanilla HTML/CSS/JS ES2022+, ES Modules** spec mandate; GitHub Pages serves static files only. Frameworks/bundlers explicitly forbidden.
- **EN/PT-BR UI toggle via `i18n.js`** (user decision 2026-06-11) not in spec; spec UI examples are EN, user wants both. Tiny dict + `t(key)`, persisted in `wc2026_prefs.lang`. Alternative (EN only) rejected by user.
- **`storage.js` pulled forward to step 2** (spec places it in phase 12) prefs (`lang`, `lastTab`) are needed from the base-layout step; building it late would mean refactoring.
### Data model
- **All match times in UTC** in `matches.json`; converted at render via `Intl.DateTimeFormat` (`formatMatchTime`). `.ics` export depends on this.
- **Knockout matches carry `bracketRef` instead of teams** teams resolved at runtime from standings + `bracket-config.json`; rounds after R32 have no config, generated by sequential pairing of winners (indices 0-1 0, 2-3 1, …).
- **Simulation never mutates JSON** overlay stored in `localStorage` `wc2026_simulation`, keyed by bracket match ids (`R32-1`, `QF-2`, `FINAL`, …).
### Mock data design (2026-06-11, step 1)
- **Real country names, fictional results** generated deterministically (seed 2026), script deleted after run; data is now static JSON.
- **State crafted to test both bracket modes:** matchdays 1-2 all `finished`; matchday 3 `finished` for groups AF, `scheduled` for GL (match 61, Group G, is `live`). So R32 slots fed by AF resolve to real teams; GL and all `third` slots show placeholders.
- **Knockout results:** `R32-2` (match 74) finished 1-1 + penalties 4-3; `R32-4` (match 76) finished 2-0. Everything else `scheduled` with `null` scores `results.json` has an entry for **all 104 matches**.
- **Image paths:** `flag`/`image` JSON values are relative to `assets/images/` (e.g. `flags/mex.svg`, `stadiums/azteca.svg`).
- **Opener** (match 1) is MEX at Estadio Azteca 2026-06-11; **final** (match 104) at MetLife 2026-07-19.
### Base layout decisions (2026-06-11, step 2)
- **Hero priority: live > next scheduled** — a live match replaces the countdown with the score + pulse badge. Mock data keeps match 61 permanently `live`, so the countdown only shows if that status is changed (verified working by temporarily setting it to `scheduled`).
- **i18n mechanics:** static HTML uses `data-i18n` / `data-i18n-aria` attributes re-applied by `applyI18n()`; dynamic renders call `t()` and listen for the `langchange` event on `document`. Phases translate via `translatePhase()` (PT: R32 = "16 avos de final").
- **Tab routing:** hash (`#matches`) + `wc2026_prefs.lastTab`, `history.replaceState` to avoid history spam; precedence on load: hash → lastTab → home.
- **Default language:** `navigator.language` startsWith `pt` → PT, else EN; only persisted when the user clicks the toggle.
- **Preview server:** `.claude/launch.json` at `R:\lucas-kalil\Projects\` defines `worldcup2026` (python http.server, port 8126) for the Claude Preview panel.
### Schedule + performance decisions (2026-06-11, step 3)
- **`schedule.js``app.js` circular import is intentional** — `app.js` calls `initSchedule()`, `schedule.js` imports `getData`/`formatMatchTime`/`flagSrc` back. Safe in native ESM because all calls happen after both modules evaluate; keep this pattern for `groups.js`/`bracket.js`/`modal.js`.
- **Filter UX:** toolbar is rebuilt only on init/langchange/clear (state restored programmatically); list re-renders on every filter change. Filter state is in-memory only (not persisted) by design.
- **Knockout cards show "TBD"** until step 7 swaps `teamColumnHTML`'s lookup to `resolveBracketTeams()`.
- **Perf: no `backdrop-filter` on repeated cards** — `.match-card` overrides `.glass` blur (huge paint cost × 104 cards, invisible over the smooth gradient). Same rule applies to any future card grid (stadiums, bracket).
- **Perf: fixed gradient lives on `body::before` (position: fixed)**, not `background-attachment: fixed` (the latter repaints the whole background on scroll).
### Standings decisions (2026-06-11, step 4)
- **Only `status: "finished"` matches count toward standings** — live scores are ignored until full-time (keeps standings stable and bracket resolution deterministic).
- **Tiebreak order:** points → goal difference → goals for → team id alphabetical (stable fallback). Verified against an independent Python computation for groups A and G.
- **`computeStandings()` / `isGroupFinished()` exported from `groups.js`** — bracket.js (step 7) must import these instead of recomputing.
### Stadiums decisions (2026-06-11, step 5)
- **`stadiums.js` module added** — spec §4 has no module for the stadiums view; a dedicated view module keeps the per-view pattern (schedule/groups/bracket) instead of growing `app.js`.
- **Cross-link:** "View matches" on a stadium card calls `setStadiumFilter(name)` (exported by `schedule.js`, resets other filters) + `navigateTo('matches')` (exported by `app.js`).
### Modal decisions (2026-06-11, step 6)
- **Native `<dialog>` + `showModal()`** — focus trap, Esc-to-close and `::backdrop` come free; no custom trap code. Backdrop click detected via `event.target === dialog` (content is in a padded inner div).
- **Focus restore:** opener element saved in `openMatchModal()` and re-focused on `close`.
- **Card → modal wiring is event delegation on `#schedule-root`** (click + Enter/Space keydown), so it survives list re-renders. Cards got `tabindex="0" role="button" aria-label` already in this step.
- **`openMatchModal(matchId)` is the public API** — bracket (step 7) and any future view should call it rather than building their own.
### Bracket decisions (2026-06-11, step 7)
- **Tree model is language-neutral**: slots are `{ teamId }` or `{ ph: { kind, … } }`; placeholder text is produced at render time by `slotDisplay()` so language switches never invalidate the tree.
- **Tree is cached** in `bracket.js`; `invalidateBracket()` exists for the simulation overlay (step 9). Results are static per page load, so nothing else invalidates it.
- **`resolveBracketTeams(matchOrRef)`** accepts a match object (group or knockout) or a bracketRef string and always returns `{ home, away }` as `{ team: Team|null, label: string }` — schedule cards, modal, and search/team filters all consume this, so knockout matches become searchable/filterable once resolved.
- **Connector geometry**: all bracket columns share equal height with `flex: 1` slots, so pair children sit at 25%/75% and the next round's node at 50% — pure-CSS connectors (`::before`/`::after` stubs + pair vertical) meet exactly. Column gap 44px = 22px out-stub + 22px in-stub. Breaking the equal-height invariant breaks the lines.
- **Final column** holds champion box (top), FINAL (middle, aligns with SF pair), third-place block (bottom); its champion/third slots suppress the incoming connector stub.
### Bracket interaction decisions (2026-06-11, step 8)
- **"Full path" on hover/focus** = hovered node + its entire feeder subtree + its winner's route to the FINAL; THIRD-PLACE lights both SFs. Non-path nodes dim via `.has-path` on the canvas. Computed from ref arithmetic (`floor(i/2)` up, `2i/2i+1` down), no tree lookup.
- **Zoom = CSS `transform: scale()` on the canvas + a `#bracket-zoom` box sized to `natural × scale`** (transform doesn't affect layout, so the box gives the scroll container the right scrollable area). Pointer-anchored: scroll adjusted so the point under the cursor stays put. Clamped 0.42.
- **Natural canvas size is measured lazily** (`ensureMeasured()`) because the bracket panel can be `hidden` at render time (offsetWidth 0).
- **Pan + pinch via Pointer Events** with `touch-action: none` on the wrap (we own all gestures there; page scroll over the bracket is intentionally captured, like a map widget).
- **Dragclick conflict:** >5px movement sets a flag; a capture-phase click listener on the wrap swallows the click that ends a drag. The flag resets on the next `pointerdown`, so synthetic `el.click()` without pointerdown can be falsely suppressed in tests — dispatch pointerdown/up first.
- **Zoom level survives langchange re-renders** (module-level `view.scale`) but intentionally not reloads (not in prefs).
### Simulation decisions (2026-06-11, step 9)
- **Separation of concerns in the tree:** `decide()` applies only real finished results; `applySimulation()` overlays user picks afterwards and never overrides a real result. Stale entries (winner no longer among the resolved teams) are silently ignored — same validation the prediction import (step 12) will use.
- **Sim UX:** "Simulation" toggle in the bracket toolbar; eligible nodes (both teams resolved, real result still `scheduled`) get dashed blue borders; clicking one opens a small native `<dialog>` picker. An unequal score auto-selects the winner; a draw requires an explicit pick (penalties implied). Empty score defaults to 1-0 for the picked winner.
- **Storage format** is exactly the complement-spec shape: `wc2026_simulation = { "R32-6": { winner: "FRA", score: "2-1" } }` keyed by bracketRef, score oriented home-away.
- **`simchange` custom event** fires after any pick/reset; `schedule.js` listens and re-renders so simulated teams appear on knockout cards too (intentional leak — resolved tree is the single source).
- **Simulated nodes** show a blue "SIM" corner chip and blue scores; the modal shows none of this (it reads real results only).
### Responsive/a11y decisions (2026-06-11, step 10)
- **Breakpoints:** ≤767 (tight spacing, bracket `--node-w: 168px`/gap 36px — connector stub offsets must stay at gap/2, overridden in the same media query), 7681439 (single-row header, centered menu), 1440+ (container widens to 1360px).
- **Tabs follow the WAI-ARIA pattern:** roving tabindex + ArrowLeft/Right/Home/End in `initTabs()`; focus follows activation.
- **Dialogs get `aria-label`** set at open time (match name + phase); schedule count is `aria-live="polite"`; countdown has `role="timer"` + label.
- **Entry animations:** every unhidden `.panel` fades in; card grids (`.match-grid/.groups-grid/.stadiums-grid > *`) slide up with a 45ms stagger on the first 6 children, 260ms for the rest. All killed by `prefers-reduced-motion`.
### Extra features decisions (2026-06-12, step 12 — done before step 11 at user request)
- **Favorites:** single global capture-phase click delegation in `app.js` handles every `.fav-btn` (schedule, groups, modal) and dispatches `favchange`; each view re-renders itself. Stars never trigger the card/modal click (guard via `closest('.fav-btn')`). Bracket shows highlight only (no stars — nodes too small). Favorite involvement = gold left border.
- **`getFavoriteMatches(matches, favorites)`** lives in `bracket.js` (needs `resolveBracketTeams`), imported by `schedule.js` for the "My matches" filter.
- **Time mode:** header `#time-toggle` flips `wc2026_prefs.timeMode` and dispatches `timemodechange`; `formatMatchTime()` already defaulted to the pref, so views just re-render.
- **Challenge:** sim entries for real-finished matches can't be created in the UI (locked) but old ones persist in storage — that's by design: predictions are made while matches are `scheduled`, then scored when results land. Card renders only when ≥1 finished knockout match exists.
- **Share/import:** `?prediction=` is stripped from the URL via `history.replaceState` whether applied or not (prevents re-prompt loops). Declining `confirm()` keeps local picks; unknown refs are rejected wholesale.
- **`.ics`:** RFC 5545 TEXT escaping (`\,` etc.) applied even though the spec template shows raw commas — RFC compliance wins; verified output imports with CRLF-only endings.
- **Custom events now in play:** `langchange`, `simchange`, `favchange`, `timemodechange` — all on `document`; views own their re-renders.
### Build complete (2026-06-12, step 11 — all 12 steps done)
- README is the user-facing manual (run, deploy, JSON maintenance, localStorage keys, acceptance checklist). Keep it in sync when data formats change.
- **Verified at completion:** spec §18 criteria all pass; JS = 74 KB total (budget 300 KB); no root-absolute paths (GitHub Pages safe). **Not yet verified:** Lighthouse > 90 (needs a deployed URL or local Lighthouse run); actual GitHub Pages deploy.
- No commits made yet — repo initialized but empty; commit when the user asks.
### Workflow
- **12-step build plan with approval gates** — user approves each step before the next starts; summary after each step. Plan: `C:\Users\Lucas\.claude\plans\read-r-lucas-kalil-projects-web-worldcup-goofy-meerkat.md`.
- **Git repo, commits only when the user asks.**
---
## Known gotchas
### 1. `fetch()` of JSON fails on `file://`
**Where:** any local testing of `index.html`
**Why:** browsers block `fetch` of local files (CORS/origin rules)
**Symptom if forgotten:** blank app, console CORS errors, wasted debugging
**Solution:** always serve via `python -m http.server` (or any static server) from the project root
### 2. `.ics` requires CRLF line endings
**Where:** `assets/js/calendar.js`
**Why:** RFC 5545 mandates `\r\n` between lines; some calendar apps reject `\n`
**Symptom if forgotten:** exported event silently fails to import in Outlook/Apple Calendar
**Solution:** join VCALENDAR lines with `\r\n` explicitly
### 3. Third-place slots are `null` until defined
**Where:** `data/bracket-config.json``thirdPlaceAssignment`
**Why:** the 8 best third-place teams are only known after the group stage
**Symptom if forgotten:** crash or "undefined" team names in R32 rendering
**Solution:** `resolveBracketTeams()` must return placeholder labels ("Best 3rd #1", "Group A Winner") whenever a slot is `null` or the group isn't finished
### 4. Claude Preview screenshots can hang (tooling, not app)
**Where:** Claude Preview panel during verification
**Why:** the preview window's screenshot pipeline occasionally gets stuck; `preview_eval` keeps working
**Symptom if forgotten:** wasted debugging hunting a nonexistent app freeze
**Solution:** `preview_stop` + `preview_start` recovers it; verify state via `preview_eval` first before suspecting the app
### 5. Stale JS modules in the dev browser
**Where:** any JS edit while previewing via `python -m http.server`
**Why:** the server sends no cache headers, so browsers heuristically cache ES modules; a normal reload can keep serving old code
**Symptom if forgotten:** "module does not provide an export" errors or old behavior despite correct code on disk
**Solution:** `Promise.all(files.map(f => fetch(f, { cache: 'reload' })))` then `location.reload()`, or DevTools hard reload
### 6. `setPointerCapture` on pointerdown kills element clicks
**Where:** `assets/js/bracket.js` drag/pan handling on `#bracket-wrap`
**Why:** capturing a pointer retargets the eventual `click` event to the capture element, so delegation via `event.target.closest('.bracket-match')` never matches — modal and simulation clicks silently die
**Symptom if forgotten:** bracket nodes unclickable with real input while synthetic `el.click()` tests still pass
**Solution:** capture only after the drag threshold (>5px) is exceeded, inside `pointermove`, wrapped in try/catch. **Always verify click flows with `preview_click` (trusted input), not `element.click()`.**
### 7. GitHub Pages serves under a subpath
**Where:** all asset/data URLs in `index.html` and JS `fetch` calls
**Why:** project pages live at `https://<user>.github.io/<repo>/`, so root-absolute paths (`/data/...`) break
**Symptom if forgotten:** works locally, 404s on GitHub Pages
**Solution:** use relative paths (`data/matches.json`, `assets/...`) everywhere
---
## Patterns for future changes
### How to update real-world data (scores, schedule)
1. Edit `data/results.json` (scores/status) or `data/matches.json` (schedule).
2. Once group stage ends: fill `data/bracket-config.json``thirdPlaceAssignment` (slot → group letter). Nothing else changes.
### Real-data migration (2026-06-12)
- `how-update.md` (project root) is the full runbook for replacing mock `data/*.json` with real World Cup 2026 data: file-by-file schemas, order of operations (stadiums → teams → groups → bracket-config.round32 → matches → results → thirdPlaceAssignment), and a cross-file integrity checklist (group membership, id ranges, bracketRef uniqueness, stadium name/city matches).
- Flags one open decision: `stadiums.json` has 30 entries (original bid shortlist) vs. the 16 venues actually used by the real tournament — confirm with user whether to trim before/while editing `matches.json`.
### How to add a UI label
1. Add the key to both `en` and `pt` dicts in `assets/js/i18n.js`.
2. Use `t("key")` at the render site — never hardcode the string.
### How to add a new localStorage preference
1. Extend the `wc2026_prefs` object shape (document the new field here).
2. Read/write only via `storage.js` `get`/`set`.
### How to add a step summary after finishing a build step
1. Mark the step `[x] ~~...~~` in `.agents/TODO.md`.
2. Append any new decisions/gotchas here (never rewrite existing entries).
3. Rewrite `project-map.md` if structure/functions changed.
4. Stop and wait for user approval before the next step.
---
## Success metrics
- Lighthouse > 90; first render < 2s; total JS < 300KB.
- Spec §18 acceptance criteria all checked (tracked in README checklist, step 11).
---
## Communication
- User communicates in English/Portuguese mix; docs in English per conventions.
- **Ask before each build step** never chain into the next step without explicit go-ahead.