From 0bebbb8cc16f3e177a3d23156ad0f3c2bc387383 Mon Sep 17 00:00:00 2001 From: LunaUrsa <1836049+LunaUrsa@users.noreply.github.com> Date: Sun, 8 Nov 2026 13:57:21 -0600 Subject: [PATCH 1/2] Keep one QueryClient and share the drugs query (WEB-007) _app.tsx created a new QueryClient on every render, so the cache was empty after each navigation and the 1 MB+ drugs.json was downloaded again. Keep one client in useState, and drop the second QueryClient and LocalizationProvider that Factsheets.tsx wrapped itself in. The factsheets table and the factsheet pages each had their own query under the same "table-data" key but with different data shapes, which would clash in a shared cache. Both now use one useDrugs() hook (src/utils/drugs.ts) with a 30 minute staleTime; a failed download now throws. Removing the table's rowCount also fixes the pager showing "1-10 of 555" after filtering (WEB-029). The tracker gains WEB-027 to WEB-030 for the issues found while deploying and testing. Co-Authored-By: Claude Opus 5.5 --- REVIEW-ISSUES.md | 82 +++++++++++++++++++++++++++++-- src/components/Factsheets.tsx | 53 ++------------------ src/pages/_app.tsx | 7 ++- src/pages/factsheets/[drug].tsx | 25 +--------- src/utils/__tests__/drugs.test.ts | 31 ++++++++++++ src/utils/drugs.ts | 27 ++++++++++ 6 files changed, 147 insertions(+), 78 deletions(-) create mode 100644 src/utils/__tests__/drugs.test.ts create mode 100644 src/utils/drugs.ts diff --git a/REVIEW-ISSUES.md b/REVIEW-ISSUES.md index 5cddec9..1245f9b 100644 --- a/REVIEW-ISSUES.md +++ b/REVIEW-ISSUES.md @@ -55,17 +55,23 @@ Priorities: **P0** exploitable or critical, fix now · **P1** real bug or exposu | WEB-024 | P3 | perf | Five UI/CSS systems and four icon sets ship together | | WEB-025 | P3 | lint | Two `react-hooks/set-state-in-effect` warnings | | WEB-026 | P3 | deps | Dev-only advisories remain (brace-expansion, js-yaml, zod) | +| WEB-027 | P2 | deploy | Both Drone servers build every push; the other server's build hangs as pending | +| WEB-028 | P2 | deploy | The shared deployer gives up after ~60 s when another deploy is running | +| WEB-029 | P3 | bug | Factsheets pager shows the full count ("1-10 of 555") after filtering | +| WEB-030 | P2 | perf | Expanding a factsheet row keeps the browser busy for tens of seconds | ### Overlap groups (same files; take together or in sequence) - **Lockfile:** WEB-001 first, then anything else that changes `package.json` or `package-lock.json` (WEB-016, WEB-019, WEB-024, WEB-026). - **Template scripts:** WEB-008, WEB-009, WEB-022 (`_document.tsx`, `_app.tsx`, `public/assets/vendor`). -- **Drug data:** WEB-007, WEB-010 (`_app.tsx`, `Factsheets.tsx`, `factsheets/[drug].tsx`). +- **Drug data:** WEB-007, WEB-010, WEB-029, WEB-030 (`_app.tsx`, `Factsheets.tsx`, + `factsheets/[drug].tsx`, `DrugInfo.tsx`). - **Auth:** WEB-005, WEB-006, WEB-025 (`keycloak.ts`, `ProfileButton.tsx`, `appeal.tsx`). - **Homepage:** WEB-003, WEB-020, WEB-021 (`pages/index.tsx`, `Counter.tsx`, `Ghost.tsx`). - **Workflows:** WEB-012, WEB-013, WEB-015, WEB-017 (`.github/workflows/`). -- **Docker/Drone:** WEB-014, WEB-016, WEB-018 (`.drone.yml`, `Dockerfile`, `.dockerignore`). +- **Docker/Drone:** WEB-014, WEB-016, WEB-018, WEB-027, WEB-028 (`.drone.yml`, `Dockerfile`, + `.dockerignore`, Drone and deployer settings on the servers). --- @@ -207,7 +213,11 @@ and pass `code_verifier` to the token endpoint. Quote and validate the avatar UR ### WEB-007 — `QueryClient` recreated on every render - Priority: P1 · Area: perf -- Status: open +- Status: fixed — fix/WEB-007-query-client +- Notes: `_app.tsx` keeps one `QueryClient` in `useState`. The table and the factsheet pages + share one `useDrugs()` hook (`src/utils/drugs.ts`, key `["drugs"]`, 30 min `staleTime`), so + `drugs.json` downloads once per page load. Their old queries used the same key with different + data shapes, which would have clashed in a shared cache. Also fixes WEB-029. - Files: `src/pages/_app.tsx:48`, `src/components/Factsheets.tsx:418` **Problem.** `` builds a new client each time @@ -313,6 +323,8 @@ does), add `permissions: contents: read` at the top of every workflow, and grant - Priority: P2 · Area: deploy - Status: open +- Notes: `docker-compose.website-prod.yml` lives on the UAT server; `DISCORD_GUILD_ID` was + added to it for #258. How the Drone step's checkout reaches it is still undocumented. - Files: `.drone.yml:72-125` **Problem.** @@ -482,6 +494,8 @@ in `eslint.config.mjs` as known debt). - Priority: P3 · Area: deps - Status: open +- Notes: on 2026-11-08 (`uat` at `7963e49`) `npm audit` reports 2 high, 27 moderate and 3 low, + from `brace-expansion`, `zod`, `minimatch`, `glob` and `deepmerge`; `--omit=dev` reports none. - Files: `package-lock.json` **Problem.** After WEB-001, full `npm audit` still reports 3 high and 19 moderate advisories, @@ -492,3 +506,65 @@ churn for the security PR, so it was left out. **Fix.** In a separate PR, run `npm audit fix` (no `--force`), then lint, test and build. Belongs to the lockfile overlap group. + +### WEB-027 — Both Drone servers build every push + +- Priority: P2 · Area: deploy +- Status: open +- Files: `.drone.yml`; repository settings on drone.tripsit.io and drone.tripsit.me + +**Problem.** The repository is active on two Drone servers. drone.tripsit.io has the UAT +runner (`env: uat`) and drone.tripsit.me has the production runner (`env: prod`). Both +receive every push and both start a build. The one without a matching runner keeps it +pending until the next push skips it (for example .me #115 and #117 for `uat`, .io #35 and +#39 for `main`). Both report under the same GitHub status, `continuous-integration/drone/push`, +so whichever server reports last sets the commit's status. If Drone kills a pending build +(.me #109 was "killed"), the commit can show as failed even though its deploy worked. + +**Fix.** Keep each branch's pipeline on one server: deactivate the repo on the server that +doesn't deploy that branch, or give the servers different status names. If both must stay +active, check how each server handles builds it can't run. + +### WEB-028 — The shared deployer gives up after about 60 s when another deploy is running + +- Priority: P2 · Area: deploy +- Status: open +- Notes: #259 makes the website's production step retry the "busy" answer (6 attempts, 30 s + apart). TripBot's `.drone.yml` needs the same change. +- Files: deployer on the production server (`docker/deploy/`, not in this repo); `.drone.yml` + +**Problem.** The website and TripBot deploy through one deployer (`http://deployer:9000`), +which runs one deployment at a time. A deploy takes about 4.5 minutes, but a second request +waits only about 60 s and then fails with `DEPLOY_RESULT=failure (deployer busy; no deployment +started)`. On 2026-11-08 this failed the website's production build #118 while TripBot #724 +was deploying. It had to be restarted by hand. + +**Fix.** Raise the deployer's wait for its lock above the longest deploy, or queue requests +on the server. Until then, keep the busy-retry in each pipeline. + +### WEB-029 — Factsheets pager ignores filters + +- Priority: P3 · Area: bug +- Status: fixed — fix/WEB-007-query-client +- Files: `src/components/Factsheets.tsx` + +**Problem.** The table passed `rowCount: meta.totalRowCount` (all 555 drugs) to +material-react-table. `rowCount` is meant for server-side pagination, so the footer showed +"1-10 of 555" even when a filter left one row. Production showed the same. + +**Fix.** Drop `rowCount`; the table counts filtered rows itself. + +### WEB-030 — Expanding a factsheet row keeps the browser busy + +- Priority: P2 · Area: perf +- Status: open +- Files: `src/components/Factsheets.tsx`, `src/components/DrugInfo.tsx` + +**Problem.** After a row on `/factsheets` is expanded, its detail cards appear in about +0.4 s, but Chrome's renderer stays busy afterwards: screenshots time out for 30–60 s and one +measured frame took about 2 s. tripsit.me (MUI 6) and tripsit.io (MUI 9) behave the same. +The cause hasn't been investigated; `DrugInfo` renders around 50–130 MUI `Grid` items per +drug, and the table is wrapped in several providers and animations. + +**Fix.** Profile an expansion in Chrome DevTools (Performance panel) to find what keeps the +main thread or compositor busy, then fix that. diff --git a/src/components/Factsheets.tsx b/src/components/Factsheets.tsx index 89030f6..fb06e6d 100644 --- a/src/components/Factsheets.tsx +++ b/src/components/Factsheets.tsx @@ -31,15 +31,8 @@ import { import { Accordion } from "@heroui/react"; import AccordionSection from "./AccordionSection"; import RefreshIcon from "@mui/icons-material/Refresh"; -import { - QueryClient, - QueryClientProvider, - keepPreviousData, - useQuery, -} from "@tanstack/react-query"; // note: this is TanStack Rea`ct Query V5 import { Category, Drug } from "tripsit_drug_db"; -import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns"; -import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { useDrugs } from "../utils/drugs"; import Grid from "@mui/material/Grid"; import KofiButton from "kofi-button"; import UnfoldMoreDoubleIcon from "@mui/icons-material/UnfoldMoreDouble"; @@ -241,37 +234,7 @@ const addCategoryStyle = (text: string | undefined) => { }; const Factsheets = () => { - const { - data: { data = [], meta } = {}, // your data and api response will probably be different - isError, - isRefetching, - isLoading, - refetch, - } = useQuery<{ - data: Array; - meta: { - totalRowCount: number; - }; - }>({ - queryKey: ["table-data"], - queryFn: async () => { - const response = await fetch( - // TripSit's drug database file - // This is fetched every time the user loads the page to ensure they have the latest data - "https://raw.githubusercontent.com/TripSit/drugs/main/drugs.json", - ); - const drugList: Drug[] = Object.values( - (await response.json()) as { [key: string]: Drug }, - ); - return { - data: drugList, - meta: { - totalRowCount: drugList.length, - }, - }; - }, - placeholderData: keepPreviousData, - }); + const { data = [], isError, isRefetching, isLoading, refetch } = useDrugs(); // Define the columns const columns = useMemo[]>( @@ -369,7 +332,6 @@ const Factsheets = () => { enableTopToolbar: true, enableTableHead: true, enableHiding: true, - rowCount: meta?.totalRowCount ?? 0, state: { isLoading, showAlertBanner: isError, @@ -417,13 +379,4 @@ const Factsheets = () => { return ; }; -const ExamplePage = () => ( - // App.tsx or AppProviders file. Don't just wrap this component with QueryClientProvider! Wrap your whole App! - - - - - -); - -export default ExamplePage; +export default Factsheets; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 074d90e..9958ab3 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,5 +1,5 @@ import { AppProps } from "next/app"; -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; // import React, { useEffect, useState } from "react"; import AOS from "aos"; // Vendor CSS Files @@ -22,6 +22,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; // not // const F = "_app.tsx"; export default function App({ Component, pageProps }: AppProps) { + // One client for the whole session. Creating it during render would replace it (and empty + // its cache) on every navigation, so drugs.json would be downloaded again on every page. + const [queryClient] = useState(() => new QueryClient()); // const [stateParam, setStateParam] = useState(""); // const setNewStateParam = () => { @@ -45,7 +48,7 @@ export default function App({ Component, pageProps }: AppProps) { return ( <> - + diff --git a/src/pages/factsheets/[drug].tsx b/src/pages/factsheets/[drug].tsx index ba8907d..857f8be 100755 --- a/src/pages/factsheets/[drug].tsx +++ b/src/pages/factsheets/[drug].tsx @@ -4,8 +4,7 @@ import { useRouter } from "next/router"; import React from "react"; -import { keepPreviousData, useQuery } from "@tanstack/react-query"; // note: this is TanStack Rea`ct Query V5 -import { Drug } from "tripsit_drug_db"; +import { useDrugs } from "../../utils/drugs"; import Header from "../../components/Header"; import Head from "../../components/Head"; @@ -21,27 +20,7 @@ const debugDrug = "cocaine"; // } export default function DrugInfo() { - const { - data: { data = [] } = {}, // your data and api response will probably be different - } = useQuery<{ - data: Array; - }>({ - queryKey: ["table-data"], - queryFn: async () => { - const response = await fetch( - // TripSit's drug database file - // This is fetched every time the user loads the page to ensure they have the latest data - "https://raw.githubusercontent.com/TripSit/drugs/main/drugs.json", - ); - const drugList: Drug[] = Object.values( - (await response.json()) as { [key: string]: Drug }, - ); - return { - data: drugList, - }; - }, - placeholderData: keepPreviousData, - }); + const { data = [] } = useDrugs(); // Go through the drug list and find the drug that matches the name, pretty_name, or any aliases we're looking for const router = useRouter(); diff --git a/src/utils/__tests__/drugs.test.ts b/src/utils/__tests__/drugs.test.ts new file mode 100644 index 0000000..d2a0717 --- /dev/null +++ b/src/utils/__tests__/drugs.test.ts @@ -0,0 +1,31 @@ +import { DRUGS_URL, fetchDrugs } from "../drugs"; + +describe("fetchDrugs", () => { + const originalFetch = global.fetch; + + afterEach(() => { + global.fetch = originalFetch; + }); + + it("returns the drugs from the keyed JSON object as an array", async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + lsd: { name: "lsd", pretty_name: "LSD" }, + mdma: { name: "mdma", pretty_name: "MDMA" }, + }), + }); + + await expect(fetchDrugs()).resolves.toEqual([ + { name: "lsd", pretty_name: "LSD" }, + { name: "mdma", pretty_name: "MDMA" }, + ]); + expect(global.fetch).toHaveBeenCalledWith(DRUGS_URL); + }); + + it("throws when the download fails, so the table shows its error banner", async () => { + global.fetch = jest.fn().mockResolvedValue({ ok: false, status: 503 }); + + await expect(fetchDrugs()).rejects.toThrow("HTTP 503"); + }); +}); diff --git a/src/utils/drugs.ts b/src/utils/drugs.ts new file mode 100644 index 0000000..a9f8d5f --- /dev/null +++ b/src/utils/drugs.ts @@ -0,0 +1,27 @@ +import { useQuery } from "@tanstack/react-query"; +import type { Drug } from "tripsit_drug_db"; + +// TripSit's drug database. Each page load fetches it once; the factsheets table and the +// factsheet pages then share that copy through the app-wide QueryClient in _app.tsx. +export const DRUGS_URL = + "https://raw.githubusercontent.com/TripSit/drugs/main/drugs.json"; + +// The file is over 1 MB and changes rarely, so navigating between pages reuses it +// instead of downloading it again. The table's refresh button still refetches. +export const DRUGS_STALE_TIME = 30 * 60 * 1000; + +export async function fetchDrugs(): Promise { + const response = await fetch(DRUGS_URL); + if (!response.ok) { + throw new Error(`Fetching drugs.json failed: HTTP ${response.status}`); + } + return Object.values((await response.json()) as Record); +} + +export function useDrugs() { + return useQuery({ + queryKey: ["drugs"], + queryFn: fetchDrugs, + staleTime: DRUGS_STALE_TIME, + }); +} From 15658f322fb5ca688efee9ae7bfce668091f1ed6 Mon Sep 17 00:00:00 2001 From: LunaUrsa <1836049+LunaUrsa@users.noreply.github.com> Date: Sun, 8 Nov 2026 13:57:52 -0600 Subject: [PATCH 2/2] Reference #264 in the WEB-007 and WEB-029 statuses Co-Authored-By: Claude Opus 5.5 --- REVIEW-ISSUES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/REVIEW-ISSUES.md b/REVIEW-ISSUES.md index 1245f9b..2413ad3 100644 --- a/REVIEW-ISSUES.md +++ b/REVIEW-ISSUES.md @@ -213,7 +213,7 @@ and pass `code_verifier` to the token endpoint. Quote and validate the avatar UR ### WEB-007 — `QueryClient` recreated on every render - Priority: P1 · Area: perf -- Status: fixed — fix/WEB-007-query-client +- Status: fixed — fix/WEB-007-query-client (#264) - Notes: `_app.tsx` keeps one `QueryClient` in `useState`. The table and the factsheet pages share one `useDrugs()` hook (`src/utils/drugs.ts`, key `["drugs"]`, 30 min `staleTime`), so `drugs.json` downloads once per page load. Their old queries used the same key with different @@ -545,7 +545,7 @@ on the server. Until then, keep the busy-retry in each pipeline. ### WEB-029 — Factsheets pager ignores filters - Priority: P3 · Area: bug -- Status: fixed — fix/WEB-007-query-client +- Status: fixed — fix/WEB-007-query-client (#264) - Files: `src/components/Factsheets.tsx` **Problem.** The table passed `rowCount: meta.totalRowCount` (all 555 drugs) to