tanshu 32eeed144f Let the owner change the name, the icon and the dates
Three things that needed a rebuild and a redeploy, or had no way in at all, now
have a Site identity panel in the admin: the restaurant's name and tagline, an
SVG icon upload, and a switch for whether a menu says when it was published.
They live in data/site.json and data/brand/icon.svg, extending ADR 0007: what
the owner changes is content, and the config is the seed plus the theme, which
still needs a rebuild.

An uploaded SVG is a document that would run with this origin's authority, so it
is reduced to shapes on the way in and served with default-src 'none'. The
archive list keeps its dates even when the switch is off, because those dates
are how one old menu is told from another.

Also fixes the live check's own trap: the app caches identity in memory, so the
tests drive it through the app rather than a second store instance, which would
be a second writer.
2026-09-22 23:11:40 +05:30
2026-09-22 23:07:38 +05:30
2026-09-22 22:35:38 +05:30

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.

S
Description
No description provided
Readme
3.4 MiB
0 Stars 1 Watchers 0 Forks
Languages
TypeScript 81%
JavaScript 6.5%
CSS 6.2%
Shell 4.5%
Jinja 0.7%
Other 1.1%