Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 79 additions & 3 deletions REVIEW-ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

---

Expand Down Expand Up @@ -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.** `<QueryClientProvider client={new QueryClient()}>` builds a new client each time
Expand Down Expand Up @@ -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.**
Expand Down Expand Up @@ -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,
Expand All @@ -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.
53 changes: 3 additions & 50 deletions src/components/Factsheets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<Drug>;
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<MRT_ColumnDef<Drug>[]>(
Expand Down Expand Up @@ -369,7 +332,6 @@ const Factsheets = () => {
enableTopToolbar: true,
enableTableHead: true,
enableHiding: true,
rowCount: meta?.totalRowCount ?? 0,
state: {
isLoading,
showAlertBanner: isError,
Expand Down Expand Up @@ -417,13 +379,4 @@ const Factsheets = () => {
return <MaterialReactTable table={table} data-bs-theme="dark" />;
};

const ExamplePage = () => (
// App.tsx or AppProviders file. Don't just wrap this component with QueryClientProvider! Wrap your whole App!
<LocalizationProvider dateAdapter={AdapterDateFns}>
<QueryClientProvider client={new QueryClient()}>
<Factsheets />
</QueryClientProvider>
</LocalizationProvider>
);

export default ExamplePage;
export default Factsheets;
7 changes: 5 additions & 2 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 = () => {
Expand All @@ -45,7 +48,7 @@ export default function App({ Component, pageProps }: AppProps) {
return (
<>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<QueryClientProvider client={new QueryClient()}>
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
</LocalizationProvider>
Expand Down
25 changes: 2 additions & 23 deletions src/pages/factsheets/[drug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<Drug>;
}>({
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();
Expand Down
31 changes: 31 additions & 0 deletions src/utils/__tests__/drugs.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
27 changes: 27 additions & 0 deletions src/utils/drugs.ts
Original file line number Diff line number Diff line change
@@ -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<Drug[]> {
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<string, Drug>);
}

export function useDrugs() {
return useQuery({
queryKey: ["drugs"],
queryFn: fetchDrugs,
staleTime: DRUGS_STALE_TIME,
});
}
Loading