Repository navigation
feat(server): pages to manage aisles - #631
Merged
Merged
Conversation
`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
marked this pull request as ready for review
October 5, 2026 23:22
… from any picker Feedback on the aisles page: - Grouping. Several spellings of one thing (butter, salted butter, cold butter…) can now become one ingredient. "Select" on the Aisles tab picks chips; "Group…" asks which name the shopping list should show and merges the rest into it (`merge_ingredients`). The edit panel lists an ingredient's names with the main one marked, and any other can be made main, removed, or added to. - Uncategorized in bulk. Rows get checkboxes, a filter and "Select all shown". The picked ones go into an aisle together, or join an existing ingredient as other names (`add_names`), which is how "cold butter" joins "butter". Only picks the filter shows are acted on. - New aisle from any picker. Every aisle select, the shopping list's "Assign aisle…" included, ends with "+ New aisle…"; the aisle is created in the same request as what it was picked for. `POST /api/aisles/changes` takes `changes`, a list applied in order and written together or not at all; an error names the failing change. The single flat change still works. The page also follows `#uncategorized` and `#text` when the hash changes, not only on load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… page Selecting ingredients printed "undefined" in the bar under the aisles, and "false" once the Group panel was cancelled: the bar was filled with the DOM's own `replaceChildren`, which writes a missing or false part out as text where `el()` leaves it out. An aisle's header had the same pattern and would print "false" for a reader. Both now go through `fill()`, which skips them as `el()` does. The aisle picker next to "Group…" and in an ingredient's edit panel read as part of grouping or naming, which never use an aisle. It is moving, so it now says so: "Move to aisle", in its own section of the bar, hidden while the Group panel is open, and on its own row of the edit panel, below the names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.conffrom the browser.Grouping, bulk filing and new aisles (233119e)
After a first round of use:
Salt/salt) are filed once, since the list matches ignoring case.What it does
cook doctor aislecheck, viacookcli_core::doctor::aisle_coverage), each with links to the recipes using it and an aisle picker.cooklang::aisle::parseaccepts it; if not, the error names the line.config/aisle.confin the recipe directory. The server now uses an aisle file that appears after startup (AppState::aisle_file()), so no restart is needed.Design notes
src/server/aisle_file.rsreads lines the waycooklang::aisle::parsedoes and changes only the lines an edit is about. Comments, and the blank linesseed/config/aisle.confuses to group a long aisle, survive.cooklang::aisle::writewould 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.revision, a hash of the file text. A change sent with an outdated revision gets409, 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.ingredients_infolowercases), and so are aisle names. A name with|or//is refused, since it would turn into a synonym or a comment.API
All under
/api/aislesand documented in/api-docsanddocs/api.md. Writes needCapability::EditLists(shopper and up), like the pantry, and are written to the activity log.GET /api/aislesfile(relative to the recipe directory) /shared(the global file)POST /api/aislesconfig/aisle.conf(201, or409if one exists)POST /api/aisles/changes{revision?, action, …}:add_aisle,rename_aisle,remove_aisle,move_aisle,add_ingredient,update_ingredient,remove_ingredient,add_names,merge_ingredients; or{revision?, changes: [ … ]}, applied in order and written together or not at all (the error names the change that failed)GET/PUT /api/aisles/rawGET /api/aisles/uncategorizedUnder
--recipes-only, guests cannot reach/aislesbecause of the existing allowlist.No absolute paths (32daca3). The aisle file's full path is never sent:
GET /api/aislesgivesfile, the path relative to the recipe directory (config/aisle.conf), orshared: truefor the global aisle file.GET /api/aisles/rawand the409fromPOST /api/aislesdon't say where the file is; the server's own log does. These reads are open to visitors who haven't signed in, so the full path would have shown them the server's directories and account name. A test checks that no answer, and not the page either, contains the server's directory. The Preferences page has the same issue from before this PR: #637.Tests
aisle_file.rs: 19 unit tests covering preserved lines, CRLF, moving blocks with their comments, case-insensitive duplicates, invalid names, adding names and grouping (across aisles, main from any entry).tests/aisles_api_test.rs: 7 tests that bootcook server(throughwith_isolated_config). They cover: only the changed lines are touched,409on a stale revision,400/404cases, raw text validation, creating the file from nothing (the shopping list picks it up without a restart), uncategorized, all-or-nothing batches, and grouped names showing on the shopping list under the main one.server_auth_test:/api/aisles/changesadded to the role matrix (reader refused, shopper and up allowed); middleware unit tests updated.tests/e2e/aisles.spec.ts: 10 Playwright tests, including grouping, making a name main, filtered multi-select with joining an existing ingredient, bulk filing into a new aisle, and "+ New aisle…" on the shopping list. They restoreseed/config/aisle.confafter each test and stub only the shopping list's item list, so they don't race other specs over the shared list.locales_testpasses).cargo fmt,cargo clippy --workspace --all-targets --all-features -- -D warnings, the same for--no-default-features --features server,lsp(the Docker build),cargo test(1149 passed) andcargo test --no-default-features(433 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 andstatic-file-search(needs acookbinary in the container), both seen onmainin earlier runs; the shopping-list specs pass when run serially with this change.🤖 Generated with Claude Code