tanshu 58ecfa0dc3 Publish menus from the browser instead of scp
The admin signs in with the shared password, creates a Menu Set, drops photos
into it (shrunk in the browser first), reorders Pages by dragging or with
up/down buttons, previews the Draft exactly as customers would see it, and
publishes it over the previous set. Restore and delete close the loop. Mutations
need a session, a same-origin request and a CSRF token derived from the session;
a Draft's media is served only to a session holder.
2026-09-22 21:22:26 +05:30
2026-09-22 21:18:49 +05:30
2026-09-22 21:18:49 +05:30
2026-09-22 21:18:49 +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, and the admin (sign in, create a set, upload and reorder Pages, preview, publish, restore, delete). Not yet built: the container, the Ansible playbook and the deploy.

Layout

src/config.ts             Site config: brand, theme, Sections, limits
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 config, one file per Site
test/                     node:test suites
docs/plan.md              The agreed plan
docs/adr/                 Decisions worth remembering

Working on it

Node 24 or newer runs the TypeScript sources directly.

npm install
npm test          # node:test
npm run typecheck # tsc --noEmit
npm run seed      # placeholder Pages in .dev/data, for design review
npm run dev       # http://localhost:3000 against .dev

To sign in locally, put a password hash in .dev/site.env:

printf 'ADMIN_PASSWORD_HASH=%s\nSESSION_SECRET=%s\n' \
  "$(node -e "import('./src/auth.ts').then(async m => process.stdout.write(await m.hashPassword('your password')))")" \
  "$(node -e "process.stdout.write(require('node:crypto').randomBytes(32).toString('base64url'))")" \
  > .dev/site.env

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%