From e3e071b6a5060dbaa2d75d65c729e4d560a0f02e Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sun, 16 Aug 2026 11:05:12 +0300 Subject: [PATCH] Differ --- .gitignore | 1 + apps/web-react/app/components/MobileNav.tsx | 20 +- apps/web-react/app/components/SideNav.tsx | 8 +- .../app/components/StreamListItems.tsx | 12 +- .../app/components/layout/TopNavMenus.tsx | 4 +- .../web-react/app/components/layout/index.tsx | 8 +- .../app/db/seed/core/frozen-clock.ts | 66 ++ .../components/DamageComboBar.tsx | 10 +- .../components/ParamComparisonDialog.tsx | 6 +- .../params/components/WeaponParamsTable.tsx | 24 +- .../params/components/WeaponPatchHistory.tsx | 8 +- apps/web-react/e2e/global-setup.ts | 82 +- .../e2e/pages/analyzer/weapon-params-page.ts | 12 +- .../pages/comp-analyzer/comp-analyzer-page.ts | 10 +- apps/web-react/e2e/pages/layout/mobile-nav.ts | 12 +- apps/web-react/e2e/pages/layout/side-nav.ts | 25 +- .../e2e/pages/layout/top-nav-menus.ts | 2 +- apps/web-react/output/smoke1/census.json | 1008 +++++++++++++++++ apps/web-react/scripts/ensure-e2e-build.ts | 100 ++ apps/web-react/scripts/route-census.ts | 503 ++++++++ apps/web-react/scripts/seed.ts | 12 +- package.json | 3 +- pnpm-lock.yaml | 34 + tooling/differ/README.md | 71 +- tooling/differ/package.json | 18 + tooling/differ/src/cli.ts | 133 +++ tooling/differ/src/differ.ts | 628 ++++++++++ tooling/differ/src/prepare.ts | 122 ++ tooling/differ/src/servers.ts | 122 ++ tooling/differ/src/types.ts | 95 ++ 30 files changed, 3023 insertions(+), 136 deletions(-) create mode 100644 apps/web-react/app/db/seed/core/frozen-clock.ts create mode 100644 apps/web-react/output/smoke1/census.json create mode 100644 apps/web-react/scripts/ensure-e2e-build.ts create mode 100644 apps/web-react/scripts/route-census.ts create mode 100644 tooling/differ/package.json create mode 100644 tooling/differ/src/cli.ts create mode 100644 tooling/differ/src/differ.ts create mode 100644 tooling/differ/src/prepare.ts create mode 100644 tooling/differ/src/servers.ts create mode 100644 tooling/differ/src/types.ts diff --git a/.gitignore b/.gitignore index 07169fb9d..56aa5f2b5 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ dump apps/web-react/scripts/output/* !apps/web-react/scripts/output/.gitkeep +tooling/differ/output/ apps/web-react/scripts/dicts/**/* apps/web-react/scripts/badge diff --git a/apps/web-react/app/components/MobileNav.tsx b/apps/web-react/app/components/MobileNav.tsx index b3a8b4123..59991999e 100644 --- a/apps/web-react/app/components/MobileNav.tsx +++ b/apps/web-react/app/components/MobileNav.tsx @@ -309,12 +309,13 @@ function MobilePanel({ - +
{icon}

{title}

{streams.length === 0 ? ( -
+
{t("front:sideNav.noStreams")}
) : null} @@ -483,7 +485,7 @@ function FriendsPanel({ )) ) : ( -
+
{user ? t("front:sideNav.friends.noFriends") : t("front:sideNav.friends.notLoggedIn")} @@ -573,7 +575,12 @@ function YouPanel({ onClick={onClose} > - {user.username} + + {user.username} + - + @@ -652,6 +659,7 @@ function GhostTabBar({ ); diff --git a/apps/web-react/app/components/layout/TopNavMenus.tsx b/apps/web-react/app/components/layout/TopNavMenus.tsx index dd322460b..b2d6ddf14 100644 --- a/apps/web-react/app/components/layout/TopNavMenus.tsx +++ b/apps/web-react/app/components/layout/TopNavMenus.tsx @@ -106,7 +106,7 @@ function DevMenu() { isOpen={isOpen} onOpenChange={setIsOpen} > -
+
{DEV_IMPERSONATE_ITEMS.map((item) => (
-
+
{visibleItems.map((item) => ( )) ) : ( -
{t("front:sideNav.noEvents")}
+
+ {t("front:sideNav.noEvents")} +
)} ) ) : ( -
+
{user ? t("front:sideNav.friends.noFriends") : t("front:sideNav.friends.notLoggedIn")} @@ -380,7 +382,7 @@ export function Layout({ }>{t("front:sideNav.streams")} {streams.length === 0 ? ( -
+
{t("front:sideNav.noStreams")}
) : null} diff --git a/apps/web-react/app/db/seed/core/frozen-clock.ts b/apps/web-react/app/db/seed/core/frozen-clock.ts new file mode 100644 index 000000000..987932b77 --- /dev/null +++ b/apps/web-react/app/db/seed/core/frozen-clock.ts @@ -0,0 +1,66 @@ +const RealDate = Date; + +/** + * Parses the `SEED_NOW` env var (epoch milliseconds or an ISO 8601 date) into + * epoch milliseconds, or returns null when it is not set. + */ +function parseSeedNow(): number | null { + const raw = process.env.SEED_NOW; + if (!raw) return null; + + const parsed = /^\d+$/.test(raw) ? Number(raw) : RealDate.parse(raw); + if (Number.isNaN(parsed)) { + throw new Error( + `SEED_NOW must be epoch milliseconds or an ISO 8601 date, got "${raw}"`, + ); + } + + return parsed; +} + +/** + * Freezes the process-global clock at `SEED_NOW` so that everything the seed + * touches — factories, `databaseTimestampNow`, date-fns arithmetic — observes + * the same instant, making seeded timestamps a pure function of `SEED_NOW`. + * No-op when `SEED_NOW` is not set. Timers are left real; only `Date`'s idea + * of "now" is pinned. + * + * Meant for dedicated seed processes (`scripts/seed.ts`, the differ's seeding + * step), never for a serving app process. + */ +export function installSeedClock() { + const seedNow = parseSeedNow(); + if (seedNow === null) return; + const fixedNow: number = seedNow; + + type DateArgs = + | [] + | [value: number | string | Date] + | [ + year: number, + monthIndex: number, + day?: number, + hours?: number, + minutes?: number, + seconds?: number, + ms?: number, + ]; + + class FrozenDate extends RealDate { + constructor(...args: DateArgs) { + if (args.length === 0) { + super(fixedNow); + } else if (args.length === 1) { + super(args[0]); + } else { + super(...(args as [number, number])); + } + } + + static now() { + return fixedNow; + } + } + + globalThis.Date = FrozenDate as DateConstructor; +} diff --git a/apps/web-react/app/features/comp-analyzer/components/DamageComboBar.tsx b/apps/web-react/app/features/comp-analyzer/components/DamageComboBar.tsx index 275206b0f..d92f83952 100644 --- a/apps/web-react/app/features/comp-analyzer/components/DamageComboBar.tsx +++ b/apps/web-react/app/features/comp-analyzer/components/DamageComboBar.tsx @@ -139,7 +139,7 @@ function SegmentBar({ subWeaponId={params.subWeaponId} specialWeaponId={params.specialWeaponId} /> - + {segment.damageValue} {segment.count > 1 ? ( ×{segment.count} @@ -148,6 +148,7 @@ function SegmentBar({
-
{inkTimeFrames}f
+
+ {inkTimeFrames}f +
); } @@ -243,6 +246,7 @@ function FilteredItem({ filterKey, onRestore }: FilteredItemProps) { return ( {isCollapsed ? null : ( -
+