Files
menu/README.md
T
tanshu c31b73d553 Let the owner manage categories, and give the menus a shelf
Categories were baked into config/<site>.site.json, so adding one meant a
rebuild and a redeploy. They are now data: data/sections.json, seeded from the
config the first time, and managed in the admin (add, rename, reorder, remove).
A category's web address is fixed when it is created because that is what a QR
code points at, and a category cannot be removed while it still holds menus.

The landing and the top bar were the ugliest parts. The landing was a column of
hairlines with tiny meta — a broadsheet, and the paper that makes this app worth
looking at was nowhere on it. It is now a shelf: each category is a row whose
right edge is a sliver of that menu's first page, bleeding off the screen, with
the shelf edge in brass and the name in heavy tight Archivo. The bar was a row
of teal pills that would not survive more categories: it now names the menu you
are reading and opens a sheet listing the rest, each with its own paper sliver,
and the current one is the only teal thing on screen. The sheet is a link to the
landing page, so navigating still works without the script.

Also fixes the category manager's own layout, which I shipped unstyled, and the
markup contract the route tests described.
2026-09-22 22:24:53 +05:30

5.7 KiB

menu-admin

Uploads, orders and publishes the customer-facing menus of a restaurant Site as photographed Pages, replacing the scp-a-JPG workflow. One instance per Site; docs/plan.md is the agreed design and CONTEXT.md is the glossary.

Status

Built: Site config, the Menu Set store, the image pipeline, the customer-facing renderer (landing page, Section pages, archived sets, media route) with the approved design, the admin (sign in, create a set, upload and reorder Pages, preview, publish, restore, delete), the verify/repair CLI, the container and the Ansible playbook. Not yet done: the parallel-run deployment on monoco, which needs a human present.

Deploying

./deploy.sh 2026-09-22          # tests, build, push to registry.tanshu.com

then the playbook in ansible-playbooks/menu-admin/ — its readme covers the parallel run, the site.env step that gives the admin a password, and cutover.

After deploying, check the running site from the outside (read-only, exits non-zero on failure, so it can gate a cutover):

npm run check-site -- https://menu-new.hopsngrains.com

Layout

src/config.ts             Site config: brand, theme, seed Sections, limits
src/storage/sections.ts   Categories: the Site's own list, edited in the admin
src/storage/manifest.ts   Manifest type, validation, atomic JSON writes
src/storage/sets.ts       The Menu Set store: create, add Pages, publish, restore, delete
src/storage/validate.ts   Boot validation: fatal problems and warnings
src/images/pipeline.ts    sharp variants: Original, Viewing image, Thumbnail
src/auth.ts               scrypt password hash, session cookie, login throttle, CSRF
src/render/               HTML builder, document shell, the customer pages, the admin
src/routes/public.ts      Landing, Section, archive and media routes
src/routes/admin.ts       Sign in, dashboard, editor, upload, publish, restore, delete
src/server.ts             buildApp (what tests drive) and main (what the container runs)
public/                   Hand-written CSS, ~2KB of progressive-enhancement JS, Archivo
config/                   Per-Site brand, theme, limits and the first category list
test/                     node:test suites
docs/plan.md              The agreed plan
docs/operating.md         Publishing, fixing and rolling back, for whoever operates it
docs/adr/                 Decisions worth remembering
deploy.sh                 Tests, build, push

The Ansible playbook lives in ansible-playbooks/menu-admin/, beside the other playbooks, because that is where it is run from.

Running it locally

Node 24 or newer runs the TypeScript sources directly; there is no build step.

npm install
npm run seed      # placeholder Pages in .dev (wipes .dev/data; for looking at the design)
npm run dev       # http://localhost:3000 against .dev

npm run dev first runs scripts/dev-env.ts, which creates .dev/site.env on first use with the password menu-dev-password (override with DEV_ADMIN_PASSWORD=...) and a random session secret, then starts the server with --watch. Sign in at http://localhost:3000/admin.

The file is gitignored and development only: the dev server runs with SECURE_COOKIES=false over http, and that password is a known constant. On the server the same file is written by scripts/provision-site-env.sh with a hash of the real password and mode 600.

Tests and checks:

npm test                                      # node:test
npm run typecheck                             # tsc --noEmit, scripts/ included
npm run verify                                # integrity of .dev/data
npm run check-site -- http://127.0.0.1:3000   # checks a running site from outside

Dragging Pages is the one interaction that only a browser can check, so there is a harness for it. It needs geckodriver on PATH and a running dev server:

geckodriver --port 4444 &
npm run check-dnd -- <set-id> native          # drags and asserts it persisted
npm run check-dnd -- <set-id> none            # just reports affordances

In VS Code

.vscode/ holds launch configurations and tasks.

Configuration What it does
Dev server (debug) runs src/server.ts with the dev environment, breakpoints live
Dev server (watch, no debugger) npm run dev in a terminal
Debug the open test file runs the file you have open under node --test, with child processes attached so breakpoints inside a test bind
Check the running site prompts for a URL and runs scripts/check-site.ts

Breakpoints land in the .ts files themselves: Node strips types in place, so line numbers are the ones you see. The debug configuration creates .dev/site.env first (preLaunchTask), so a fresh clone serves a working admin immediately.

Tasks (Command Palette → Run Task) cover the same ground: seed, the dev server, tests, typecheck, verify and repair. .dev, .tmp and .npm-cache are hidden from the explorer, search and the file watcher, so a watched server does not restart on its own data writes.

Who can do what

/ and /<section>/ are public. /admin needs the password; a Draft's Pages are served only to a signed-in session, and Originals have no route at all.

Storage model

data/
├── originals/<section>/<set-id>/<page-id>.<hash>.<ext>   # kept, never served
├── drafts/<section>/<set-id>/                            # manifest.json + variants
└── published/<section>/<slug>/                           # manifest.json + variants

The directory decides a Menu Set's state; the manifest carries metadata only. <slug> is the valid-until date, else the publication date. The Current Menu Set for a Section is the published one with the newest publishedAt. Published sets are immutable: changing a menu means publishing a new set.