# /browse — the project index Every project, of every kind, in one list. **Zero client JavaScript**: the filters are links that change `searchParams`, and `?q=` is a plain GET form. Nothing here hydrates, and `pnpm build` still reports the page as server-rendered. ## The four filters | filter | param | values | |---|---|---| | kind / template | `?kind=`, `?template=` | registry ids | | state | `?state=` | `draft` `windows` `fetched` `built` `shipped` `stale` | | open decisions | `?open=blocking` \| `?open=1` | from the decision counts | | text / recency | `?q=`, `?sort=name\|recent` | substring over the haystack | **Counts come from the unfiltered set.** A chip whose number changes when you click a different chip moves under the cursor, and the whole point of a filter row is to say how much is behind each one. An e2e spec asserts the chip's number equals the number of cards rendered. `?q=` is a `
` carrying the other filters as hidden inputs — the `/browse/find` idiom — so searching does not throw away the filters you set, and any filtered view is one pasteable URL. ## The state vocabulary One vocabulary across every kind, not per-kind words, because the point of the filter is to ask "what is half-done" without first asking "half-done at what". A kind maps its own situation onto these; it does not invent a seventh. ## The cards `components/projects/ProjectGrid.tsx` **contains no kind ids and no per-kind branches**, and that is the design working rather than an omission: a kind's summariser has already rendered its own facts (`19 clips · 4m46s · 17 sources`, `3/4 cuts · 2 variants`) and its own flags. The grid lays out strings. A kind can attach `data-*` attributes to its card through `attrs`, which is how a song keeps `data-missing` as an assertion surface without the grid knowing what a cut is. The poster falls back: the deliverable → a built **segment** (which already carries the chrome, so the card looks like the video mid-build) → a raw clip → nothing. Served by `/api/browse/poster?project=…`, which takes **no client-supplied `rel`** — the frame is the one the summariser chose. ## Routing `app/browse/[...path]` resolves the **longest** path prefix that is a project and hands the rest to that kind's view. `a/b` being a project must not stop `a/b/c` from being one. - `[]` → the project page - `["wide"]` on a song → the cut page, unchanged - `["clip","c04"]` on a report video → the clip bench - anything else → 404, rather than a page that silently drops half its URL The static tool pages (`/browse/decisions`, `find`, `sources`, `faces`, `trim`, `at`) still win their routes, and a spec asserts each is 200. ## Cost Listing never probes and never shells out — the rule `listSongs()` already followed, extended to every kind. A summary is memoised against a signature of mtimes and sizes, so it survives for as long as the project has not changed and is discarded the moment it has. The `open` filter needs decision counts for every project, which means running each kind's reducer. That is memoised the same way; the expensive input is cue files, and their *derived* answers are cached against the file's own mtime. ## Discovered by getting it wrong once **Tailwind's source detection is turned OFF, and the three real directories are named in `app/globals.css`.** It has to be. Auto-detection honours `.gitignore` but scans everything else, so a scratch `NEXT_DIST_DIR` got read, its binary turbopack cache yielded a garbage class candidate, and every page 500d on a CSS parse error with nothing wrong in the CSS. Then it happened a second time from **this file**: an earlier draft quoted the corrupt candidate to explain the first failure, Tailwind scanned the markdown, and the trap re-created itself. Hence `source(none)` plus explicit `@source` — a doc, a fixture, a test artefact or a stray dist dir can no longer poison the stylesheet at all. If you add a directory that holds class names, name it there. **`lmdb` must be in `serverExternalPackages`.** Bundled, Turbopack tries to resolve its optional `moduleRequire('cbor-x')` and fails the whole module graph — so every page importing `lib/projects` 500s naming a package nothing here uses.