Recipes browse page (/recipes + /recipe?id=) - follow-up B from #98 [depends on A] #101

Closed
opened 2026-07-07 06:54:04 +00:00 by coilysiren · 2 comments
Owner

The recipes browse surface from the #98 design: a recipe list + detail analogous to /items -> /item, reading the recipe data that follow-up A hydrates. This is follow-up B.

Depends on #98-A (the recipe data ingest / /preview/recipes.json). Do not start until A has landed a real recipe payload.

Scope

  • /recipes - list page modeled on pages/Items.tsx: URL-driven ?q= filter (client-side, on product + ingredient pretty names, capped like LIST_ROWS), rows deep-link to /recipe?id=.... Numbers via lib/format.ts.
  • /recipe?id=... - detail modeled on pages/Item.tsx (query-param key + the fetched.id === id stale-guard). Shows the BOM tree, station, required skill + level, labor / calorie / time cost, and cross-links: the product's current market price (market.py), who has crafted it (crafting.py by_crafted), and where to buy / sell it (logistics.py).
  • Filters: by profession/skill (skill.name), by station, by ingredient (reverse lookup - "what can I make with X", the highest-value filter), by product (default search), by table tier.
  • New lib/recipesApi.ts typed client mirroring /preview/recipes.json. /recipe is URL-only (reached from /recipes), one hub card in Home.tsx, nav link in Layout.tsx only if it earns primary nav.

Conventions & done

One heading tier + minimal prose, Layout/Hero, vitest component tests including the empty/degraded branch. Update docs/FEATURES.md. Green on ward exec test + lint. Full design context on #98.

The recipes browse surface from the #98 design: a recipe list + detail analogous to `/items` -> `/item`, reading the recipe data that follow-up **A** hydrates. This is follow-up **B**. **Depends on #98-A (the recipe data ingest / `/preview/recipes.json`).** Do not start until A has landed a real recipe payload. ## Scope - **`/recipes`** - list page modeled on `pages/Items.tsx`: URL-driven `?q=` filter (client-side, on product + ingredient pretty names, capped like `LIST_ROWS`), rows deep-link to `/recipe?id=...`. Numbers via `lib/format.ts`. - **`/recipe?id=...`** - detail modeled on `pages/Item.tsx` (query-param key + the `fetched.id === id` stale-guard). Shows the BOM tree, station, required skill + level, labor / calorie / time cost, and cross-links: the product's current market price (`market.py`), who has crafted it (`crafting.py` `by_crafted`), and where to buy / sell it (`logistics.py`). - **Filters**: by profession/skill (`skill.name`), by station, by ingredient (reverse lookup - "what can I make with X", the highest-value filter), by product (default search), by table tier. - New `lib/recipesApi.ts` typed client mirroring `/preview/recipes.json`. `/recipe` is URL-only (reached from `/recipes`), one hub card in `Home.tsx`, nav link in `Layout.tsx` only if it earns primary nav. ## Conventions & done One heading tier + minimal prose, `Layout`/`Hero`, vitest component tests including the empty/degraded branch. Update `docs/FEATURES.md`. Green on `ward exec` test + lint. Full design context on #98.
Member

🔒 Reserved by ward agent --driver claude — container engineer-claude-eco-app-101 on host KAI-DESKTOP-TOWER is carrying this issue (reserved 2026-07-07T07:16:44Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

Do not comment on or edit this issue to steer the run while it is reserved. The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a new issue, dispatched fresh — that is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494).

