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.
134 lines
5.7 KiB
Markdown
134 lines
5.7 KiB
Markdown
# 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
|
|
|
|
```sh
|
|
./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):
|
|
|
|
```sh
|
|
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.
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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.
|