Commit Graph
13 Commits
Author SHA1 Message Date
tanshu 33a8d56e8b Add the Great Bear and Hops n Grains Mohali Sites
One image serves every Site, so a new Site is three small files: an inventory
entry, a vars file naming its web address and data root, and a config that seeds
its theme and first category list (baked into the image, which is why a new Site
needs a new tag).

Both reuse the directory their old static menus were served from, and the caddy
role drops the snippet the retired nginx-menu playbook wrote for each hostname,
so a deploy repoints each address at its own container with nothing else to do.

The seeded category lists are a starting point, not a decision: categories, names
and icons are all editable in each Site's own admin. A test now checks the
inventory, config and vars agree for every Site and that no config is orphaned,
because a half-added Site was the obvious way to get this wrong.
2026-09-23 00:09:36 +05:30
tanshu e64f65642f Add ESLint and Prettier
Prettier owns formatting: .prettierrc.json, format and format:check scripts, and
a one-off pass over the repo so the check is green from the start. Embedded
language formatting is off deliberately - the renderers build HTML in html
tagged templates and letting Prettier reflow those would change the markup
customers receive. docs/ is excluded because it is hand-wrapped prose, and shell
scripts have no Prettier parser.

ESLint covers the JavaScript: the two browser scripts, the build and check
scripts, and its own config. no-var is scoped out for the browser scripts on
purpose; they are plain ES5-shaped IIFEs and a hoisting difference would only
show up in a browser.

ESLint cannot lint the TypeScript here: typescript@7 is the native port with no
JavaScript compiler API, and typescript-eslint requires a 4.8-6.1 peer, so it
refuses to install and forcing it through would leave a parser that cannot read
the compiler. tsc --noEmit stays the TypeScript gate; README records the two
ways to change that.

lint and format:check now run in deploy.sh, and VS Code formats on save with
Prettier and surfaces ESLint findings.
2026-09-22 23:15:08 +05:30
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
tanshu b6f05f6828 Preload the fonts the design actually uses
The document still preloaded the Archivo file the old design used, which is now
gone: a 404 on every page load. It preloads Playfair Display and Plus Jakarta
Sans instead, and the asset test names a font that exists.
2026-09-22 23:07:59 +05:30
tanshu c5c0b7d005 Put the other menus in the top bar
The bar hid them behind a switcher and a sheet. It now names every menu that
exists, which is what was asked for; it scrolls sideways when the categories
outgrow the width, and the current one is the only filled item. Without the
script it is simply a row of links, so nothing is lost when JavaScript is off.
2026-09-22 22:35:38 +05:30
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
tanshu 7bf67cc299 Make reordering work on the page you are actually looking at
Two defects, both found by driving the real editor in Firefox:

1. The editor drew grab handles with a grab cursor on sets it refuses to
   reorder, so the live Food menu looked draggable and did nothing. Handles and
   the move buttons are now rendered only when reordering is possible, and the
   notice says what can still be done.

2. Reordering was saved only from dragend. Browsers suppress mouse events during
   a drag and dragend is not guaranteed to arrive, which left the rows visibly
   reordered while the server kept the old order — the drag looked like it
   worked and then reverted on reload. The order now commits from dragover after
   a pause in the drag, with drop/dragend/mouse events as faster paths, deduped
   against the last confirmed order and reported in the page.

To make (1) coherent the Current set may now be reordered while its Pages stay
fixed: order is presentation, media URLs are content-hashed and pages are
rendered per request, so nothing stale can be served (ADR 0006). Archives stay
frozen. Admin pages are no-store and assets cache for 5 minutes, so a fix like
this one is not masked by a stale admin.js.

scripts/check-dnd.mjs drives the real editor and asserts the drag persists; it
is the only automated check that reaches this path.
2026-09-22 22:01:52 +05:30
tanshu a635c5a214 Answer an oversized photo with the reason, not a 500
A file past the size limit makes the multipart stream itself throw, which
escaped the per-file catch and reached the error handler: the phone that just
sent a 35 MB photo got 'Something went wrong'. Catch the limit errors around the
part loop, say which limit was hit, and report it once rather than twice. Also
asserts that derivatives carry no EXIF while the Original keeps the uploaded
bytes.
2026-09-22 21:41:12 +05:30
tanshu af083415a7 Make the admin's buttons work in a browser
Every mutating form now carries the CSRF token as a hidden field, and the
upload route accepts it as a multipart field as well as a header. Driving the
real UI in Firefox found what the header-only tests could not: Create Draft,
Save, Publish, Delete and the up/down buttons all posted without a token and
were rejected. Adds npm run check-site, which verifies a running Site from the
outside and exits non-zero so it can gate a cutover.
2026-09-22 21:32:52 +05:30
tanshu bb9ddb437f Ship a repair command and a container
verify reports integrity problems and repair rebuilds derivatives from the
Originals, which is what makes the file layout trustworthy without a database.
The image runs TypeScript on Node 24 with no build step, writes only under
MENU_ROOT, and the provision script writes the untracked site.env using the
image's own hasher so the server needs no Node.
2026-09-22 21:24:47 +05:30
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
tanshu 9cb4021229 Serve the customer-facing menus
Landing page, Section pages and archived sets rendered per request from the
store, with the approved design: hand-written CSS, self-hosted Archivo, a
copper pour line in the Section rail, an admin-only Thumbnail and a text page
index. Media is keyed by Page id so URLs survive publishing, and a Draft's
media needs a session. Includes the scrypt admin auth and the login throttle
the media rule needs.
2026-09-22 21:18:49 +05:30
tanshu edea83228c Menu Set store, image pipeline and Site config
The store keeps a Menu Set's state in the directory layout (drafts/ versus
published/) and its metadata in a manifest, with publishing as a single
directory rename. Editing is allowed on Drafts only, so a published menu can
never change under a customer. Stages 2 and 3 of docs/plan.md, with tests.
2026-09-22 21:15:00 +05:30