From d2a01931f542db57d9e2c378b4775734b960fba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bag=C3=B3=20=C3=81d=C3=A1m=20Adri=C3=A1n?= <151737980+CsakEgyBago@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:10:05 +0200 Subject: [PATCH 1/6] fix(settings): guard JSON.parse in SettingsContext against corrupted localStorage If the value stored under the "settings" key in localStorage is invalid JSON (browser extension overwrite, storage corruption, manual DevTools edit, power-loss mid-write), the previous unguarded JSON.parse threw a SyntaxError during the SettingsProvider useState initializer. Because SettingsProvider sits at the root of the component tree, this crashed the entire app with a blank screen and no recovery path for the user. Fixed by wrapping the JSON.parse call in a try/catch that falls back to null on failure, matching the already-correct pattern used in useDebugSettings.ts. The parsed value is cast as Partial so TypeScript can still type-check the individual property accesses and their ?? default fallbacks. Co-Authored-By: Claude Sonnet 4.6 --- client/src/pages/settings/SettingsContext.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/src/pages/settings/SettingsContext.tsx b/client/src/pages/settings/SettingsContext.tsx index bca28f0..68dc270 100644 --- a/client/src/pages/settings/SettingsContext.tsx +++ b/client/src/pages/settings/SettingsContext.tsx @@ -37,7 +37,12 @@ export function useSettings() { export function SettingsProvider({ children }: { children: React.ReactNode }) { const [settings, setSettings] = useState(() => { const saved = localStorage.getItem("settings"); - const parsed = saved ? JSON.parse(saved) : null; + let parsed: Partial | null = null; + try { + parsed = saved ? (JSON.parse(saved) as Partial) : null; + } catch { + parsed = null; + } return { useAnimations: parsed?.useAnimations ?? true, useLiquidGlass: parsed?.useLiquidGlass ?? true, From fbc4d0ea6080e218bf0567f1bde9afe4ee088bf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bag=C3=B3=20=C3=81d=C3=A1m=20Adri=C3=A1n?= <151737980+CsakEgyBago@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:19:59 +0200 Subject: [PATCH 2/6] fix(releases): set GitHub staleTime to 10 min to respect API rate limit The constant GITHUB_STALE_TIME was set to 0 while the comment directly above it read "staleTime of 10 minutes keeps well within the rate limit." The contradiction meant React Query treated cached release data as always stale, firing a new GitHub API request on every single mount of the Releases page plus every 5-minute polling interval. GitHub's unauthenticated API is capped at 60 requests per hour per IP; heavy page-switching or multiple users sharing a NAT IP could exhaust that budget quickly, causing the page to show a fetch-error banner. Changed to: const GITHUB_STALE_TIME = 10 * 60 * 1000 (10 minutes) Updated the inline comment to match the value exactly. Co-Authored-By: Claude Sonnet 4.6 --- client/src/pages/releases/useReleases.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/pages/releases/useReleases.ts b/client/src/pages/releases/useReleases.ts index d2d39c5..6de5c05 100644 --- a/client/src/pages/releases/useReleases.ts +++ b/client/src/pages/releases/useReleases.ts @@ -16,7 +16,7 @@ const GITHUB_API_URL = `https://api.github.com/repos/${GITHUB_REPO}/releases`; * If the app is ever made private, move this fetch to a backend proxy endpoint * so the GitHub token never ships in the client bundle. */ -const GITHUB_STALE_TIME = 0; // always treat as stale so refetches go through +const GITHUB_STALE_TIME = 10 * 60 * 1000; // 10 minutes — matches comment above const GITHUB_GC_TIME = 30 * 60 * 1000; // 30 minutes const GITHUB_REFETCH_INTERVAL = 5 * 60 * 1000; // poll every 5 minutes From 472912cd5e999d3ed97872fdceef0ed1ec8aa7e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bag=C3=B3=20=C3=81d=C3=A1m=20Adri=C3=A1n?= <151737980+CsakEgyBago@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:39:33 +0200 Subject: [PATCH 3/6] fix(admin): mark AdminUserDTO.username as optional to match backend reality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AdminUserDTO interface declared `username: string` as a required field, but the accompanying comment (and the optional-chaining already used when filtering users in useAdminPageLogic.ts) made clear the backend never actually returns it. This was a type lie: TypeScript believed username was always present, so it would never warn if new code accessed it without a fallback — silently receiving undefined at runtime. Changed `username: string` to `username?: string` so the type honestly reflects what the API sends. All existing call sites already have `|| email` guards or optional-chaining, so no runtime behaviour changes. Future code that tries to use .username directly without a guard will now get a compile-time error instead of a silent runtime bug. Co-Authored-By: Claude Sonnet 4.6 --- client/src/hooks/useAdmin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/hooks/useAdmin.ts b/client/src/hooks/useAdmin.ts index 5ee93af..77e7fd9 100644 --- a/client/src/hooks/useAdmin.ts +++ b/client/src/hooks/useAdmin.ts @@ -21,7 +21,7 @@ export interface AdminUserDTO { id: number; email: string; /** Not yet returned by the backend; components fall back to email. */ - username: string; + username?: string; role: string; is_banned: boolean; /** Formatted datetime string of when the ban expires; empty string = not banned. */ From 84542a6acaf2c51d7011059785b3b9cfbc0eb892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bag=C3=B3=20=C3=81d=C3=A1m=20Adri=C3=A1n?= <151737980+CsakEgyBago@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:53:54 +0200 Subject: [PATCH 4/6] fix(news): add empty-state message when no posts are visible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The news page had no empty-state UI. When a user unchecked all four category filters the virtualiser rendered zero items and the content area went completely blank — no message, no hint, just an unexplained void. The same blank behaviour hit any future case where posts exist but nothing matched the current filter/search state. Added a conditional

block below the virtualised

    that renders only when loading is done and visiblePosts.length is 0. It shows two different messages: • "Select at least one category to see posts." — when selectedCategories is empty • "No posts found." — when categories are selected but nothing matches (search query, or genuinely no posts in those categories) Both strings are translated in locales/en/news.json and locales/hu/news.json under the new "noCategories" key; "noResults" already existed and is reused for the second case. Co-Authored-By: Claude Sonnet 4.6 --- client/src/locales/en/news.json | 1 + client/src/locales/hu/news.json | 1 + client/src/pages/news/NewsPage.tsx | 7 +++++++ 3 files changed, 9 insertions(+) diff --git a/client/src/locales/en/news.json b/client/src/locales/en/news.json index aa7eb56..836557d 100644 --- a/client/src/locales/en/news.json +++ b/client/src/locales/en/news.json @@ -50,6 +50,7 @@ "deleteFailed": "Failed to delete post." }, "noResults": "No posts found.", + "noCategories": "Select at least one category to see posts.", "edit": "Edit", "delete": { "confirm": "Confirm Deletion", diff --git a/client/src/locales/hu/news.json b/client/src/locales/hu/news.json index e1263b1..33ccc4b 100644 --- a/client/src/locales/hu/news.json +++ b/client/src/locales/hu/news.json @@ -50,6 +50,7 @@ "deleteFailed": "Nem sikerült törölni a bejegyzést." }, "noResults": "Nem található bejegyzés.", + "noCategories": "Válassz legalább egy kategóriát a bejegyzések megtekintéséhez.", "edit": "Szerkesztés", "delete": { "confirm": "Törlés megerősítése", diff --git a/client/src/pages/news/NewsPage.tsx b/client/src/pages/news/NewsPage.tsx index c3d5eea..4443b6f 100644 --- a/client/src/pages/news/NewsPage.tsx +++ b/client/src/pages/news/NewsPage.tsx @@ -212,6 +212,13 @@ export function NewsPage() { ); })}
+ + {/* Empty state — shown when loading is done but nothing is visible */} + {!postsLoading && visiblePosts.length === 0 && ( +

+ {selectedCategories.length === 0 ? t("noCategories") : t("noResults")} +

+ )} ); From 3aed33a80f0bb30aaa2fb9db96826808ca449dc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bag=C3=B3=20=C3=81d=C3=A1m=20Adri=C3=A1n?= <151737980+CsakEgyBago@users.noreply.github.com> Date: Wed, 15 Apr 2026 20:13:22 +0200 Subject: [PATCH 5/6] fix(auth): replace Number(bigint userId) with String() for localStorage keys userId in AuthContext is stored as bigint. Five places converted it to Number before using it in localStorage key strings (selected_profile_) or as a React Query key. Number(bigint) silently loses precision for IDs larger than 2^53-1 (~9 quadrillion), which would produce the wrong key and potentially share or delete the wrong profile entry for a different user. Changes: - Navbar.tsx, AccountMenu.tsx, ProfileSelectorPage.tsx: template literals now use String(userId) instead of Number(userId) for the localStorage key. - ProfilesContext.tsx: profileStorageKey helper updated to accept string|number; all localStorage get/set/remove calls now pass String(userId) directly (bypassing numUserId which is only needed for API calls). The selectProfile useCallback dependency array cleaned up to [profiles, userId] after numUserId was removed from the callback body. - useItems.ts: Number(userId) kept for queryKeys.profiles.byUserId() since that API requires a number; a comment was added explaining the precision trade-off and when to revisit it. For current sequential DB IDs, String(123n) and Number(123n) produce identical key strings ("123"), so existing stored profiles are not affected by this change. Co-Authored-By: Claude Sonnet 4.6 --- client/src/components/nav/AccountMenu.tsx | 2 +- client/src/components/nav/Navbar.tsx | 2 +- .../profile-selector/ProfileSelectorPage.tsx | 4 ++-- .../pages/profile-selector/ProfilesContext.tsx | 16 ++++++++-------- client/src/pages/webstore/useItems.ts | 5 ++++- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/client/src/components/nav/AccountMenu.tsx b/client/src/components/nav/AccountMenu.tsx index 0eff0bd..ef06fc8 100644 --- a/client/src/components/nav/AccountMenu.tsx +++ b/client/src/components/nav/AccountMenu.tsx @@ -37,7 +37,7 @@ export default function AccountMenu() { const handleLogout = async () => { try { - if (userId) localStorage.removeItem(`selected_profile_${Number(userId)}`); + if (userId) localStorage.removeItem(`selected_profile_${String(userId)}`); await logoutMutation.mutateAsync(); setIsLoggedIn(false); setUserId(null); diff --git a/client/src/components/nav/Navbar.tsx b/client/src/components/nav/Navbar.tsx index 86a2142..a7d91d9 100644 --- a/client/src/components/nav/Navbar.tsx +++ b/client/src/components/nav/Navbar.tsx @@ -46,7 +46,7 @@ const Navbar = () => { const handleLogout = async () => { try { - if (userId) localStorage.removeItem(`selected_profile_${Number(userId)}`); + if (userId) localStorage.removeItem(`selected_profile_${String(userId)}`); await logoutMutation.mutateAsync(); setIsLoggedIn(false); setUserId(null); diff --git a/client/src/pages/profile-selector/ProfileSelectorPage.tsx b/client/src/pages/profile-selector/ProfileSelectorPage.tsx index 4b1f917..3c7d174 100644 --- a/client/src/pages/profile-selector/ProfileSelectorPage.tsx +++ b/client/src/pages/profile-selector/ProfileSelectorPage.tsx @@ -97,13 +97,13 @@ export function ProfileSelectorPage() { useEffect(() => { if (location.state?.change) return; if (!userId) return; - const stored = localStorage.getItem(`selected_profile_${Number(userId)}`); + const stored = localStorage.getItem(`selected_profile_${String(userId)}`); if (stored) navigate("/app/releases", { replace: true }); }, [userId, location.state, navigate]); const handleLogout = async () => { try { - if (userId) localStorage.removeItem(`selected_profile_${Number(userId)}`); + if (userId) localStorage.removeItem(`selected_profile_${String(userId)}`); await logoutMutation.mutateAsync(); setIsLoggedIn(false); setUserId(null); diff --git a/client/src/pages/profile-selector/ProfilesContext.tsx b/client/src/pages/profile-selector/ProfilesContext.tsx index 3209fb9..b053bd9 100644 --- a/client/src/pages/profile-selector/ProfilesContext.tsx +++ b/client/src/pages/profile-selector/ProfilesContext.tsx @@ -20,7 +20,7 @@ const ProfileContext = createContext(defaultProfileContext); export { ProfileContext }; -function profileStorageKey(userId: number) { +function profileStorageKey(userId: number | string) { return `selected_profile_${userId}`; } @@ -38,13 +38,13 @@ export function ProfileProvider({ children }: { children: React.ReactNode }) { // fetched profiles list. We validate that the stored ID still exists — // the user may have deleted the profile since the last session. useEffect(() => { - if (!numUserId || fetchedProfiles.length === 0) return; - const stored = localStorage.getItem(profileStorageKey(numUserId)); + if (!userId || !numUserId || fetchedProfiles.length === 0) return; + const stored = localStorage.getItem(profileStorageKey(String(userId))); if (!stored) return; const storedId = parseInt(stored, 10); const stillExists = fetchedProfiles.some((p: ProfileResponse) => p.id === storedId); if (stillExists) setSelectedProfileId(storedId); - }, [numUserId, fetchedProfiles]); + }, [userId, numUserId, fetchedProfiles]); const profiles = useMemo( () => @@ -95,15 +95,15 @@ export function ProfileProvider({ children }: { children: React.ReactNode }) { const found = profiles.find((p) => p.name === name) || null; const id = found?.id ?? null; setSelectedProfileId(id); - if (numUserId) { + if (userId) { if (id !== null) { - localStorage.setItem(profileStorageKey(numUserId), String(id)); + localStorage.setItem(profileStorageKey(String(userId)), String(id)); } else { - localStorage.removeItem(profileStorageKey(numUserId)); + localStorage.removeItem(profileStorageKey(String(userId))); } } }, - [profiles, numUserId], + [profiles, userId], ); return ( diff --git a/client/src/pages/webstore/useItems.ts b/client/src/pages/webstore/useItems.ts index 2d4d196..4c43234 100644 --- a/client/src/pages/webstore/useItems.ts +++ b/client/src/pages/webstore/useItems.ts @@ -128,7 +128,10 @@ export function useItems() { queryClient.invalidateQueries({ queryKey: queryKeys.purchases.byProfileId(profileId ?? 0), }); - // Refetch profile so coin balance updates after purchase + // Refetch profile so coin balance updates after purchase. + // Note: Number(bigint) loses precision for IDs > 2^53-1; acceptable here + // because queryKeys.profiles.byUserId requires a number and sequential DB + // IDs never reach that magnitude. If the ID strategy changes, update this. if (userId !== null) { queryClient.invalidateQueries({ queryKey: queryKeys.profiles.byUserId(Number(userId)), From ab9402fe52b090897e85992bd47f4efb5775b378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bag=C3=B3=20=C3=81d=C3=A1m=20Adri=C3=A1n?= <151737980+CsakEgyBago@users.noreply.github.com> Date: Wed, 15 Apr 2026 20:17:17 +0200 Subject: [PATCH 6/6] chore: add Errors.md bug report and apply Prettier formatting fixes Errors.md documents the five client-side bugs found during the audit session, their explanations, severity ratings, and fix prompts. The three admin files (AdminPage.tsx, AdminPageContent.tsx, UserDetail.tsx) were auto-formatted by Prettier during the test-client run that followed the username type fix; those formatting changes are captured here rather than mixed into the type-fix commit. Co-Authored-By: Claude Sonnet 4.6 --- Errors.md | 220 ++++++++++++++++++++ client/src/pages/admin/AdminPageContent.tsx | 2 +- 2 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 Errors.md diff --git a/Errors.md b/Errors.md new file mode 100644 index 0000000..db746d9 --- /dev/null +++ b/Errors.md @@ -0,0 +1,220 @@ +# Client-Side Bug & Glitch Report + +> Scope: `client/src/` only — server code is excluded. +> All issues below are fixable by modifying client code alone. +> Files are linked as relative paths from the repo root. + +--- + +## Bug #1 — App-breaking crash if `localStorage` settings are corrupted + +**File:** [client/src/pages/settings/SettingsContext.tsx](client/src/pages/settings/SettingsContext.tsx#L40) +**Severity:** HIGH + +### What it is +`SettingsContext` reads the saved settings on startup like this: + +```ts +const parsed = saved ? JSON.parse(saved) : null; +``` + +There is no `try/catch` around `JSON.parse`. If the value stored under the `"settings"` key in `localStorage` is invalid JSON — corrupted storage, a browser extension writing to it, or someone manually editing it in DevTools — this line throws a `SyntaxError`. Because `SettingsProvider` is at the very root of the component tree (inside `RootLayout`), the entire app fails to mount and the user sees a blank crash screen. + +Compare this to `useDebugSettings.ts`, which *does* have the protection: +```ts +try { + return { ...DEFAULTS, ...JSON.parse(...) }; +} catch { + return { ...DEFAULTS }; +} +``` +`SettingsContext` is missing that same safety net. + +### When it could happen +- Rarely, but once it does the user is completely stuck with no way to recover without opening DevTools and clearing storage. +- A browser extension that writes to `localStorage`, a corrupted browser profile, or power-loss during a write could all trigger it. + +### Fix prompt +``` +In client/src/pages/settings/SettingsContext.tsx, the useState initializer at line ~38 +calls JSON.parse without a try/catch. If localStorage contains invalid JSON the entire +app crashes. Wrap the JSON.parse in a try/catch so that on failure it falls back to the +default values. Model the fix after the already-correct pattern in +client/src/hooks/useDebugSettings.ts (its getDebugSettings function). +Do not change any other logic. +``` + +--- + +## Bug #2 — GitHub API rate-limit risk on the Releases page + +**File:** [client/src/pages/releases/useReleases.ts](client/src/pages/releases/useReleases.ts#L14-L19) +**Severity:** MEDIUM + +### What it is +There is a contradictory comment and constant at the top of the file: + +```ts +/** + * staleTime of 10 minutes means React Query only refetches when data is older + * than 10 minutes, keeping well within the rate limit in normal usage. + */ +const GITHUB_STALE_TIME = 0; // always treat as stale so refetches go through +``` + +The comment says "10 minutes of staleTime." The actual value is `0`. Because `staleTime: 0` means the cache is always considered stale, React Query will fire a new GitHub API request every single time the user navigates to the Releases page (even from the cache, it re-fetches in the background). There is also a `refetchInterval: 5 * 60 * 1000` (every 5 minutes) and `refetchOnMount: true`. + +The unauthenticated GitHub API allows only 60 requests per hour per IP. If multiple users share a NAT IP (an office, school, shared hosting), or if one user navigates back and forth several times, the limit gets burned quickly. Once it hits, GitHub returns a 403/429 and the Releases page shows an error banner. + +### When it could happen +- Every time the Releases page mounts, one request fires immediately (mount + stale cache). +- Every 5 minutes while the page stays open, another fires. +- More than one user behind the same IP compounds this. + +### Fix prompt +``` +In client/src/pages/releases/useReleases.ts, the constant GITHUB_STALE_TIME is set to 0 +but the comment above it says "10 minutes." This causes React Query to refetch GitHub +releases on every single page mount, which burns through the 60-requests/hour +unauthenticated GitHub API rate limit quickly. + +Change the constant to: const GITHUB_STALE_TIME = 10 * 60 * 1000; + +Also update the comment to accurately reflect the value. Do not change anything else. +``` + +--- + +## Bug #3 — `AdminUserDTO.username` is typed as a required `string` but the backend never sends it + +**File:** [client/src/hooks/useAdmin.ts](client/src/hooks/useAdmin.ts#L20-L30) +**Severity:** LOW-MEDIUM (type safety / future crash risk) + +### What it is +The type definition says: + +```ts +export interface AdminUserDTO { + username: string; // ← typed as always present + ... +} +``` + +But the comment directly above it reads: +> "NOTE: `username` is not returned by the backend yet — components fall back to `email` when it is absent." + +At runtime `username` is `undefined`. Every component that renders it already has a `|| fallback` guard (`user.username || user.email`), and the search filter uses optional chaining (`u.username?.toLowerCase()`), so nothing crashes today. But the TypeScript type says it is a non-optional `string`, so TypeScript will never warn if someone adds new code that accesses `.username` directly without a fallback — and that code would silently receive `undefined` at runtime. + +### When it could happen +- A developer adds a new feature in the admin panel that reads `user.username` directly, trusting the type. TypeScript will not flag it. The feature ships and crashes at runtime in production. + +### Fix prompt +``` +In client/src/hooks/useAdmin.ts, the AdminUserDTO interface declares `username: string` +as a required field, but the backend does not return it (acknowledged in the comment on +line ~23). This is a type lie — at runtime the field is always undefined. + +Change the declaration to: `username?: string;` + +This makes the type honest and ensures TypeScript will warn whenever username is +accessed without a fallback. Do not change any other code. +``` + +--- + +## Bug #4 — News page goes silently blank when all category filters are deselected + +**File:** [client/src/pages/news/NewsPage.tsx](client/src/pages/news/NewsPage.tsx) + [client/src/pages/news/useNewsPosts.ts](client/src/pages/news/useNewsPosts.ts#L158-L160) +**Severity:** LOW (UX glitch) + +### What it is +In `useNewsPosts.ts`, when the list of selected categories is empty the hook immediately returns an empty array: + +```ts +const filteredPosts = useMemo(() => { + if (selectedCategories.length === 0) return []; + ... +``` + +This is technically correct — showing no posts when no categories are selected makes sense. However `NewsPage.tsx` has no empty-state UI for this scenario. When a user clicks through and deselects all four category filters: + +- The skeleton loader is hidden (it only shows when `postsLoading` is true, which is false once the initial fetch finishes). +- The virtualised list renders nothing. +- The post count badge reads "0 posts." +- The content area is just blank. + +There is no message like "Select at least one category" or "No posts to show." The user sees an unexplained empty void and may think something broke. + +### Fix prompt +``` +In client/src/pages/news/NewsPage.tsx, when all category filters are deselected the +post list is empty but there is no empty-state message shown — just a blank area. + +After the virtualised
    element (around line 117), add a conditional block that +renders when `!postsLoading && visiblePosts.length === 0`. It should show a small, +centered explanatory message (e.g. "No posts match your current filters."). +Style it consistently with the rest of the page using `subtextColor`. +Do not change any other logic or layout. +``` + +--- + +## Bug #5 — `BigInt` user IDs silently lose precision when converted to `Number` + +**Files (all affected):** +- [client/src/components/nav/Navbar.tsx](client/src/components/nav/Navbar.tsx#L49) +- [client/src/components/nav/AccountMenu.tsx](client/src/components/nav/AccountMenu.tsx#L40) +- [client/src/pages/profile-selector/ProfileSelectorPage.tsx](client/src/pages/profile-selector/ProfileSelectorPage.tsx#L100) +- [client/src/pages/profile-selector/ProfilesContext.tsx](client/src/pages/profile-selector/ProfilesContext.tsx#L31) +- [client/src/pages/webstore/useItems.ts](client/src/pages/webstore/useItems.ts#L134) + +**Severity:** LOW (latent / future-proof risk) + +### What it is +`userId` in `AuthContext` is stored as `bigint`. In several places it is converted to `Number` before being used as a `localStorage` key or a query parameter: + +```ts +localStorage.removeItem(`selected_profile_${Number(userId)}`); +``` + +JavaScript's `Number` type can only represent integers up to `2^53 - 1` (about 9 quadrillion) without losing precision. If a user ID ever exceeds that — unlikely with sequential database IDs today, but possible if the ID strategy changes — the conversion silently produces the wrong number. This means: + +- `selected_profile_${Number(userId)}` could produce the same key for two different users. +- On logout, the wrong localStorage entry is cleared. +- The webstore query key `queryKeys.profiles.byUserId(Number(userId))` could return a cached result from a different user. + +All five occurrences share the same pattern, so fixing one requires fixing all. + +### Fix prompt +``` +In the client, `userId` from AuthContext is typed as `bigint` but is cast to Number +in five places before being used as a localStorage key or query key. Number(bigint) +silently loses precision for values above 2^53-1. + +For the localStorage key uses: + - client/src/components/nav/Navbar.tsx:49 + - client/src/components/nav/AccountMenu.tsx:40 + - client/src/pages/profile-selector/ProfileSelectorPage.tsx:100 and 106 + - client/src/pages/profile-selector/ProfilesContext.tsx:31 + +Replace Number(userId) with String(userId) everywhere it's used as a localStorage key. + +For the query key use in client/src/pages/webstore/useItems.ts:134, check how +queryKeys.profiles.byUserId is typed and whether it can accept number | string; +if it already accepts a number, converting via Number is fine to keep for now but +add a comment explaining the precision risk. + +Do not change anything else. +``` + +--- + +## Summary Table + +| # | File | Issue | Severity | Fixable? | +|---|------|-------|----------|---------| +| 1 | `SettingsContext.tsx:40` | Unguarded `JSON.parse` crashes entire app | **HIGH** | Yes | +| 2 | `useReleases.ts:19` | `staleTime: 0` burns GitHub rate limit; comment says 10 min | **MEDIUM** | Yes | +| 3 | `useAdmin.ts:24` | `username` typed as `string`, backend sends `undefined` | **LOW-MED** | Yes | +| 4 | `NewsPage.tsx` | No empty-state UI when all category filters deselected | **LOW** | Yes | +| 5 | Multiple files | `Number(bigint userId)` precision loss for large IDs | **LOW** | Yes | diff --git a/client/src/pages/admin/AdminPageContent.tsx b/client/src/pages/admin/AdminPageContent.tsx index 47e1bba..a5be868 100644 --- a/client/src/pages/admin/AdminPageContent.tsx +++ b/client/src/pages/admin/AdminPageContent.tsx @@ -25,7 +25,7 @@ export function AdminPageContent({ animReady = true }: { animReady?: boolean }) return ( <>
    {useAnimations ? (