Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Private prediction game for a closed friend group. One manager (you) approves participants each round after they pay via Paybox; participants predict the score of 7 Israeli league matches per round and compete for a jackpot.
Live: https://motzkin-legends.vercel.app
Repo: https://github.com/Shlitay/motzkin-legends
Supabase project: teuqvtarqminvutoozsx
Manager account: [email protected] (must have role = 'manager' in the users table — see below if it ever needs re-setting)
This is not a mock-data demo anymore. Google login, onboarding, predictions, payment-approval requests, manager approve/reject, and both leaderboards all read and write the real Supabase database. As of this writing, 6 real participants have registered and set round 1 predictions.
The whole UI is Hebrew + RTL (converted 2026-08-21, straight replace — no
language toggle exists or is planned). <html lang="he" dir="rtl"> in
src/app/layout.tsx drives the mirroring; see "Design system" below for the
font swap this required.
npm install # if you haven't already
npm run dev
Open http://localhost:3000. Needs .env.local with
NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY (gitignored,
already set up locally — values are the same as the Vercel env vars below).
NEXT_PUBLIC_SUPABASE_URL=https://teuqvtarqminvutoozsx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=sb_publishable_-f7C2NCsa3oGQxFT3aHEtw_vDxNLmyf
Deploys automatically on every push to master via Vercel's GitHub
integration — no manual deploy step needed.
Auth
src/lib/supabase/{client,server,middleware}.ts, src/proxy.ts — Next.js 16 renamed middleware.ts → proxy.ts, same thing)src/app/auth/callback/route.ts exchanges the OAuth code, routes to /onboarding only if avatar/default-score are still unset on the profile rowproxy.ts refreshes the session every request and gate-keeps /manager/* behind role = 'manager' — real server-side protectionrls.sql — Row Level Security on every table. Shared game data (rounds/matches/teams/scoring) is world-readable, manager-only-writable. Each user reads/writes only their own profile & predictions. A trigger blocks self-promotion to role = 'manager' through the app — that can only be set directly in SQLPages (src/app/), all wired to real data:
/ and /manager/login — real Google sign-in
/onboarding — required first login: avatar + default score, writes to users
/home — removed 2026-09-20. /predictions is now the landing page after login (callback, onboarding, middleware and the login button all point there); /home remains only as a redirect to /predictions (next.config.ts) for old bookmarks. The avatar/name header, the two stat cards and the round-comments section went with it — participants check their own stats by tapping their row on /leaderboard.
Round approval status (RoundApprovalStatus.tsx) — self-contained (fetches its own open round + the current user's round_participation status), shown on /home, /predictions, /leaderboard, /rules (not /manager — the manager account doesn't participate). Status is color-coded: waiting = light orange (border-draw/bg-draw) with a clock icon, approved = light green (border-brand/bg-brand) with a checkmark icon, rejected = plain card with red text (not color-coded — wasn't asked for). Since 2026-09-08, /predictions' sendPrediction() is what actually puts a participant onto the manager's approval list — it upserts a round_participation row (ignoreDuplicates: true, so it only ever creates one, never overwrites an existing waiting/approved/rejected status) alongside the predictions upsert, since submitting predictions is itself proof of intent to play. This component's status === null state (no round_participation row yet) dropped its "תאשר אותי שלחתי כסף" button and the separate "לקבוצת פייבוקס" button entirely — the confirm-click it used to do is now redundant with submitting predictions, and the group-link button was redundant with the inline Paybox link already in the sentence above it. What's left is just that one line of text with the inline link.
Round countdown (RoundCountdown.tsx) — same 4 pages as the approval card, a live days/hours/minutes/seconds countdown to the open round's deadline_at, ticking every second. The countdown math itself is plain epoch-millisecond subtraction (always correct, timezone doesn't matter for a duration). The displayed kickoff clock time underneath, and /predictions' own deadline caption, both go through () — explicit conversion, not the viewer's own device timezone, since match kickoffs are inherently Israel-local (this replaced ' old , which implicitly used the browser's local timezone — technically correct today since real participants are in Israel, but wrong if anyone ever checked from abroad).
News ticker (NewsTicker.tsx) — a scrolling marquee shown on /predictions, /leaderboard, /rules, and /manager. Since 2026-10-28 every item is automatic (the manager-editable news_strip slots and /manager's "רצועת חדשות" modal were removed; the table is left in place, unread): (1) approved participants for the current round, (2) the last finished round's rank-1 winner + payout (same pot rule as /leaderboard's crown: round_participant_count × entry fee − one entry fee), (3) the current round's match count. Each item is its own dir="rtl" span so a Latin nickname or trailing digit can't reorder against the separators. RTL note: the ticker is forced dir="ltr" — under the page's global RTL, a flex row's natural resting position anchors to its right edge and only extends leftward, so the standard duplicate-content-and-translateX(-50%) marquee trick just shrinks the visible content into a growing blank gap instead of looping. This bit twice before being caught with an isolated Playwright test (multi-frame screenshots of the animation, not just a static one) — same fix pattern as the "1X2" logo and the countdown digits.
Header (TopBar.tsx) — now has a solid white background + 1px bottom border (previously transparent, floating over content), fixed + z-40 so it stays pinned above all normal page content while scrolling. Deliberately kept below z-50, which every modal (ProfileModal, ScoringRulesModal, NewsStripModal, ParticipantModal) uses — a header above the modal layer would poke through a modal's dimmed backdrop instead of being covered by it. Three things live in it besides the logo:
JackpotBadge.tsx) — gold gradient pill, white text, approved-participant count × a hardcoded ₪20 entry fee (ENTRY_FEE_ILS in the component — not manager-configurable, revisit if the buy-in ever changes). Hides itself if there's no open round. Has a looping shine/glint sweep (.shine-badge in globals.css, respects prefers-reduced-motion) — sized to fit the 4-item header at a 390px mobile width without pushing the manager/back link off-screen; verified with zoomed-in Playwright screenshots across the animation cycle since the effect is too subtle to confirm from a single static frame.InviteFriendsButton.tsx) — builds {origin}?utm_source=shareBtn&utm_medium={full_name} from the current user's Google full_name (not nickname — "the name from Google" specifically), e.g. "Itay Arad" → utm_medium=itay_arad. Uses the native share sheet (navigator.share) on mobile, falls back to clipboard copy on desktop. Changed 2026-08-23 from a bespoke ?ref={slug} param (which nothing ever read) to real utm_source/utm_medium — GA4 parses these natively into its own Acquisition reports (Session source/medium), so who-shared-what is visible in standard GA4 reports with no custom event code. Still not tied to a specific resulting app user/signup — GA4 sessions are anonymous (client_id), so this shows traffic per referrer, not a definitive "this exact account came from this exact link." A users.referred_by column + onboarding write would be needed for that; deliberately not built (asked, declined for now).resolvedAction prop).Every page's <main> reserves top padding to clear this fixed header — pt-28 (112px) was calibrated to the old, shorter transparent header and never revisited after the background/badge/button additions made it taller. Measured the real rendered header height with a throwaway route + Playwright bounding-box check (77px) rather than guessing from the Tailwind classes, then set pt-20 (80px, a ~3px buffer) everywhere pt-28 appeared — cut the dead space between the header and each page's first content block without risking clipping.
Favicon (src/app/icon.svg) — Next.js's static-file convention (auto-detected, no layout.tsx metadata needed): the "1X2" mark, white on a turf-green rounded square.
Analytics — Google Analytics 4 (Measurement ID G-GX7JT1JLL4), gated behind Analytics.tsx rather than rendering <GoogleAnalytics> (from @next/third-parties/google) directly. On mount it checks auth.getUser() + the users.role column, and skips loading GA entirely when the logged-in user is the manager — excludes the manager's own usage from the numbers without relying on GA4's IP-based "internal traffic" filters (unreliable across the manager's different devices/networks). Anonymous visitors (not logged in yet, e.g. the login page) are still tracked; only a known manager session is excluded. Verified empirically with a headless-browser network check that the gtag.js request actually fires for an anonymous visitor — the manager-exclusion path itself couldn't be verified the same way (no real login available in the dev environment) but reuses the identical role-lookup pattern TopBar.tsx already uses successfully in production.
Profile (ProfileModal.tsx) — real avatar picker, real default-score editor, and a nickname field (shown instead of the Google name everywhere a name displays; falls back to the Google name when unset).
Design system — "Home Pitch" kit (turf green + trophy gold):
src/app/globals.cssnext/font/google; Caveat (handwriting) for the logo signature only. Heebo replaced Work Sans on 2026-08-21 — Work Sans has no Hebrew glyphs at all, so it silently fell back to a mismatched system font once the UI went Hebrew.Logo.tsx (large, login hero), TopBar.tsx (compact, fixed top bar, links back to /predictions on every page). Brand name stays in Latin script even in the Hebrew UI (decided during the RTL conversion) — the OneXTwoIcon SVG in icons.tsx has an explicit direction: ltr style, since SVG <text> inherits the page's dir otherwise and clips/misplaces glyphs (hit this for real during the RTL conversion — the "1" disappeared until that fix landed).If ever setting this up again from scratch (or double-checking the current project has everything applied), run these in the SQL editor in order:
schema.sql — full data modelrls.sql — Row Level Security policiesseed-round1.sql — creates the season, round 1, and its 7 matchesfix-default-score-nullability.sql — fixes a bug where default scores defaulted to 1 instead of null, which broke the "needs onboarding" check (already applied on the live project)add-avatar-to-season-stats.sql — extends the season_stats view with avatar (already applied)add-nickname.sql — adds the nickname column + extends season_stats with display_name (already applied, but worth double-checking if anything nickname-related ever errors)add-round-comments.sql — creates the round_comments table + RLS for the round discussion feature, removed from the UI 2026-09-20 (table still exists, unused; already applied)add-lock-expired-rounds-function.sql — creates lock_expired_rounds(), called lazily by the app (on /predictions, /leaderboard, /manager load) to lock a round and fill per-match default scores once its deadline passes. It's a no-op for any round whose deadline hasn't passed yet, so applying it doesn't touch round 1's current data (already applied)add-news-strip.sql — creates the singleton news_strip table + RLS for the manager-editable news ticker (already applied)add-round-comments-delete-policy.sql — adds a delete RLS policy so a participant can delete their own round comment (to free up their one-per-round slot and post a new one). Not yet applied — run this before the "Delete" button on /home comments will work.add-hebrew-team-names.sql — translates teams.name and round 1's existing matches.home_team/away_team text to Hebrew, as part of the Hebrew/RTL conversion. Pure UPDATE of existing rows' text (both tables are free text, not foreign-keyed), doesn't touch predictions/round_participation at all. Applied — /predictions shows Hebrew club names and matching crests.add-submit-match-result-function.sql — creates submit_match_result(), called from /manager's "תוצאות מחזור" screen once per match saved: records the score and recomputes every affected participant's //// for the whole round in one go (already applied — , which changes this function's signature).App-side enforcement (/predictions): predictionsOpen = !predictions_open_at || now >= predictions_open_at. Feeds into the same readOnly gate as isOpenRound/submitted (score inputs, submit button), plus a new amber note card ("הגשת ניחושים למחזור X תיפתח ב-... אפשר כבר עכשיו לשלוח תשלום דרך Paybox") shown only while isOpenRound && !predictionsOpen. Like the existing deadline check, this is enforced client-side only, not at the DB/RLS level — matches rls.sql's own documented limitation ("prediction insert/update isn't deadline-checked here... enforced app-side for now"), not a new gap.
28. fix-season-stats-rounds-played.sql — real bug fix (found 2026-09-01, once round 3's new approvals started showing up before round 3 had begun): season_stats.rounds_played counted payment_status = 'approved' regardless of whether that round had actually started, so being approved for a still-open round already counted as a "played" round on /leaderboard's "הכי הרבה השתתפויות" table. create or replace view adds and r.status <> 'open' (via a new join to rounds) to both rounds_played and avg_points's filter clause — total_points/season_hits/season_towards didn't need the same fix, since an unstarted round's points/hits/towards are already 0/null. Superseded by migration 29 below.
29. fix-season-stats-rounds-played-v2.sql — migration 28 still wasn't right: it depended on current payment_status = 'approved', but /manager's "reset approved participants" action (added earlier the same session) sets payment_status back to waiting for an already-finished round to prep the next one's signups — which then silently erased rounds_played for anyone reset, even though they genuinely played and scored (their total_points/rank were never touched, only payment_status). Real fix: for a round with status <> 'open', participation is now determined by whether the user actually has rows in predictions for that round's matches (an exists subquery, immune to the reset action, since sendPrediction()/backfill_late_approval() are the only things that ever write to predictions). Not yet applied.
30. add-round-participant-count-function.sql — migration 29's fix wasn't quite enough either: 's round-winner payout still showed 0 after being updated to count distinct for the round, because that count came from a query run — fully subject to RLS for whoever's logged in. Once round 2's approvals were reset, the could lose access to for round 2, so they'd only see their own predictions, undercounting everyone else's. Adds , a SQL function (same mechanism already uses) that bypasses RLS entirely — now calls this via instead of querying directly. Also explicitly sets 's , belt-and-suspenders for migration 29's own subquery, which runs inside a view and already default to bypassing RLS via the view owner's permissions in Postgres 15+, but wasn't worth leaving to an unconfirmed default.
31. — creates round 4's 7 matches (added 2026-09-08), same shape/pattern as //. Fixture source: user-provided 365scores.com screenshot, checked 2026-09-08. All 14 teams already have entries in () — no new crests needed, every team name string re-verified byte-for-byte against the existing keys. Kickoff times converted from the screenshot's Israel-local display to UTC (−3h), same convention as round 3. Unlike round 3 (which needed a follow-up in migration 27, since the column didn't exist yet at seed time), is set directly in this seed's own — the column already exists — to the preceding Thursday 20:00 Israel time (2026-09-10 17:00 UTC), the established house rule. is the first match's kickoff (2026-09-13 17:30 UTC = 20:30 Israel time), matching the existing "deadline = first kickoff" convention.
32. — companion to ' new per-day locking (see that section above), found while implementing it: 's default-prediction backfill previously fired once, for every match in the round at once, at the exact moment the round's status flipped → (the round's very first kickoff) — which would've defaulted later-day matches (e.g. round 4's Monday/Tuesday games) the instant the first day's game started, defeating per-day locking entirely. , same signature: moves that backfill out of the transition step and into the existing loop that already seeds 0-0 into kicked-off-but-unscored matches one round at a time, gating it the same way — — so a match only gets defaulted once its own kickoff has actually passed.
33. — one-time DATA repair (found 2026-09-08 investigating showing an empty "נקודות מחזור 3" table): round 3's had despite being fully played and scored (7 matches, ) — confirmed via a diagnostic join against / counts per round (round 1: 9/9, round 2: 7/7, round 3: 7/0). Root cause not identified — nothing in this repo's migration history ever deletes from — but / (the real source of truth reads from) were intact, so this backfills one row per user with real rows for a round-3 match (, since they clearly played and got real scored results), then calls to populate //etc from the real data. This is also why the "מחזור אחרון" card was showing round 2's numbers, not round 3's — with round 3 empty for everyone, the round_number-desc sort correctly fell back to round 2, which migration adding the round-number label to that card's title (see above) made possible to actually confirm.
34. — changes the round-standings tiebreak rules (requested 2026-09-10): was points → exact-hit count → earliest submission time; now points → exact-hit count → whoever earned more points on one specific match per round the manager flags as "⭐ המשחק המרכזי" (the main event — e.g. a derby). The earliest-submission-time tiebreak is dropped entirely, since per the user prediction timing is no longer meant to matter for anything — this migration's last statement also clears round 4's (the Thursday-20:00 restriction from migration 27/31), and future scripts should just stop setting that column rather than giving it a value. New plus a partial unique index (, on where ) enforcing at most one per round. New RPC ( + gate, same pattern as ) does the whole round's flag-flip as one atomic — 's match-results screen () got a "⭐/☆ משחק מרכזי" toggle button per match that calls it (passing clears the round's main event). Surfaced to participants too: a gold-tinted badge/border on the flagged match everywhere it renders — ' /, and 's per-participant predictions list (border tint only there, given the compact one-line-per-match layout). ' 3rd tiebreak step updated to match.
35. — real gap found 2026-09-13, asked directly ("can other participants see tomorrow's predictions before they lock?"): (migration 25) gated visibility on the whole round's status (), not per match — since a round flips to the moment its match anywhere kicks off, every approved participant could already read everyone's predictions for later, still-open days too, once day 1 started. Per-day was fixed earlier this session (' ); per-day had been left round-wide. Drops and recreates the policy so it gates on the same day-based rule instead — , the SQL equivalent of — a match's predictions become visible once earliest kickoff has passed, independent of other days. No client-side change needed: already renders "-" for any match RLS doesn't return a row for, so a hidden day just displays as unpredicted instead of leaking real numbers.
allow-service-role-manager-actions.sql — lets a Supabase service-role-key connection (no real manager login — used by the scheduled 365scores score-update automation, since this app is Google-OAuth-only and a headless agent can't complete that login flow) satisfy is_manager() too, alongside the existing real-manager-session check. Reuses submit_match_result()/set_main_event() as-is rather than having the automation reimplement scoring math against raw tables with a full RLS-bypassing key. Applied 2026-09-13, then reverted by migration 37 below — the automation it supported turned out to be broken (cloud sandbox egress-blocks Supabase entirely) and was cancelled 2026-09-14 in favor of manual score entry.
revert-service-role-manager-actions.sql — reverts migration 36: the score-update automation it supported was cancelled (2026-09-14, user going back to manual score entry), so a bare service-role key no longer has any reason to pass is_manager(). Restores the original, narrower definition (real logged-in manager session only). Applied 2026-09-14.
add-goal-difference-bonus-points.sql — first cut of the goal-difference scoring idea (flat exact/correct/bonus model). Superseded same day by migration 39 below before ever being applied — do not run this one, run 39 instead.
restructure-scoring-into-win-samediff-draw.sql — the user asked for the scoring UI itself to be reorganized into 3 independent categories rather than one global exact/correct pair plus a flat bonus, so scoring_rules now has 5 columns instead of 3: win_hit_points/win_towards_points (actual result is a win, exact score vs correct-winner-wrong-difference), same_diff_bonus_points (a bonus added on top of win_towards_points when the winner AND the exact goal difference are both correct but not the exact score — e.g. predict 2-0, actual 3-1 → win_towards_points + same_diff_bonus_points), draw_hit_points/draw_towards_points (actual result is a draw, exact score vs correct-draw-wrong-score). No separate "same-diff hit" — confirmed with the user that an exact score always already has a matching difference too, so that case is scored by win_hit_points, not a second hit field. compute_prediction_points() (its signature changed, so explicitly dropped first) now branches on draw-vs-win first, then exact/matching-diff/neither. Manager UI (ScoringRulesModal.tsx) now shows 3 grouped sections ("ניצחון בית/חוץ", "הפרש שערים זהה" — a single "בונוס" field, not a full point value — "תיקו"); still shows the for participants () so the ladder reads as real earnable points, not raw config. Edited in place before ever being applied (caught via a live "column does not exist" error the same session) rather than a new migration number.
The first-cut UI for this (an "everyone's predictions" list on /predictions) was removed the same day — see the /leaderboard feature just below instead, which is what actually shipped.
Also, to make the manager account actually a manager (role defaults to
participant for everyone, including this account, on first login):
update users set role = 'manager' where email = '[email protected]';
Prediction game for users.drawio.pdf — original wireframes. Superseded in spirit by what's actually built, but still useful for the manager fixture-entry screen, which isn't built yet.
/manager (2026-08-26) rather than kept as a fake: a real reset means creating a new round + matches, which needs this screen first.MatchResultsModal.tsx, opened via "תוצאות מחזור" on /manager. Per-match score inputs + a per-row save button, fetches the latest round overall (not status = 'open' — see the scoring-engine note below for why that filter would break here). Requires migration 12.submit_match_result() SQL function (migration 12) does the whole recompute — predictions.points_earned (exact score beats correct-direction, sign() comparison so a predicted draw vs. an actual draw counts as correct-direction too), then a full, non-incremental recompute of round_participation.total_points/exact_score_count/correct_result_count/rank for the whole round every time a match is saved (cheap at 7 matches × ~10 participants, and always self-consistent regardless of what order matches get scored in). Deliberately not in scope: is_round_winner/crown badge (only meaningful once all 7 matches are final — separate concern, see item 6) and auto-transitioning rounds.status to 'finished'. No live-push — this app has no realtime subscriptions anywhere; participants see updated numbers the next time they load or refresh /predictions//leaderboard, same fetch-on-mount pattern as everything else. /predictions now also shows each match's final score as a small line under the participant's own prediction, once set.lock_expired_rounds() (see migration 8 above) locks a round and fills per-match default scores once its deadline passes, called lazily whenever a round-dependent page loads (src/lib/lockExpiredRounds.ts). Requires migration 8 to be applied./home stat cardsseason_stats row and their most-recently-played round_participation row, same pattern ParticipantModal.tsx already used.Half the app (NewsTicker, JackpotBadge, RoundCountdown, RoundApprovalStatus, /manager's participant lists, /leaderboard's round-points table, /home's round-comments section) independently queried rounds filtered to .eq("status", "open"), several using .single(). That's fine right up until a round's deadline actually passes and lock_expired_rounds() flips it to 'locked' — which happened for real on 2026-08-23 (round 1's deadline was 22.8.2026 20:00) and broke /manager outright with a PostgREST "Cannot coerce the result to a single JSON object" error, while the other components just silently vanished (no crash, but the jackpot badge, countdown, approval card, etc. all disappeared exactly when the manager started entering results).
Fixed with one shared helper, getCurrentRound() in src/lib/currentRound.ts — the latest round overall (order by round_number desc limit 1), not filtered by status — now used by all seven of the above instead of each having its own ad-hoc query. MatchResultsModal.tsx already had this exact fix from when it was first built (see item 2 above); this pass generalized it everywhere else and removed the duplication. predictions/page.tsx was already unaffected — it fetches every round and falls back to the last one in the list when none is 'open'.
Real matches run live — they start at 0-0 and the manager updates the score as it happens, not just once at the end. Three linked changes (migrations 13–16 above):
lock_expired_rounds() seeds a still-unscored match to 0-0 (is_final = false) once that match's own kickoff_at has passed, and computes initial predictions.points_earned/standings from it — "empty" isn't a real state once a match has actually begun. Only touches matches with home_score is null, so a result the manager may have already entered pre-lock is never clobbered. Fixed 2026-08-23 (migration 19): the original version gated this on the round locking, not each match's own kickoff — harmless when every match in round 1 shared one fake kickoff time, but wrong once migration 18 gave it real staggered kickoffs, since it would've force-seeded 0-0 for matches still days from starting the moment the round locked.matches.is_final (migration 13) drives a new "המשחק הסתיים" (the match ended) checkbox per row in MatchResultsModal.tsx, saved alongside the score via submit_match_result()'s new 4th parameter. On /predictions, the result line reads "תוצאה נוכחית" (current result) while is_final = false, "תוצאה סופית" (final result) once checked — same score-order fix from the previous commit applies to both.status now follows its matches — submit_match_result() sets it to 'finished' once every match in the round is final, and reverts it to 'locked' if a final gets unchecked again (fixing a mistake keeps the invariant true in both directions). Nothing else in the app branches on 'finished' specifically — every "current round" lookup already goes through getCurrentRound() (see the fix above), which isn't filtered by status, so a finished round keeps displaying correctly everywhere.submit_match_result()) was extracted into a shared recompute_round_standings(round_id) (migration 14) so lock_expired_rounds() can call the identical logic instead of duplicating it.scoring_rules row (default 10 / 5). Saving inserts a new versioned row (effective_from = now()); past rounds keep whatever scoring was current when they were played. Built — ScoringRulesModal.tsx, opened from a button on /manager. No migration needed (table + RLS already existed).[email protected] is the workspace/manager account and never predicts. Itay's personal participation happens through a separate account, [email protected], which plays like any other participant. The current season_stats exclusion of role = 'manager' rows is correct as-is — no code change was needed./home. Any participant can post a comment visible to everyone; each participant can have at most one comment per round, enforced both in the UI and by a DB unique constraint on (round_id, user_id). A participant can delete their own comment (round_comments_delete_own policy) to free up their one slot and post a new one instead — there's no in-place edit. Built — see item 6 above.Wording was worked out first in a reviewed glossary (a Claude Artifact, not part of this repo — every English string mapped to a proposed Hebrew one, with review comments applied) before any code changed. Key terms, now used consistently everywhere they appear:
/manager/login ("ברוך שובך").OneXTwoIcon note under Design system above for the RTL glyph-clipping bug that caused.Verification caveat: only / and /manager/login were actually visually checked in a browser (screenshotted via Playwright) — every other page requires a real Google-authenticated session, which isn't available in the dev/build environment this was built in. npx tsc --noEmit and next build both pass, and every English string was swept for with a final grep pass, but /home, /predictions, /leaderboard, /rules, /manager, and all the modals (Profile, Scoring rules, News strip, Participant popup) still want a real click-through once deployed, especially for RTL layout mirroring (nav icon order, StatCard's divide-x stat row, TopBar button side) that wasn't force-verified — it's expected to mirror correctly via the global dir="rtl" flip, but wasn't watched happen.
formatIsraelDeadline()src/lib/israelTime.tsIntl.DateTimeFormat(..., { timeZone: "Asia/Jerusalem" })/predictionsformatDeadline/predictions — reads real rounds/matches, reads/writes real predictions rows (upsert on submit). Generic across rounds (not hardcoded to round 1), though only round 1 exists so far. Round 1 fixtures: staggered 22.8-3.9.2026 (see migration 18 — originally seeded with one fake shared time). Each match row shows the team's real club crest (TeamLogo, 28px, TEAM_LOGOS in mock-data.ts) next to its name — changed 2026-08-23, replaced the earlier kit-color two-tone vertical bar (TeamColorBar/TEAM_COLORS, itself a replacement for small side-by-side dots before that). Crests are self-hosted PNGs in public/team-logos/ (downloaded from football-logos.cc rather than hotlinked, so the app doesn't depend on their CDN staying up) — verified real/working with actual HTTP requests (not just trusting the fetched URLs) before downloading, since a fetch-summarized page can hallucinate plausible-looking asset URLs. RTL gotcha (fixed 2026-08-23): the match row is a flex row that mirrors under the page's global RTL — home_box is DOM-first so it lands visually rightmost, away_box visually leftmost. The "תוצאה נוכחית" (current result, live matches only — see below for ended matches) line is plain text below the row, so it doesn't participate in that mirroring — its number order had to be set by hand to match ({finalAwayScore}-{finalHomeScore}, away first since that's the left side), not the source-order-looking home-away, which silently mismatched the row's actual left-right layout. Confirmed the box-to-side mapping with an isolated Playwright screenshot before picking the order, same as every other RTL fix this project has needed. Per-day locking (added 2026-09-08): a round's matches can span several days (round 4 is the first real case — Sun/Mon/Tue), so editability is no longer gated by the round's overall status/deadline alone. israelDateKey() (src/lib/israelTime.ts) groups matches by Israel-local calendar date; a match is locked once its own day's earliest kickoff has passed, independent of other days in the same round — Sunday's game starting no longer locks Monday's or Tuesday's. Replaced the old "שליחת ניחוש" → "עדכון ניחוש" toggle (which made the whole round read-only after one submit, needing a manual click to re-open editing) with score boxes that just stay directly editable for any not-yet-locked match; the button is now a single always-available "שמירת ניחושים" save, enabled once every still-editable match is filled in (already-locked matches are excluded from that check and from what gets upserted, so a never-predicted locked match can't get silently overwritten with 0-0 — Number("") is 0). See update-lock-expired-rounds-per-day-default-fill.sql for the matching backend fix this needed.
Match status (added 2026-08-23): every match on /predictions is one of not-started / live / ended, computed client-side (matchStatus(), src/lib/matchStatus.ts — shared with /leaderboard's round summary), no new column — is_final wins over the clock so a manager can end a match early. A 30s interval ticks the page's now state so a match flips from not-started to live on its own if the page is left open across kickoff, without needing a refresh (this app still has no realtime subscriptions anywhere). Not-started and live matches render via MatchRow — its own bordered card with a colored status pill (gray "טרם החל", green "בשידור חי" pulsing dot).
Ended-match card (EndedMatchCard, redesigned 2026-08-24 from a Claude Design handoff — design_handoff_prediction_result_card/Match Result Card.dc.html, variant "1B"): a colored status rail + body (crests, the real final score large and centered, the user's prediction struck through on a miss) + a tinted points column, all pixel-matched from the handoff's exact spec (colors, type, spacing) rather than freehanded. Replaced an earlier gold/silver/gray tier-colored card. The color/label tier (exact hit / correct direction / miss) is derived by directly comparing the prediction to the actual score (deriveOutcome()) — mirroring submit_match_result()'s own exact/sign() comparison — rather than from the points_earned value, since scoring_rules' point amounts are manager-configurable and a value-based threshold would silently break if they change. RTL note: the card's root row (rail, body, points column) and its meta row (status text, outcome label) both mirror under the page's global RTL exactly like MatchRow above — verified directly against the handoff's raw HTML source (not just eyeballing a screenshot) after an initial pass had the status text and outcome label swapped. Second RTL bug, fixed 2026-08-24: the score itself (both the real score and the prediction line) is forced dir="ltr" — but that only fixes digit shaping, not which side it visually lands on. Home renders on the right (RTL mirroring), yet a home-first string ({actualHome} : {actualAway}) reads left-to-right regardless, putting home's digit on the left — right next to the away team box. Away-first ({actualAway} : {actualHome}) is what actually lines each digit up with its own team; the handoff's own literal markup had this the "obvious" home-first way, invisible with its own near-even sample data (2:1, 1:1) but obvious once real, lopsided scores showed up. Same fix this codebase's previous card design already needed once for its own current-result line.
/leaderboard — three real tables (this round's points, season points, most played), each row clickable → opens ParticipantModal with that participant's avatar/name/last-round/season stats. Round switcher added 2026-08-31: same pattern as /predictions — fetches every round, defaults to whichever is open (or the latest), prev/next chevrons browse the rest. Only the round-points table (and its RoundMatchSummary not-started/live/ended badges) follows the selected round; season points and most-played stay whole-season regardless. This is also what makes the crown/payout badge (below) work for any past finished round, not just whichever is current — browse back to it and the winner/pot show the same way they did live. Predictions link: while the selected round is locked (started, not yet finished), the modal also shows a "צפייה בניחושי המשתתף" text link above the stat cards. Clicking it swaps the modal to a second screen — not shown alongside the stats, replaces them — a plain list, one line per match, showing that participant's predicted score for every match in the round (home team — score — away team), plus a "חזרה" link back to the stats screen. (Went through two shapes before landing here: first an inline section under the stat cards, then a one-match-at-a-time slide carousel — the user's actual ask was a full list, all matches at once.) The link itself only appears when the round is locked; disappears once it's open (nothing to show yet) or finished (not the point of this view once the round's actually over — check the regular per-round stats instead). Reads via predictions_select_locked_round (migration 25) — the viewer has to themselves be an approved participant of that round for it to return any rows, independent of whose predictions are being viewed.
Round-winner payout (added 2026-08-31, fixed 2026-09-02): once the selected round is finished (every match final), the round-points table's rank-1 row gets a tilted gold CrownIcon on their avatar and a shining gold "זכה בקופה: X ₪" label above their name (LeaderRow's crown/jackpotLabel props, applied only to rows[0] — already the true rank-1 winner since rows arrives pre-sorted by the DB's own tiebroken rank). Payout = participant-count × ENTRY_FEE_ILS (20) minus one entry fee — second place gets their own buy-in refunded rather than it going to the winner. TopBar's JackpotBadge (the running "קופה: X ₪" pill) now also resets to 0 once the round is finished, instead of continuing to show the already-paid-out total.
Bug fixed 2026-09-02: the participant count originally came from a live payment_status = 'approved' count — same class of bug as season_stats.rounds_played (see migration 29 above). /manager's "reset approved participants" action flips payment_status back to waiting for an already-finished round to prep the next one's signups, which silently zeroed out that round's displayed payout (a real production sighting: round 2's winner showed "זכה בקופה: 0 ₪" after round 3's signups reset round 2's approvals). Fixed the same way as rounds_played — pastParticipantCount now counts distinct predictions.user_id for that round's matches instead of querying payment_status at all, immune to later resets.
— removed 2026-09-20 along with /home round discussion (RoundComments.tsx)/home. The round_comments table and its policies are still in the database, just unused.
/rules — static text, redesigned 2026-08-24 into cards (before-kickoff, scoring, jackpot/tiebreakers, default-prediction fallback) instead of a plain bulleted list. The scoring card reads the live scoring_rules row instead of hardcoding point values. Documents the 3-step round tiebreak (points → exact-hit count → who submitted first, see migration 20 below) that recompute_round_standings() actually computes.
/manager — real approve/un-approve (writes round_participation.payment_status), a remove action on the waiting list (deletes the round_participation row outright, for someone who clicked "money sent" without paying — sends them back to the pre-approval screen rather than a rejected one), real leaderboard widget, a "Scoring rules" editor (prefilled from the current scoring_rules row, saves a new versioned row — see decision below), and a "News strip" editor (see below). "Reset round" was removed 2026-08-26 — it was local-only/fake (cleared the on-screen lists but never touched the database) and risked a manager believing a reset had happened before a real-money round. Re-add it once the fixture-entry screen exists to back it with a real reset (see Phase 0 below).
"Reset approved participants" (added 2026-08-31, unrelated to the removed one above) — a real, DB-backed bulk unapprove(): every currently-approved round_participation row for the round is set back to waiting in one write. Button (איפוס משתתפים מאושרים, danger-styled) only renders once roundStatus === "finished" (every match in the round is final) and there's at least one approved participant — behind a confirm popup ("are you sure", names the exact count and round number) before it fires. Meant as cleanup once a round is fully done, ahead of the next round's sign-ups.
points_earnedtotal_pointsexact_score_countcorrect_result_countrankadd-match-is-final.sql — adds matches.is_final (default false), tracking whether a score is live or final. Applied — verified 2026-08-26 via a direct PostgREST query against the live project (select=id,is_final returned 200 [], not a column-not-found error); this note previously said "Not yet applied" but was stale.add-recompute-round-standings-function.sql — extracts the aggregate/rank recompute out of submit_match_result() into a shared recompute_round_standings(round_id), so lock_expired_rounds() can call the same logic. Applied — verified 2026-08-26 by calling the RPC live (got 204, not a function-not-found error); this note previously said "Not yet applied" but was stale.update-lock-expired-rounds-seed-scores.sql — extends lock_expired_rounds() (migration 8) to seed every still-unscored match to 0-0 the moment a round locks, and compute initial points/standings from that baseline. create or replace, same signature, safe over the existing function (already applied — superseded by migration 17 below, which fixes a gap in this version).update-submit-match-result-final-flag.sql — extends submit_match_result() with a 4th parameter, p_is_final; the round's own status now follows its matches ('finished' once every match in the round is final, reverts to 'locked' if a final gets unchecked). Drops the old 3-argument function first, since the signature changed (already applied).update-lock-expired-rounds-selfheal.sql — fixes a gap in migration 15: the 0-0 seeding was nested inside the loop that only fires for rounds transitioning from open to locked at that exact moment, so round 1 (already locked days before migration 15 existed) never re-qualified and stayed unscored. Splits seeding into its own loop over any round with status in ('locked', 'finished') that still has unscored matches, regardless of when it locked — self-heals round 1 retroactively the next time this function runs (any page load). Superseded by migration 19 either way (see below), so whether this one specifically ran no longer matters.fix-round1-kickoff-times.sql — data-only fix, not a schema change: round 1's kickoff_at was seeded with one fake time (22.8 20:00) for all 7 matches; real fixtures are staggered from 22.8 through 3.9. Corrects the 4 not-yet-finished matches' kickoff_at to their real kickoff times (source: 365scores.com, checked 2026-08-23) so the new not-started/live/ended status badges on /predictions (see below) are accurate. Home/away order was double-checked against the real fixture list and is already correct — untouched. v2, corrected 2026-08-23: v1 encoded the 365scores clock times as Israel-local (+03:00), but they're actually UTC — caught live when the Netanya-Sakhnin card showed 20:00 on 365scores against this file's 17:00, exactly the UTC+3 gap. v2 encodes them as literal UTC (Z) instead. Re-running v2 is safe even if v1 was already applied (plain UPDATE keyed on team names). Applied.update-lock-expired-rounds-per-match-kickoff.sql — fixes a real bug in migrations 15/17's 0-0 seeding: it seeded every unscored match in a locked round to 0-0, with no check on that match's own kickoff_at — harmless while every match shared one fake kickoff time, but wrong now that migration 18 gives round 1 real staggered kickoffs (a match still days from actually starting would get force-seeded 0-0 and start contributing to standings). Gates the seeding loop, and the points update that follows it, on kickoff_at <= now() per match. Supersedes migration 17 — safe to run directly even if 17 was never applied. recompute_round_standings() already handles null points_earned/home_score correctly, so it didn't need changes. Applied.add-first-prediction-tiebreak.sql — adds a third round-standings tiebreaker (added 2026-08-24): after total points and exact-hit count, whoever submitted their predictions earliest wins the tie. Uses min(predictions.submitted_at) per participant per round — no schema change needed, since sendPrediction() never includes submitted_at in its upsert payload, so PostgREST leaves it untouched on an edit; it's already "first created, not last updated" by construction. create or replace on recompute_round_standings(), same signature. Applied.fix-retroactive-early-seed-repair.sql — one-time DATA repair (not a function change), added 2026-08-24 after a user reported the leaderboard showing 10 points for a round where the per-match breakdown only summed to 5. Root cause: migration 19 fixed lock_expired_rounds() going forward, but never cleaned up matches it had already force-seeded to 0-0 while still days from their real kickoff (a leftover from migrations 15/17's un-gated seeding) — one such match happened to match a participant's drawn prediction, scoring a spurious +5 that total_points (a cached sum) faithfully included. Nulls out home_score/away_score/points_earned for any match whose kickoff_at is still in the future (a real score can't exist yet, so any score present is leftover bad seeding), then calls recompute_round_standings() for every locked/finished round. Re-running is safe (a no-op once no future-kickoff match has a score). Run this once, then check /leaderboard.seed-round2.sql — creates round 2's 7 matches (added 2026-08-24), same shape as seed-round1.sql. Round 1 was treated as a pilot and left unfinished on purpose (2 matches never played) rather than waited out — safe to do because getCurrentRound() (src/lib/currentRound.ts) picks the round with the highest round_number regardless of status, so every page cuts over to round 2 the moment this runs, with round 1 still browsable read-only via /predictions' prev/next picker. One consequence: /manager's "תוצאות מחזור" results modal also uses getCurrentRound(), so it stops reaching round 1's matches once round 2 exists — finalizing those 2 leftover matches later would need direct SQL, not the modal. Fixture source: 365scores.com, checked 2026-08-24; kickoff times are literal UTC per migration 18's finding.add-backfill-late-approval-function.sql — adds backfill_late_approval(round_id, user_id) (added 2026-08-26, found live during round 2). Fixes a real gap: lock_expired_rounds()'s one-time default-prediction fill only covers participants already approved at the moment a round locks — someone approved after that has zero predictions rows for the round, so their score isn't stale, it's genuinely never computed. This backfills their default prediction for any already-kicked-off match they're still missing one for, scores it, and recomputes. src/app/manager/page.tsx's approve() now calls this after every approval (harmless no-op for an on-time approval), so a late approval self-heals immediately instead of needing a manual fix. Run this once in the SQL editor before it'll work — then, for anyone already approved late before this migration existed, toggle them to "המתנה" and back to "אישור" in /manager to trigger the backfill retroactively.add-predictions-visible-after-round-locked.sql — adds a second, additive RLS select policy (predictions_select_locked_round) so any participant can read everyone's predictions rows for a round once that round's status <> 'open' — previously predictions_select_own_or_manager (rls.sql) meant only the predictor themselves or the manager could read a prediction row at all. Superseded by migration 25 below — the original version scoped this to any logged-in user, not just that round's approved participants.fix-predictions-locked-round-approved-only.sql — tightens migration 24: drops and recreates predictions_select_locked_round so it additionally requires the viewer to have an approved round_participation row for that specific round, not just any authenticated user of the app. Safe to run whether or not migration 24 ran first. Not yet applied — run this instead of (or in addition to, it's idempotent) migration 24.seed-round3.sql — creates round 3's 7 matches (added 2026-09-01), same shape/pattern as seed-round1.sql/seed-round2.sql. Fixture source: user-provided 365scores.com screenshot, checked 2026-09-01. All 14 teams already have entries in TEAM_LOGOS (mock-data.ts) — no new crests needed, and every team name string was verified byte-for-byte against the existing keys (catches the kind of subtle mismatch, e.g. the internal " in בית"ר ירושלים, that would otherwise silently break a crest lookup). Kickoff times converted from the screenshot's Israel-local display to UTC (−3h) for storage, per the corrected convention fix-round1-kickoff-times.sql/seed-round2.sql established — not yet applied, creating it as 'open' will cut every page over to round 3 the moment it runs, the same way round 2's seed did to round 1.add-predictions-open-at.sql — adds rounds.predictions_open_at (nullable timestamptz), letting a round accept payment/approval before predictions can actually be submitted. Null means no restriction (rounds 1-2, and any future round where this isn't set, behave exactly as before). Set manually per round at seed time, same pattern as deadline_at — no automatic day-of-week derivation. Sets round 3's to 2026-09-03 20:00 Israel time (17:00 UTC), the "Thursday 20:00" house rule. Not yet applied./leaderboardpredictions.user_idpredictions from the browserpredictions_select_locked_roundround_participant_count(p_round_id)security defineris_manager()/leaderboardsupabase.rpc(...)predictionsseason_statssecurity_invoker = falseexists (select ... from predictions ...)seed-round4.sqlseed-round1.sqlseed-round2.sqlseed-round3.sqlTEAM_LOGOSmock-data.tsUPDATEpredictions_open_atinsertdeadline_atupdate-lock-expired-rounds-per-day-default-fill.sql/predictionslock_expired_rounds()openlockedcreate or replace functionm.kickoff_at <= now()fix-round3-missing-participation.sql/leaderboardround_participationstatus = 'finished'matchesround_participationround_participationpredictionsmatchesrecompute_round_standings()round_participationpredictionspayment_status = 'approved'recompute_round_standings()total_pointsrank/homeadd-match-main-event-tiebreak.sqlpredictions_open_atseed-roundN.sqlmatches.is_main_event boolean not null default falsematches_one_main_event_per_roundround_idis_main_eventset_main_event(p_round_id, p_match_id)security defineris_manager()submit_match_result()update/managerMatchResultsModal.tsxnull/predictionsMatchRowEndedMatchCardParticipantModal/rulesfix-predictions-visible-per-day.sqlpredictions_select_locked_roundr.status <> 'open''locked'/predictionsisDayLocked()(kickoff_at at time zone 'Asia/Jerusalem')::dateisraelDateKey()ParticipantModal/ruleswin_towards_points + same_diff_bonus_pointsseed-round5.sql — creates round 5's 10 matches (added 2026-09-19), the first round using the new 10-matches-per-round format (up from 7 — see the same day's decision). Mixes 6 Israeli Premier League fixtures with 4 European club matches (Stuttgart-Dortmund, Roma-Inter, Brighton-Arsenal, Sevilla-Barcelona), using the 8 European crests added the same day. Fixture source: user-provided screenshots, checked 2026-09-19; kickoff times confirmed already Israel-local as shown (not the European matches' own country time) before converting to UTC. Every team name verified programmatically against TEAM_LOGOS in constants.ts before this file was written, not just eyeballed. predictions_open_at intentionally left unset, per migration 34's "future seeds should stop setting that column." Applied 2026-09-18.
fix-backfill-late-approval-scoring-columns.sql — real bug found live 2026-09-18, the moment a round-5 participant was approved: backfill_late_approval() (migration 23) had its own independent copy of the old exact/correct scoring formula that migration 39 missed when restructuring scoring_rules into 5 columns — submit_match_result() and lock_expired_rounds() were both updated at the time, this third copy wasn't, so every approval started raising "column scoring_rules.exact_score_points does not exist" (this function runs on every approval, not just late ones — see /manager's approve()). Fixed the same way as 38/39: switched to the shared compute_prediction_points() helper instead of a fourth copy-pasted case expression. Not yet applied — check before round 6's first approval, or every approval will hit the same error.
seed-round6.sql — creates round 6's 18 matches (added 2026-09-28), the first round that's entirely Champions League — no Israeli Premier League fixtures at all, one match for every one of the 36 teams seeded in migrations 39/40 (add-champions-league.sql / add-champions-league-remaining-teams.sql). Fixture source: user-provided screenshots, checked 2026-09-28; kickoffs (19:45/22:00) treated as already Israel-local — they line up exactly with the real UEFA CL 18:45/21:00 CET slots, Israel running one hour ahead of CET on these dates before both Israel's and Europe's DST end (~25.10.2026) — converted to UTC (−3h) for storage. Every team name verified programmatically against the teams rows inserted by migrations 39/40, not just eyeballed. predictions_open_at intentionally left unset, per migration 34. v1 silently inserted 0 rows — its where name = '2026/27 Season' lookup missed the real seasons.name, which has a pre-existing typo ('2026/27 Seasor'); fixed in place (v2) by hardcoding the season's actual id instead of matching on name at all. All 36 teams now have a TEAM_LOGOS crest (added same week). Applied 2026-09-29 (v2).
fix-round5-missing-participation.sql — same exact gap as migration 33 (fix-round3-missing-participation.sql), this time on round 5: 10 matches, fully scored, status = 'finished', but 0 round_participation rows — found 2026-09-29 via /leaderboard showing an empty "נקודות מחזור 5" table. Confirmed real predictions exist (9 users × 10 matches = 90 rows, everyone predicted everything) before writing this, so it's a genuine backfill, not a guess. Root cause found this time (not just patched): /manager's "הסרה" button on the waiting list used to hard-delete the round_participation row — safe before 2026-09-08, dangerous after, once submitting real predictions became the only way onto that list at all (see src/app/manager/page.tsx's removeFromWaiting(), fixed same session to set payment_status = 'rejected' instead of deleting). Applied 2026-09-29, confirmed via the same diagnostic query (participation_count: 9).
seed-round7.sql — creates round 7's 5 matches (added 2026-10-15), back to a plain Israeli Premier League round after round 6's all-Champions-League one — no European fixtures this time. Fixture source: user-provided screenshot, checked 2026-10-15; all 5 matches play Saturday 17.10.2026, Israel-local kickoffs (18:45–19:30) converted to UTC (−3h) — still IDT on this date. All 10 teams already existed in teams/TEAM_LOGOS, no new crests needed; every team name verified programmatically before writing the file. season_id is hardcoded directly (not looked up by name) per migration 42's lesson — seasons.name still has its pre-existing typo ('2026/27 Seasor'), unfixed as of this writing. predictions_open_at intentionally left unset, per migration 34. Not yet applied.
add-round7-day2-matches.sql — adds round 7's 2nd day (added 2026-10-15, same session): Sunday 18.10.2026, 3 more matches on top of migration 44's Saturday 5 — same multi-day-round shape as round 4, per-day locking already handles it with no app-code change. Mixes one more Israeli fixture (Hapoel Beer Sheva–Hapoel Jerusalem) with two European ones (Juventus–Lazio, Leeds–Man Utd). Juventus, Lazio, and Leeds are brand new teams — none were on the 36-team Champions League list from migrations 39/40, so only linked to their real domestic league (Serie A, Serie A, Premier League respectively), not Champions League. Added names-only, no crests yet — same as the 30-team CL batch, ask for crest URLs separately. Kickoffs treated as already Israel-local (21:45/16:00/20:15), reasoned from standard Serie A/Premier League slots the same way round 6's did, not explicitly re-confirmed with the user this time — worth double-checking if the times look off once played. Looks up round 7 by round_number, not by season — must run after migration 44, since it's a silent no-op if round 7 doesn't exist yet. Not yet applied.
add-round7-day3-match.sql — adds round 7's 3rd day (added 2026-10-15, same session): Monday 19.10.2026, 1 more match (Beitar Jerusalem–Maccabi Tel Aviv, both already in teams/TEAM_LOGOS, no new crests needed) on top of migrations 44/45's 8. Straightforward Israeli fixture, kickoff already Israel-local, no timezone ambiguity. Same round-number lookup pattern as migration 45 — must run after 44 and 45. Not yet applied.
add-round7-day2-extra-match.sql — adds a 4th match to round 7's Sunday 18.10.2026 (added 2026-10-15, same session): Real Madrid–Sevilla, La Liga. Both teams already exist, no new crests needed. Kickoff (22:00) treated as already Israel-local, same reasoning as migration 45's European kickoffs, not re-confirmed. Same round-number lookup pattern — must run after migration 44; independent of 45/46 otherwise. Not yet applied.
seed-round7-all.sql — single-paste convenience version combining everything migrations 44–47 do (round 7 + all 10 matches across 3 days + the 3 new teams) into one script, requested by the user to avoid pasting four separate files. Run this one file OR 44+45+46+47 in order — never both, since matches has no unique constraint guarding against duplicate rows on a second run. Migrations 44–47 stay as the authoritative, individually-documented history; this is purely a convenience copy. Not yet applied.
add-rank-movement-tracking.sql — adds round_participation.previous_rank, snapshotted inside recompute_round_standings() itself every time it runs (set previous_rank = rp.rank, rank = ranked.rnk — the old-row value of rank is what lands in previous_rank, correctly landing on "no change" when a recompute doesn't actually move anyone). Powers a new /leaderboard feature: a small green/red arrow on the top-3 avatars showing whether that participant's rank just improved or dropped, only while the round is locked (live) — nothing shows once finished, since standings are final by then. create or replace function, same signature, based on the latest definition in fix-standings-count-only-started-matches.sql (confirmed via grep across every .sql file first, per this project's own redefinition pitfall). Applied 2026-10-19 — but note: the app code shipped in the same commit as this migration, and it was applied after the app code was already live, which broke round 7's round-points table (query errored on the then-missing column, silently rendering zero rows) until the user ran this. Lesson: when app code depends on a new column, get the migration applied before/with the code going live, not after — don't assume there's always a safe grace period. Also: previous_rank only reflects changes from the moment this migration ran onward — it can't retroactively reconstruct rank history from before the column existed, so no arrows appear until the next real recompute happens after applying it.
seed-round8.sql — creates round 8's 7 matches (added 2026-10-20): 5 Israeli Premier League fixtures + 2 European (Chelsea–Tottenham, Napoli–Roma), all Saturday 24.10.2026. Napoli and Roma already existed with crests; Chelsea and Tottenham are brand new, added names-only, linked to Premier League, no crests yet. This is the last round before Israel's/the UK's/Italy's DST all end (25.10.2026) — the next round's European kickoffs, if any, need a different UTC offset than the -3h used here and every prior round. season_id hardcoded directly, per migration 42's lesson. Not yet applied.
add-round8-day2-match.sql — adds round 8's 2nd day (added 2026-10-20, same session): Sunday 25.10.2026, 1 match (Hapoel Petah Tikva–Beitar Jerusalem, both already existing teams, no new crests needed). The predicted DST wrinkle from migration 50 landed immediately — this match kicks off at 20:15, after Israel's clocks move back from IDT to IST (UTC+2) at 2am that same morning, so it converts with -2h, not the -3h every single prior match in this project has used (including this same round's own Saturday 7, still on IDT). Looks up round 8 by round_number — must run after migration 50. Not yet applied.
add-round8-day3-match.sql — adds round 8's 3rd day (added 2026-10-20, same session): Monday 26.10.2026, 1 match (Maccabi Haifa–Hapoel Beer Sheva, both already existing teams). Fully past the DST boundary now, so also -2h (IST) like migration 51. Looks up round 8 by round_number — must run after migration 50. Not yet applied.
seed-round8-all.sql — single-paste convenience version combining everything migrations 50–52 and 54 do (round 8 + all 10 matches across 3 days + the 2 new teams) into one script, same pattern as seed-round7-all.sql. Updated in place (never applied yet) when migration 54 was added, rather than leaving it stale. Run this one file OR 50+51+52+54 in order — never both. Migrations 50–52 and 54 stay as the authoritative, individually-documented history. Not yet applied.
add-round8-day2-extra-match.sql — adds a 2nd match to round 8's Sunday 25.10.2026 (added 2026-10-20, same session): Barcelona–Real Madrid (El Clásico), La Liga. Both teams already exist, no new crests needed. Kickoff (22:00) treated as already Israel-local, same -2h (IST) offset as migration 51's Sunday match — Spain's own DST ends this same day too (EU-wide), so the Israel-vs-Spain +1h relationship holds the same before and after. Same round-number lookup pattern — must run after migration 50; independent of 51/52 otherwise. Not yet applied.
seed-round9.sql — creates round 9 with its Saturday 31.10.2026 matches (added 2026-10-28): 4 Israeli Premier League fixtures + Bayern–Dortmund. All teams already existed with crests; also links Bayern to Bundesliga (it was Champions League only). First seed fully on IST, so -2h. More days, if any, go in separate add-round9-*.sql files. Applied via migration 58 (seed-round9-all.sql) — do NOT run separately.
add-round9-day2-matches.sql — adds round 9's Sunday 1.11.2026 (added 2026-10-28, same session): Liverpool–Arsenal, Maccabi Petah Tikva–Hapoel Beer Sheva, Juventus–Napoli. All teams already existed with crests; also links Liverpool to Premier League and Napoli to Serie A (both were Champions League only). IST, -2h. Looks up round 9 by round_number — must run after migration 55. Applied via migration 58 (seed-round9-all.sql) — do NOT run separately.
add-round9-day3-matches.sql — adds round 9's Monday 2.11.2026 (added 2026-10-28, same session): Hapoel Ramat Gan–Maccabi Tel Aviv, Hapoel Tel Aviv–Maccabi Haifa, both 20:30. Existing teams, no new crests/links. Brings round 9 to 10 matches. Must run after migration 55. Applied via migration 58 (seed-round9-all.sql) — do NOT run separately.
seed-round9-all.sql — single-paste convenience version of migrations 55–57 (round 9 + all 10 matches + the 3 league links). Run this one file OR 55+56+57 in order — never both. Applied 2026-10-28.
add-round9-day2-superclasico.sql — adds an 11th match to round 9 (added 2026-10-28, after 58 was applied): Boca Juniors–River Plate, Sunday 1.11.2026 20:00 Israel time. Two new teams (בוקה ג׳וניורס, ריבר פלייט) + new league Liga Profesional Argentina. Crests added to public/team-logos/ + TEAM_LOGOS. Applied 2026-10-28.
add-round-intro-message.sql — one-time per-round intro popup (added 2026-10-28): rounds.intro_message (manager edits it in /manager → "הודעת מחזור", RoundIntroModal.tsx) + round_intro_seen (user_id, round_id) so each participant sees it once per round, across devices (RoundIntroPopup.tsx, mounted on /predictions, /leaderboard, /rules). Also sets round 9's message. Applied 2026-10-28.
add-round-popup-settings.sql — replaces 60's per-round rounds.intro_message (left in place, unread) with a round_popup singleton (title, body, main_color, bg_color, show_once) edited from /manager → "הודעת מחזור", with live preview. One saved config that automatically re-triggers every new round (seen still tracked per round in round_intro_seen). show_once = false → shown once per browser session (sessionStorage) for the whole round. Applied 2026-10-28.
is_round_winnerRoundComments.tsx/home