diff --git a/REVIEW-ISSUES.md b/REVIEW-ISSUES.md index 5cddec9..2413ad3 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 (#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 + 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 (#264) +- 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, + }); +}