---
name: odc-web-route
description: Add or change a route in apps/web — a page under src/routes, its loader, or the useXQuery / useXMutation hook it prefetches. Use also when tsc reports TS2345 on a route literal, or when route-tree.ts or the Playwright suite's RouteTo union is missing it (gen:routes).
---

A route file is the one kind of source in this repo that two **generated** artifacts are derived
from, and the app you just edited regenerates neither. Until both catch up, a perfectly correct route
fails `tsc` in two different workspaces.

## Two generated artifacts, two owners

| Artifact                                     | Derived from                              | Regenerated by                                                                                                                                                           |
| -------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `apps/web/src/route-tree.ts` (git-tracked)   | every file under `apps/web/src/routes/`   | **the user**, by running `pnpm dev` for web — any vite serve or build of `apps/web` runs the `tanstackRouter` plugin in `apps/web/vite.config.ts`, which writes the file |
| `testing/src/generated/route.d.ts` (ignored) | `FileRouteTypes['to']` in `route-tree.ts` | **you**: `pnpm --filter @opendatacapture/testing gen:routes`                                                                                                             |

**The user carries the baton first.** `gen:routes` derives `RouteTo` from `route-tree.ts`
(`testing/AGENTS.md`), so running it against a stale tree writes the same file back:
your literal is still not a member of `RouteTo`, and the `pageModels` map in
`testing/src/support/fixtures.ts` still rejects it as a key. Ask the user for the regeneration, then
run `gen:routes` again — repeating it costs nothing.

The root `AGENTS.md` prohibition covers the first row only. `gen:routes` is not the route-tree
generator — it reads `route-tree.ts` and never writes it — and running it is your job, not the
user's.

**TS2345 on your own route literal is then the expected state of a correct new route**, not a defect
to chase; `.agents/docs/playbooks/add-web-route.md` shows the exact message. Confirm first that the
`createFileRoute` literal is character-for-character the file path, leading `/_app` included — a
mismatched literal raises the same error and survives regeneration. Once it matches, carry on with
the rest of the change and leave both `route-tree.ts` and its generator to the user, unedited (root
`AGENTS.md` hard rule).

Done when your reply names the regeneration the user still owes, and either every literal you wrote
appears in `testing/src/generated/route.d.ts` or your reply says it cannot yet.

## Where the procedure lives

Open the playbook before you write the file; it is the order of operations, and this skill does not
repeat it.

| Open                                          | When                                                                                                                                                                                                              |
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `.agents/docs/playbooks/add-web-route.md`     | Writing or moving a file under `src/routes/` — filename-to-URL mapping, the `createFileRoute` literal, `validateSearch`, `loaderDeps`, the nav entry                                                              |
| `.agents/docs/playbooks/add-web-data-hook.md` | The data the route needs has no hook yet, or you are changing one — every input that changes the response belongs in the `queryKey`                                                                               |
| `apps/web/AGENTS.md`                          | Before naming any route file — a `.` is a path separator, so a stray `logs.test.tsx` becomes the route `/logs/test` — and for the rest of this app: layer folders, the store, translations, where a test may live |

## One key, two callers

The loader prefetches through the hook's `queryOptions` factory and the component calls the hook
(`add-web-data-hook.md` step 3). **Sharing the factory is half of it — the arguments must match too.**
`apps/web/src/routes/_app/datahub/index.tsx` passes `{ groupId: currentGroup?.id }` on both sides and
reaches one cache entry. `_app/admin/audit/logs.tsx` does not: the loader prefetches `deps.search`, the
component asks for `{ ...search, page, sortOrder }`, and `useAuditLogsQuery` keys on the whole params
object — so the prefetched entry is never read and the page fetches again on mount.

Done when every `ensureQueryData` call in your loader passes both the factory and the arguments the
component's hook passes.

## Before you leave

Add a `data-testid` to whatever the e2e test will click or assert on, then open
`.agents/docs/playbooks/add-e2e-test.md` before writing the spec — it owns the selector contract and
the fixture registration. The unit test goes in a `__tests__/` folder beside the file that changed,
named after it: `src/hooks/__tests__/` for a query hook, `src/routes/**/__tests__/` for the route
itself.

The playbook's `## Verify` block is the check to run, with one ordering caveat: `pnpm test:e2e`
boots `apps/web` through `pnpm dev:test` (`testing/playwright.config.ts`), which regenerates
`route-tree.ts` as a side effect. Run it once the user has regenerated, when it rewrites the file to
the same content and leaves no diff.

Done when every element the spec selects carries a `data-testid`, the unit test file exists, and the
Verify block has run clean apart from TS2345 on your own route literal.
