Skip to content

feat(server): pages to manage aisles - #631

Open
LeMyst wants to merge 2 commits into
cooklang:mainfrom
LeMyst:feat/aisle-pages
Open

LeMyst wants to merge 2 commits into
cooklang:mainfrom
LeMyst:feat/aisle-pages

Conversation

@LeMyst

@LeMyst LeMyst commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #630.

A new /aisles page, linked from the shopping list header ("Manage aisles") and from Preferences next to the aisle file's path, manages aisle.conf from the browser.

Aisles

What it does

  • Aisles tab: the aisles in file order, which is the order the shopping list uses. Each ingredient is a chip showing its first name and its other names. You can add, rename, move up or down and remove an aisle (removing one confirms how many ingredients go with it). Click a chip to rename the ingredient, change its synonyms, move it to another aisle or remove it.
  • Uncategorized tab: the ingredients the recipes and menus use that no aisle names (the cook doctor aisle check, via cookcli_core::doctor::aisle_coverage), each with links to the recipes using it and an aisle picker.
  • Text tab: the raw file. It is saved only once cooklang::aisle::parse accepts it; if not, the error names the line.
  • No aisle file: a "Create aisle.conf" button starts an empty config/aisle.conf in the recipe directory. The server now uses an aisle file that appears after startup (AppState::aisle_file()), so no restart is needed.
  • Shopping list: items in other get an "Assign aisle…" picker. Picking an aisle adds the ingredient to it and regroups the list.
Editing an ingredient Uncategorized Shopping list
Text Mobile Preferences

Design notes

  • Line edits, not a rewrite. src/server/aisle_file.rs reads lines the way cooklang::aisle::parse does and changes only the lines an edit is about. Comments, and the blank lines seed/config/aisle.conf uses to group a long aisle, survive. cooklang::aisle::write would drop both. Comments directly above an aisle move and are removed with it; CRLF files stay CRLF. In the end-to-end test, adding, moving and removing an ingredient leaves seed's file byte-identical.
  • Revisions. Every response carries a revision, a hash of the file text. A change sent with an outdated revision gets 409, and the page reloads the aisles and asks you to try again. On the Text tab your typing is kept, and saving a second time replaces the other change. Changes are also serialised with an in-process lock.
  • Uniqueness. Ingredient names are kept unique ignoring case, because the shopping list matches them that way (ingredients_info lowercases), and so are aisle names. A name with | or // is refused, since it would turn into a synonym or a comment.
  • Backstop. A change that would leave a previously valid file unreadable to the strict parser is refused.

API

All under /api/aisles and documented in /api-docs and docs/api.md. Writes need Capability::EditLists (shopper and up), like the pantry, and are written to the activity log.

GET /api/aisles aisles, revision, parser warnings
POST /api/aisles create config/aisle.conf (201, or 409 if one exists)
POST /api/aisles/changes {revision?, action, …}: add_aisle, rename_aisle, remove_aisle, move_aisle, add_ingredient, update_ingredient, remove_ingredient
GET/PUT /api/aisles/raw the file as text
GET /api/aisles/uncategorized ingredients no aisle names, with their recipes

Under --recipes-only, guests cannot reach /aisles because of the existing allowlist.

Tests

  • aisle_file.rs: 16 unit tests covering preserved lines, CRLF, moving blocks with their comments, case-insensitive duplicates and invalid names.
  • tests/aisles_api_test.rs: 7 tests that boot cook server (through with_isolated_config). They cover: only the changed lines are touched, 409 on a stale revision, 400/404 cases, raw text validation, creating the file from nothing (the shopping list picks it up without a restart), and uncategorized.
  • server_auth_test: /api/aisles/changes added to the role matrix (reader refused, shopper and up allowed); middleware unit tests updated.
  • tests/e2e/aisles.spec.ts: 6 Playwright tests. They restore seed/config/aisle.conf after each test and stub only the shopping list's item list, so they don't race other specs over the shared list.
  • Translations in all 9 locales (locales_test passes).

cargo fmt, cargo clippy --all-targets (no warnings) and cargo test (1139 passed) are clean. The full Chromium suite locally: aisles, shopping-list and preferences specs all pass serially. With parallel workers, the only failures are the known shopping-list races between workers and static-file-search (needs a cook binary in the container), both seen on main in earlier runs; the shopping-list specs pass when run serially with this change.

🤖 Generated with Claude Code

LeMyst and others added 2 commits October 5, 2026 23:28
`aisle.conf` decides how the shopping list is grouped, but the web UI only
printed its path, and an ingredient no aisle named fell silently under
"other". A new /aisles page, reached from the shopping list and from
Preferences, shows the aisles in file order and lets a shopper:

- add, rename, reorder and remove aisles;
- add, rename, move and remove ingredients and their `a | b` synonyms;
- see the ingredients the recipes use that no aisle names (the
  `cook doctor aisle` check) and file each under an aisle;
- edit the file as text, saved only once the aisle parser reads it;
- start a `config/aisle.conf` where there is none.

The shopping list's "other" items get an "Assign aisle" picker too.

Edits go through `server::aisle_file`, which changes only the lines
concerned, so comments and the blank lines that group a long aisle stay
put; `cooklang::aisle::write` would have dropped both. Each change carries
the revision of the file it was made against and is refused with 409 if
the file moved on. Ingredient names are kept unique ignoring case, as the
shopping list matches them. The server now also picks up an aisle file
created after it started.

The routes live under /api/aisles at `Capability::EditLists`, like the
pantry; every write is recorded in the activity log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`AppState::aisle_file` sat in the impl block that only exists with
`sync`, so `--no-default-features --features server,lsp` (the Docker
image's build) did not compile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LeMyst
LeMyst marked this pull request as ready for review October 5, 2026 23:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Manage aisles (aisle.conf) from the web server

1 participant