From 8da2b66ac60f0c573daf3952f9addc2c0e5a9dee Mon Sep 17 00:00:00 2001
From: Kalle <38327916+Sendouc@users.noreply.github.com>
Date: Sun, 16 Aug 2026 19:14:22 +0300
Subject: [PATCH] Diff leaderboard
---
MIGRATION.md | 8 +++
apps/web-react/app/db/seed/dev/misc.ts | 11 +++-
apps/web-react/e2e/leaderboards.spec.ts | 13 +++-
apps/web/package.json | 2 +-
apps/web/src/lib/components/Image.svelte | 6 +-
.../src/lib/components/layout/Layout.svelte | 6 ++
.../components/layout/TopRightButtons.svelte | 5 ++
apps/web/vite.config.ts | 11 ++++
migration-manifest.json | 6 +-
packages/components/src/Select.svelte | 4 +-
pnpm-lock.yaml | 10 +--
tooling/differ/README.md | 4 ++
tooling/differ/scripts/memwatch.sh | 64 +++++++++++++++++++
tooling/differ/src/differ.ts | 39 +++++++++++
tooling/differ/src/prepare.ts | 1 +
tooling/differ/src/servers.ts | 5 +-
16 files changed, 177 insertions(+), 18 deletions(-)
create mode 100755 tooling/differ/scripts/memwatch.sh
diff --git a/MIGRATION.md b/MIGRATION.md
index 943d2353c..3b66183a4 100644
--- a/MIGRATION.md
+++ b/MIGRATION.md
@@ -193,3 +193,11 @@ zod codec.
a manual `Response` (Kit's `redirect()` only allows 3xx).
- The differ impersonates with `maxRedirects: 0` so the redirect target may be
a not-yet-migrated route.
+- Numeric `width`/`height` on `` are set through the DOM property setters
+ on hydration, which truncate fractions (`27.76` → `27`, an 1px layout shift
+ vs React per image). Pass dimension attributes as strings when they can be
+ fractional — `Image.svelte` does this for all its images.
+- SvelteKit 3 + adapter-node derive the CSRF/remote-function self-origin from
+ proxy headers, guessing `https://` bare; e2e builds set
+ `paths.origin` + `csrf.trustedOrigins: ["*"]` (see `vite.config.ts`) or every
+ tooling/e2e form POST is rejected 403.
diff --git a/apps/web-react/app/db/seed/dev/misc.ts b/apps/web-react/app/db/seed/dev/misc.ts
index 1e2a71754..4cf017d38 100644
--- a/apps/web-react/app/db/seed/dev/misc.ts
+++ b/apps/web-react/app/db/seed/dev/misc.ts
@@ -41,9 +41,14 @@ export async function seedMisc({
await seedNotifications(users, tournaments);
await seedUserReports(users, sendouq);
- await LiveStreamFactory.replaceAll(
- users.showcaseIds.slice(0, STREAM_COUNT).map((userId) => ({ userId })),
- );
+ // the differ seeds a world where nobody is live on Twitch: live-stream chrome
+ // (sidebar streams, friend stream activity) depends on the tournament cluster,
+ // which migrates to apps/web in a later wave
+ if (process.env.SEED_QUIET_WORLD !== "true") {
+ await LiveStreamFactory.replaceAll(
+ users.showcaseIds.slice(0, STREAM_COUNT).map((userId) => ({ userId })),
+ );
+ }
await SplatoonRotationFactory.replaceAll();
}
diff --git a/apps/web-react/e2e/leaderboards.spec.ts b/apps/web-react/e2e/leaderboards.spec.ts
index 1491b748b..e29c85a79 100644
--- a/apps/web-react/e2e/leaderboards.spec.ts
+++ b/apps/web-react/e2e/leaderboards.spec.ts
@@ -10,6 +10,16 @@ test.describe("Leaderboards", () => {
page,
factories,
}) => {
+ // season 1 renders its first ten entries through the static top-ten
+ // showcase (names come from top-ten.json, not the database), so the
+ // asserted users must rank below ten fillers to appear as normal rows
+ for (let i = 0; i < 10; i++) {
+ const filler = await factories.UserFactory.create();
+ await factories.SkillFactory.create(
+ { userId: filler.id, mu: 40 - i },
+ { matchesCount: 10 },
+ );
+ }
const better = await factories.UserFactory.create({
discordName: "BetterPlayer",
});
@@ -17,7 +27,7 @@ test.describe("Leaderboards", () => {
discordName: "WorsePlayer",
});
await factories.SkillFactory.create(
- { userId: better.id, mu: 30 },
+ { userId: better.id, mu: 25 },
{ matchesCount: 10 },
);
await factories.SkillFactory.create(
@@ -52,7 +62,6 @@ test.describe("Leaderboards", () => {
weaponSplId: 40,
});
await factories.XRankPlacementFactory.create({
- playerUserId: zonesPlayer.id,
name: LOW_POWER_NAME,
playerSplId: `runner-up-${zonesPlayer.id}`,
mode: "SZ",
diff --git a/apps/web/package.json b/apps/web/package.json
index e9eaf511b..cafde1505 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -20,7 +20,7 @@
},
"dependencies": {
"@epic-web/cachified": "5.6.3",
- "@lucide/svelte": "0.544.0",
+ "@lucide/svelte": "1.28.0",
"@sendou/components": "workspace:*",
"@sendou/in-game-lists": "workspace:*",
"@sendou/utils": "workspace:*",
diff --git a/apps/web/src/lib/components/Image.svelte b/apps/web/src/lib/components/Image.svelte
index 7e944742e..d1e370a25 100644
--- a/apps/web/src/lib/components/Image.svelte
+++ b/apps/web/src/lib/components/Image.svelte
@@ -34,12 +34,14 @@ let {
{};
+
const breadcrumbs = $derived(
(page.data as { breadcrumbs?: Breadcrumb[] }).breadcrumbs ?? [],
);
@@ -422,6 +426,8 @@ function formatRelativeDate(timestamp: number) {
showSupport={Boolean(!user?.roles.includes("MINOR_SUPPORT"))}
showSearch={Boolean(user)}
isLoggedIn={Boolean(user)}
+ onChatToggle={user ? noopUntilChatRebuild : undefined}
+ onChatModalToggle={user ? noopUntilChatRebuild : undefined}
chatUnreadCount={0}
/>
diff --git a/apps/web/src/lib/components/layout/TopRightButtons.svelte b/apps/web/src/lib/components/layout/TopRightButtons.svelte
index 5d5ca4ac2..496695c72 100644
--- a/apps/web/src/lib/components/layout/TopRightButtons.svelte
+++ b/apps/web/src/lib/components/layout/TopRightButtons.svelte
@@ -254,6 +254,11 @@ const isMac = $derived(
font-size: var(--font-2xs);
background-color: var(--color-bg-higher);
font-family: inherit;
+
+ /** needs to go away so we have enough space even with both side panels open */
+ @media screen and (max-width: 1100px) {
+ display: none;
+ }
}
.chatButtonWrapperPersistent {
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
index 31a21840b..9d478f010 100644
--- a/apps/web/vite.config.ts
+++ b/apps/web/vite.config.ts
@@ -3,6 +3,8 @@ import adapter from "@sveltejs/adapter-node";
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";
+const IS_E2E_BUILD = process.env.VITE_E2E_TEST_RUN === "true";
+
export default defineConfig(({ mode }) => ({
define: {
__GIT_COMMIT__: JSON.stringify(process.env.RENDER_GIT_COMMIT ?? ""),
@@ -19,6 +21,15 @@ export default defineConfig(({ mode }) => ({
adapter: adapter(),
compilerOptions: { experimental: { async: true } },
experimental: { remoteFunctions: true },
+ // e2e/differ tooling POSTs (impersonate, theme) carry no Origin header,
+ // and the adapter guesses https:// when no proxy headers are present, so
+ // origin checks can never pass against the http test servers
+ ...(IS_E2E_BUILD
+ ? {
+ csrf: { trustedOrigins: ["*"] },
+ paths: { origin: process.env.VITE_SITE_DOMAIN },
+ }
+ : {}),
}),
],
test: {
diff --git a/migration-manifest.json b/migration-manifest.json
index 4932282cf..16d132683 100644
--- a/migration-manifest.json
+++ b/migration-manifest.json
@@ -76,8 +76,8 @@
"status": "pending"
},
"leaderboards": {
- "status": "migrated",
- "notes": "phase 2 vertical slice; verified pending differ + e2e run"
+ "status": "verified",
+ "notes": "phase 2 vertical slice; differ clean (pixel/head/resource/aria) and e2e green against both apps 2026-08-16"
},
"lfg": {
"status": "pending"
@@ -730,7 +730,7 @@
"feature": "top-search"
},
"features/leaderboards/routes/leaderboards.tsx": {
- "status": "migrated",
+ "status": "verified",
"path": "/leaderboards",
"feature": "leaderboards",
"notes": "ported by hand as the phase 2 golden slice; loader -> getLeaderboards query, action -> skipTeam/unskipTeam commands"
diff --git a/packages/components/src/Select.svelte b/packages/components/src/Select.svelte
index 368046ced..826203b21 100644
--- a/packages/components/src/Select.svelte
+++ b/packages/components/src/Select.svelte
@@ -246,6 +246,7 @@ const hasVisibleItems = $derived(items.size > 0);
const uid = $props.id();
const labelId = $derived(label ? `${uid}-select-label` : undefined);
+const valueId = `${uid}-select-value`;