run seed context — what this run is carrying (ward#609)
  • Resolved: coilyco-gaming/eco-app#101 · branch issue-101 · driver claude · workflow direct-main
  • Run: engineer-claude-eco-app-101 · ward v0.419.0 · dispatched 2026-07-07T07:16:44Z
  • Comment thread: 0 included in the pre-flight read, 0 stripped (ward's own automated comments).

Issue body as seeded:

The recipes browse surface from the #98 design: a recipe list + detail analogous to `/items` -> `/item`, reading the recipe data that follow-up **A** hydrates. This is follow-up **B**.

**Depends on #98-A (the recipe data ingest / `/preview/recipes.json`).** Do not start until A has landed a real recipe payload.

## Scope

- **`/recipes`** - list page modeled on `pages/Items.tsx`: URL-driven `?q=` filter (client-side, on product + ingredient pretty names, capped like `LIST_ROWS`), rows deep-link to `/recipe?id=...`. Numbers via `lib/format.ts`.
- **`/recipe?id=...`** - detail modeled on `pages/Item.tsx` (query-param key + the `fetched.id === id` stale-guard). Shows the BOM tree, station, required skill + level, labor / calorie / time cost, and cross-links: the product's current market price (`market.py`), who has crafted it (`crafting.py` `by_crafted`), and where to buy / sell it (`logistics.py`).
- **Filters**: by profession/skill (`skill.name`), by station, by ingredient (reverse lookup - "what can I make with X", the highest-value filter), by product (default search), by table tier.
- New `lib/recipesApi.ts` typed client mirroring `/preview/recipes.json`. `/recipe` is URL-only (reached from `/recipes`), one hub card in `Home.tsx`, nav link in `Layout.tsx` only if it earns primary nav.

## Conventions & done

One heading tier + minimal prose, `Layout`/`Hero`, vitest component tests including the empty/degraded branch. Update `docs/FEATURES.md`. Green on `ward exec` test + lint. Full design context on #98.

Static container doctrine and seed boilerplate are identical every run and omitted here (they ride ward v0.419.0).

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `engineer-claude-eco-app-101` on host `KAI-DESKTOP-TOWER` is carrying this issue (reserved 2026-07-07T07:16:44Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. **Do not comment on or edit this issue to steer the run while it is reserved.** The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a **new issue, dispatched fresh** — that is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494). <details><summary>run seed context — what this run is carrying (ward#609)</summary> - **Resolved:** `coilyco-gaming/eco-app#101` · branch `issue-101` · driver `claude` · workflow `direct-main` - **Run:** `engineer-claude-eco-app-101` · ward `v0.419.0` · dispatched `2026-07-07T07:16:44Z` - **Comment thread:** 0 included in the pre-flight read, 0 stripped (ward's own automated comments). **Issue body as seeded:** ``` The recipes browse surface from the #98 design: a recipe list + detail analogous to `/items` -> `/item`, reading the recipe data that follow-up **A** hydrates. This is follow-up **B**. **Depends on #98-A (the recipe data ingest / `/preview/recipes.json`).** Do not start until A has landed a real recipe payload. ## Scope - **`/recipes`** - list page modeled on `pages/Items.tsx`: URL-driven `?q=` filter (client-side, on product + ingredient pretty names, capped like `LIST_ROWS`), rows deep-link to `/recipe?id=...`. Numbers via `lib/format.ts`. - **`/recipe?id=...`** - detail modeled on `pages/Item.tsx` (query-param key + the `fetched.id === id` stale-guard). Shows the BOM tree, station, required skill + level, labor / calorie / time cost, and cross-links: the product's current market price (`market.py`), who has crafted it (`crafting.py` `by_crafted`), and where to buy / sell it (`logistics.py`). - **Filters**: by profession/skill (`skill.name`), by station, by ingredient (reverse lookup - "what can I make with X", the highest-value filter), by product (default search), by table tier. - New `lib/recipesApi.ts` typed client mirroring `/preview/recipes.json`. `/recipe` is URL-only (reached from `/recipes`), one hub card in `Home.tsx`, nav link in `Layout.tsx` only if it earns primary nav. ## Conventions & done One heading tier + minimal prose, `Layout`/`Hero`, vitest component tests including the empty/degraded branch. Update `docs/FEATURES.md`. Green on `ward exec` test + lint. Full design context on #98. ``` Static container doctrine and seed boilerplate are identical every run and omitted here (they ride ward v0.419.0). </details> <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Owner

WARD-OUTCOME: done - /recipes directory + /recipe?id= detail shipped over the static recipe graph, with skill/station/ingredient/product filters, market cross-links, nav + Home card, and vitest coverage; landed on main (f82dc51).

Honest retrospective: this one went smoothly, mostly because follow-up A did the hard part. The recipe DTO from recipes.py was already clean and complete, so the frontend was largely a faithful copy of the /items -> /item pattern with the fields swapped. Modeling on the existing pages paid off — the URL-driven filter/update helpers ported almost verbatim.

Two things fought back mildly. First, the one place I consciously diverged from the seed: it asks for the fetched.id === id stale-guard from Item.tsx, but the recipe graph is a single static bundled payload with no per-id endpoint, so there is genuinely nothing to guard — I fetch the index once and resolve the recipe client-side by name. I kept the empty/not-found/error branches the guard exists to protect and left a comment explaining the divergence, rather than mechanically copying a guard that would never fire. Second, ?tier= (table tier) is dead in the vanilla seed — tableTierRequired is null everywhere until the cost engine (C) derives it — so I made the tier facet self-hiding until real values land, instead of shipping an empty control.

Confidence is high: 14 new component tests cover the filters, the reverse-lookup ('what can I make with X'), and every empty/degraded/not-found branch; frontend-test/build/lint and the precommit suite are all green; and I confirmed the live server serves /preview/recipes.json (1,453 recipes) and both SPA routes 200. I couldn't drive the rendered page through the Playwright MCP — its browser sandbox can't reach the container's localhost — so runtime verification leaned on curl + the component suite rather than a real click-through.

Rough edges / possible follow-ups: the full recipes.json is ~1.3MB uncompressed (well-cached, gzips to ~150KB, shared by both routes, so fine for now, but a lighter list projection would help if it grows); and the market cross-link is a link to /item, not embedded live price data on the recipe page itself — a deliberate scope call reading 'cross-links' literally, worth revisiting once the cost engine (C) gives the recipe page its own pricing to show.

WARD-OUTCOME: done - /recipes directory + /recipe?id= detail shipped over the static recipe graph, with skill/station/ingredient/product filters, market cross-links, nav + Home card, and vitest coverage; landed on main (f82dc51). Honest retrospective: this one went smoothly, mostly because follow-up A did the hard part. The recipe DTO from `recipes.py` was already clean and complete, so the frontend was largely a faithful copy of the `/items` -> `/item` pattern with the fields swapped. Modeling on the existing pages paid off — the URL-driven filter/update helpers ported almost verbatim. Two things fought back mildly. First, the one place I consciously diverged from the seed: it asks for the `fetched.id === id` stale-guard from `Item.tsx`, but the recipe graph is a single static bundled payload with no per-id endpoint, so there is genuinely nothing to guard — I fetch the index once and resolve the recipe client-side by name. I kept the empty/not-found/error branches the guard exists to protect and left a comment explaining the divergence, rather than mechanically copying a guard that would never fire. Second, `?tier=` (table tier) is dead in the vanilla seed — `tableTierRequired` is null everywhere until the cost engine (C) derives it — so I made the tier facet self-hiding until real values land, instead of shipping an empty control. Confidence is high: 14 new component tests cover the filters, the reverse-lookup ('what can I make with X'), and every empty/degraded/not-found branch; frontend-test/build/lint and the precommit suite are all green; and I confirmed the live server serves `/preview/recipes.json` (1,453 recipes) and both SPA routes 200. I couldn't drive the rendered page through the Playwright MCP — its browser sandbox can't reach the container's localhost — so runtime verification leaned on curl + the component suite rather than a real click-through. Rough edges / possible follow-ups: the full `recipes.json` is ~1.3MB uncompressed (well-cached, gzips to ~150KB, shared by both routes, so fine for now, but a lighter list projection would help if it grows); and the market cross-link is a link to `/item`, not embedded live price data on the recipe page itself — a deliberate scope call reading 'cross-links' literally, worth revisiting once the cost engine (C) gives the recipe page its own pricing to show.
Sign in to join this conversation.
No description provided.