There is no previous system kept serving, so the playbook no longer knows about one: the retire flags, the temporary web address, the task that removed the old nginx snippet and the task that stopped the old container are gone. One run starts the container and points the Site's web address at it, and deploying an earlier tag is the rollback. The vars lose vhost, retire_nginx_snippet, retire_nginx_container and legacy_container; the Caddy role keeps only the snippet upsert. The deployment readme drops its staged-rollout sections and the glossary of words that no longer exist, and the plan's sequence and the operating notes describe the single run instead.
162 lines
8.0 KiB
Markdown
162 lines
8.0 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 # format, types, tests, podman build+push, deploy
|
|
SKIP_DEPLOY=1 ./deploy.sh 2026-09-22 # build and push only
|
|
```
|
|
|
|
The image is built with **podman** as a multi-arch manifest and pushed to
|
|
`registry.tanshu.com/menu-admin` (`Makefile`, mirrored from the other apps on
|
|
this machine). The Ansible deployment lives in `ansible/` in this repo, with one
|
|
inventory entry per Site; see `ansible/readme.md` for how a deploy runs and how to
|
|
roll one back.
|
|
|
|
## 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/identity.ts Name, tagline, icon and display switches, 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
|
|
```
|
|
|
|
### Linting and formatting
|
|
|
|
```sh
|
|
npm run lint # oxlint
|
|
npm run lint:fix
|
|
npm run format # prettier --write
|
|
npm run format:check # what deploy.sh runs
|
|
```
|
|
|
|
**oxlint** lints the TypeScript and the JavaScript together. This project's
|
|
`typescript@7` is the native port with no JavaScript compiler API, which every
|
|
ESLint TypeScript parser needs — `typescript-eslint` requires a
|
|
`>=4.8.4 <6.1.0` peer and refuses to install — and oxlint parses TypeScript
|
|
without one, so it is the linter that works here. It runs the correctness
|
|
category and takes about 20 ms over the whole repo. `.oxlintrc.json` turns four
|
|
opinionated rules off, and oxlint's config takes no comments, so the reasoning
|
|
lives here: `no-await-in-loop` because the loops that await are sequential on
|
|
purpose (one photo at a time, memory bounded); `unicorn/consistent-function-scoping`
|
|
because the helpers close over their route's dependencies; `unicorn/no-array-sort`
|
|
because `toSorted()` is worth adopting but that is a refactor rather than a bug;
|
|
and `oxc/no-map-spread` because clarity beats the micro-optimisation. Delete the
|
|
line to see what a rule would say. `npm run typecheck` remains the
|
|
deeper TypeScript gate (strict, with `noUnusedLocals`, `noUncheckedIndexedAccess`,
|
|
`exactOptionalPropertyTypes` and `erasableSyntaxOnly`).
|
|
|
|
**Prettier** owns formatting (`.prettierrc.json`); `docs/` is excluded because
|
|
those are hand-wrapped notes rather than code. `embeddedLanguageFormatting` is
|
|
off on purpose: the renderers build HTML inside `html` tagged template literals,
|
|
and letting Prettier reflow those would change the markup customers receive.
|
|
|
|
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.
|