All notable changes to TonyMenu.
The project does not currently publish versioned releases — each entry is dated
and corresponds to a deploy of main. If you fork and tag, switch to the
Keep-a-Changelog [X.Y.Z] heading style.
Each release entry includes a Notes for AI agents subsection. Those notes are written for LLM coding assistants picking up this codebase in a future session: they call out file moves, schema invariants, and gotchas that aren't obvious from the diff but matter when extending the feature.
ai_voice_enabledsetting and migration0008_ai_voice_enabled.sql.- Admin Chat AI sub-toggle for Tony voice dictation, shown only when AI chat is enabled.
- Public catalog
features.aiVoiceflag used by the frontend to show or hide the microphone and dictation language picker.
- Turning off Tony AI chat also turns off voice dictation.
- Demo reset enables voice dictation alongside Tony chat.
None. Existing deployments default voice dictation to disabled until the owner enables it.
git pull
(cd backend && npx wrangler d1 migrations apply risto-menu-demo-db --remote --config wrangler.demo.toml)
(cd backend && npm run deploy)
(cd web && npm run deploy:cf)- Voice dictation is intentionally a sub-feature of
aiChat: API responses coerceaiVoicefalse whenaiChatis false. - Keep backend schema,
packages/schemas, web API types, andRestaurantData.featuresin sync when adding feature flags.
menus.availableDays— optional list of weekday codes (mon,tue,wed,thu,fri,sat,sun) on each menu, alongside the existingavailableFrom/availableTotime window. A diner only sees the menu on days included in the list.null(or omitted) means every day.- 7 weekday toggle chips in the admin menu edit modal, plus a pill in the menu list row showing which days the menu is restricted to.
isMenuAvailableNow(web/src/lib/menu-schedule.ts) now takes an optionalavailableDaysfield and applies anchor-to-start-day semantics for overnight windows: a Fri 22:00 → 02:00 schedule withavailableDays=['fri']is active Fri 22:00–24:00 AND Sat 00:00–02:00, because the wrap belongs to Friday's slot. The day check uses the diner's local time (same convention as the existing time-of-day check).- The admin
PATCH /admin/menus/:menuIdpayload acceptsavailableDays. All-7-days and empty arrays are normalized tonullserver-side so we don't store two equivalent representations of "every day".
None. Existing menus default to availableDays = null (every day) and
catalog consumers that ignore the new field continue to work.
git pull
(cd backend && npx wrangler d1 migrations apply menu-db --remote)
(cd backend && npm run deploy)
(cd web && npm run deploy:cf)0006_menu_available_days.sql— adds nullableavailable_days TEXT(JSON array) tomenus. Backfill is implicit (existing rows getNULL).
- Storage is a JSON text column matching the
customLocales/allergenspattern, not a bitmask. Keep that consistent if you add more multi-value fields to menus. - The validator (
UpdateMenuBodySchemainpackages/schemas/src/admin.ts) rejects empty arrays and duplicates. The admin route additionally collapses a 7-element array tonullbefore writing. - The predicate's anchor-to-start-day rule for overnight windows means
you cannot derive the active day from
now.getDay()alone — you must consultavailableFrom/availableTofirst to decide whethernowis in the wrap-around portion (which belongs to yesterday's slot). Date.getDay()returns 0=Sun..6=Sat; the predicate maps viaWEEKDAY_BY_INDEXto the schema'smon..suncodes. Keep that mapping in sync withWEEKDAYSinpackages/schemas/src/catalog.ts.
- Real SVG flags for standard locales (it / en / de / fr / es / nl / ru / pt)
via the
country-flag-iconspackage, replacing emoji that rendered as letter pairs on Windows. The flags appear in the public LanguagePicker, the admin translation tabs, and the Lingue settings page. - Custom locales (e.g.
vec) gain an optionalflagUrl. Admins can upload a per-locale flag image from the Lingue settings page; the upload reuses the existing R2 image pipeline (JPEG/PNG/WebP, 5 MB cap) and stores objects underimages/settings/flag-<code>-*.{ext}. Old keys are deleted on re-upload and on remove. - New backend endpoints:
POST /admin/locale-flag/:codeandDELETE /admin/locale-flag/:code. - New shared UI primitive
<Flag>(inweb/src/components/ui/Flag.tsx) that renders, in order: a custom uploaded URL → bundled SVG for known locales → uppercase code-chip fallback.
customLocales[]items gain an optionalflagUrlfield in bothUpdateSettingsBodySchemaand the public catalogfeatures.customLocales. Optional and backwards-compatible — existing rows continue to work.
None. Existing catalog consumers ignore the new flagUrl field.
git pull
(cd backend && npx wrangler d1 migrations apply menu-db --remote)
(cd backend && npm run deploy)
(cd web && npm run deploy:cf)No DB migrations in this release. customLocales[*].flagUrl lives inside
the existing settings.custom_locales JSON column.
- none in this release
country-flag-iconsSVG strings are imported per-file (country-flag-icons/string/3x2/<CC>) and converted to data URLs inweb/src/lib/locale-flags.ts. The barrelcountry-flag-icons/react/3x2pulls every flag — keep imports per-file.web/src/types/country-flag-icons.d.tsdeclares the per-file subpath modules; the package only ships types for the index.- Custom-flag uploads deliberately do not accept SVG (parser-level XSS). If that ever changes, sanitize before storing.
Flagaccepts adecorativeprop. Use it whenever the locale label text is rendered next to the flag (tabs, dropdown rows) — otherwise the accessible name double-includes the language and breaks testing-librarygetByRole("button", { name: ... })lookups.
- Arbitrary user-defined menus replace the rigid
seated/takeawaysplit. Each menu has a code (URL slug), title (with i18n),publishedtoggle, drag-orderable position, and a curated standard icon. Owners create and rename menus from a new top-level admin page (?s=menus). - Many-to-many entry membership: a single dish can appear on any subset of
menus via the new
menu_entry_memberships(menu_id, entry_id)join table. - Per-entry
hiddenflag (independent of per-menu draft) for owner-only items that should disappear from the public catalog without being deleted. - Items admin page gains a menu filter (
All/<each menu>/No menu) and a "Show hidden" toggle so orphans from the migration are findable. - Curated set of inline-SVG icons rendered by
<MenuIcon>:utensils,lunch,dinner,breakfast,wine,beer,cocktail,coffee,pizza,burger,dessert,salad,fish,bread. Picker grid in the Menus admin modal. - New backend endpoints:
GET/POST /admin/menus,PATCH/DELETE /admin/menus/:id,PATCH /admin/menus/reorder.POST /admin/categorieswas missing entirely before — also added.
- Catalog response shape:
categories[]is now top-level (with entries that carrymenuIds[]andhidden);menus[]carries metadata only (id,code,title,i18n,published,sortOrder,icon). OpeningScheduleis a singleWorkingHours(noseated/takeawaysplit). The Hours admin page collapsed accordingly.MenuEntryadmin form: visibility radio replaced with a per-menu chip multi-select + a separate "Hidden" toggle.- Public route is
/[locale]/menudriven by?type=<menu-code>.?type=drinksaliases to adrinks- ortakeaway-coded menu so old QR codes from the seated/takeaway era keep working.
MenuVisibilitySchema('all' | 'seated' | 'takeaway' | 'hidden') and theMenuSelectionenum.menuVisibilityis gone fromMenuEntry.RestaurantMessages.onCartSeated/onCartTakeaway(no cart flow exists).web/src/stores/menuSelectionStore.{ts,test.ts}and theuseIsSeated/useIsTakeaway/useHasSelectionhooks.
- 0001_multi_menu.sql — schema rewrite + data backfill:
- drops
menu_categories.menu_id(categories become flat), - drops
menu_entries.visibility, addsmenu_entries.hidden, - adds
menus.published,menus.sort_order, - creates
menu_entry_memberships, - seeds memberships from the legacy visibility flag (
'all'→ all menus,'<code>'→ menu with that code,'hidden'→ no memberships +hidden=1), - assigns
menus.sort_order(seated=0, takeaway=1, others 2), - collapses
settings.opening_schedulefrom{seated, takeaway}to a singleWorkingHours, preferringseated.
- drops
- 0002_menu_icon.sql — adds
menus.iconTEXT NOT NULL DEFAULT 'utensils'.
- Apply migrations:
npx wrangler d1 migrations apply menu-db --remote. - After deploy, sign in to
/admin?s=menusand rename the legacyseated/takeawaycodes to whatever fits the restaurant (e.g.food,drinks,lunch,wines). - Pick a standard icon for each menu — every menu defaulted to
utensils. - Use the Items page filter
All menus → No menuplus "Show hidden" to find any entries the migration orphaned (these will be entries that hadvisibility='hidden'in the old model). Re-attach to a menu or delete.
- The membership table is the single source of truth for which menu an entry
belongs to. Categories are flat (no
menuIdparent). Do not reintroduce a category→menu hierarchy — seedocs/adr/if a decision record is added later. - The
?type=query param is intentional. Static export (output: "export"innext.config.ts) forbids runtime dynamic route segments, so a/menu/[code]route would require enumerating every menu code at build time — but codes are user-defined in D1. Keep menu routing query-based. MENU_ICONSlives in two places:packages/schemas/src/catalog.ts(zod enum, used to validate writes) andweb/src/components/menu/MenuIcon.tsx(renderer + admin picker). They must stay in sync. Re-exporting from@menu/schemasto the web package was tried and currently breaks Turbopack barrel resolution at SSR; if you fix that, you can collapse the two lists.<MenuIcon>falls back toutensilsfor unknown kinds, so adding a new icon kind is forward-compat (DB writes can land before the frontend ships the SVG case).- Public layout (
web/src/app/[locale]/layout.tsx) does NOT load Font Awesome — only the admin layout does. Public-facing icons must be inline SVG. If you add an admin-only feature, FA classes are fine. setEntryMemberships()inbackend/src/routes/admin.tsis delete-then-insert. It is not transactional across the two statements. D1 doesn't support multi-statement transactions; if you refactor, keep the contract that the memberships set is fully replaced (not merged).- Backend tests use seed helpers in
backend/src/__tests__/helpers/db.ts:seedMenu(db, id, code, title?, {published?, sortOrder?}),seedCategory(db, id, name?, sortOrder?)(nomenuIdarg — categories are flat),seedEntry(db, id, categoryId, {hidden?, ...}),seedMembership(db, menuId, entryId). - Drizzle-kit's interactive
generatewas bypassed via a tiny Python PTY wrapper to feed Enter for the rename-vs-create-column prompt (we wanted "create"). If you regenerate migrations and don't have a TTY, do the same or write the SQL by hand and updatemeta/_journal.jsonmeta/<n>_snapshot.json.
- The git-shield pre-push hook flags drizzle snapshot UUIDs as secrets and
Italian dish names as PII. The narrow allowlist regexes live in
.pii-allowlist. New Italian fixtures may need additions.