Files
menu/docs/operating.md
T
tanshu 537a4f1e46 Match the deploy system to hops and greatbear
menu-admin was the odd one out: it built from the working directory with a date
tag and a multi-arch manifest, deployed with `-e tag=`, and kept the Docker-era
container name. The other two apps build from `git archive HEAD` and tag with the
commit, so what runs on the host is exactly what git knows about; build only for
the architecture emily actually runs; and use one slug for the container, its
network and its data directory.

The build is now their three lines: `podman build --platform linux/arm64` from
`git archive HEAD`, tagged `:<commit>` and `:latest`, pushed with `podman push`
rather than a manifest. `deploy.sh` pushes the commit first, then refuses a dirty
tracked tree or a HEAD that is not origin/main — which is what makes the commit
tag trustworthy — takes the same `--force` and `--no-push`, and ends in the same
`-u tanshu ... -e image_tag=<commit>`. Its own lint/typecheck/test gate stays,
but behind `--checks`, so a plain deploy behaves as theirs does.

The registry credential moves into vars/default.yml and the role logs in, pulls
and logs out around it, as their roles do. The image is only pulled at deploy
time, so the unit keeps podman's default `Pull=missing` and a reboot never needs
the registry. This is the same plaintext credential the other two repos carry.

One real bug goes with it: the healthcheck gate ran before the handler that
restarts a changed unit, so a run that only changed the unit checked the stale
container. `meta: flush_handlers` before the gate is the fix the sibling repos
already made.

The container, network and data directory are all menu-<site> now. The dotted
Docker-era name and the NetworkAlias it needed are gone, so `podman logs
menu-<site>` and `systemctl --user status menu-<site>` are the same two commands
across all three apps. The Dockerfile keeps its name for the same reason: podman
reads it as readily as a Containerfile, and the siblings use that name too.
2026-10-16 11:10:59 +05:30

137 lines
7.4 KiB
Markdown

# Operating the menus
For whoever is on the other end of "the menu is wrong". The design is in
`plan.md`, the vocabulary in `CONTEXT.md`, and the deployment in
`../ansible/readme.md`.
Each Site is one rootless Podman **Quadlet** unit on `emily` — a systemd *user*
service named `menu-<site>` — behind the shared Caddy unit. There is no Docker in
this path.
## Where things are
| Thing | Where |
| --------------- | ----------------------------------------------------------------------------------------- |
| The app | user unit `menu-<site>.service` on `emily`, container `menu-<site>`, image `registry.tanshu.com/menu-admin:<commit>` |
| Menu content | `~/podman-data/menu-<site>/data` — `originals/`, `drafts/`, `published/` |
| Admin | `https://<vhost>/admin` |
| The password | `~/podman-data/menu-<site>/site.env` (mode 600, never in git) |
| Categories | `data/sections.json`, edited in the admin |
| Brand and theme | `config/<site>.site.json` in this repo, baked into the image (the palette follows the Stitch system in `docs/design/stitch/`) |
Everything runs as the user `tanshu`, so every command below is run on `emily` as
that user. `sudo` is never needed, and never works for these units.
## Everyday tasks
**Publish a new menu.** Sign in at `/admin`, find the Section, fill in "New Menu
Set" (label, optional valid-until date), Create Draft, then drop the photos in.
Drag to reorder — the order in the editor is the order customers see. Press
Preview to check it, then Publish. Publishing archives whatever was current.
**Put an older menu back.** Open the Section's "older menus" fold in the admin,
open the set you want and press "Make current". Its URL does not change.
**Change or reset the admin password.** Run the provision script again with
`--force`: it overwrites the password hash, and generates a fresh session secret,
which signs out anyone who was signed in. Nothing else changes.
```sh
ssh -t emily bash /tmp/provision-site-env.sh ~/podman-data/menu-hopsngrains --force
ssh emily systemctl --user restart menu-hopsngrains
```
There is no minimum length and no password policy — the one rule is that the
password cannot be empty. It can also be set from a machine that has this repo
checked out, with the prompt on that machine rather than the server:
```sh
cd ~/programming/menu-admin
read -r -s -p 'Admin password: ' PW; echo
HASH=$(printf '%s' "$PW" | node -e "let s='';process.stdin.setEncoding('utf8');process.stdin.on('data',c=>s+=c).on('end',async()=>{const m=await import('./src/auth.ts');process.stdout.write(await m.hashPassword(s))})")
SECRET=$(node -e "process.stdout.write(require('node:crypto').randomBytes(32).toString('base64url'))")
printf 'ADMIN_PASSWORD_HASH=%s
SESSION_SECRET=%s
' "$HASH" "$SECRET" | ssh emily "podman run --rm -i --userns=keep-id:uid=1000,gid=1000 -v ~/podman-data/menu-hopsngrains:/data --entrypoint sh registry.tanshu.com/menu-admin:<commit> -c 'umask 077 && cat > /data/site.env'"
ssh emily systemctl --user restart menu-hopsngrains
```
**Add, rename, reorder or remove a category.** In the admin, open the
**Categories** fold at the bottom of the dashboard. A new category starts empty,
so customers do not see it until you publish a menu into it. Reordering there is
the order customers see. Two rules: a category's web address is fixed when it is
created (rename the name as often as you like — a printed QR code keeps working),
and a category can only be removed once it holds no menus.
**Change the restaurant's name or tagline, the icon, or the published dates.** In
the admin, open the **Site identity** fold: the name and tagline customers see,
a checkbox for whether a menu says when it was published, and an SVG upload for
the icon (under 256 KB; the upload replaces the bundled mark and "Remove the
uploaded icon" brings it back). None of this changes your web addresses.
**Change the colours.** Those are still `config/<site>.site.json`, so they need a
new build and push (`./deploy.sh`).
**Fix a missing image.** If a Page's thumbnail or viewing image is lost while its
Original survives, the app can rebuild them:
```sh
podman run --rm --userns=keep-id:uid=1000,gid=1000 -e MENU_ROOT=/data \
-e MENU_SITE_CONFIG=/app/config/hopsngrains.site.json \
-v ~/podman-data/menu-hopsngrains:/data \
--entrypoint node registry.tanshu.com/menu-admin:<commit> src/cli/repair.ts
```
`src/cli/verify.ts` does the same inventory without changing anything, and is
what the server refuses to boot on. Both need the `--userns=keep-id` mapping, or
they would see the files as a different user and fail to write.
## When something looks wrong
| Symptom | Look here |
| --- | --- |
| Landing page says the menus are being updated | no Section has a Current Menu Set — publish one in the admin |
| A Section 404s | that Section has no Current set, or the URL is misspelled |
| Sign-in rejects the right password | `podman logs menu-<site>`, then check `site.env` exists and is owned by `tanshu` |
| Admin is missing entirely | `site.env` was unreadable at boot, so the admin is disabled: run the provision script and `systemctl --user restart menu-<site>` |
| A photo uploads but looks wrong | the browser shrinks to 3000px before upload; that is expected, and the Original is kept |
| The container will not start | `systemctl --user status menu-<site>` and `podman logs menu-<site>` — a fatal integrity problem makes it refuse to serve a data directory it cannot trust |
| The unit is "not found" | its `.network` file is missing, so Quadlet refused to generate it: check `~/.config/containers/systemd/` |
| Caddy reload failed during a deploy | nothing is down: Caddy validates before applying. Read the error in the playbook output |
| Everything is slow | check the tag is current; a Viewing image is ~1600px, so a Section page should not be megabytes |
**Logs.** `podman logs -f menu-<site>`, or `journalctl --user -u menu-<site>
-f` for the unit. The app logs JSON lines, one per event, including every upload,
publish, restore and failed sign-in.
**Restarting.** `systemctl --user restart menu-<site>`. A *reload* of the Caddy
config is `podman exec caddy caddy reload --config /etc/caddy/Caddyfile`, which
does not drop the other sites.
**Checking from outside.** No sign-in, no writes, exits non-zero on failure, so
it can gate a cutover:
```sh
npm run check-site -- https://menu.hopsngrains.com
```
## Backups
The app is the only writer. The irreplaceable part is
`~/podman-data/menu-<site>/data/originals` — the photos exactly as uploaded, from
which everything else can be regenerated with `repair`. Back that directory up;
`site.env` is worth keeping too (otherwise you re-provision the password).
Because of `keep-id`, everything there is owned by `tanshu`, so a plain
`tar`/`rsync` of `~/podman-data/menu-<site>` is complete — no subuid-owned files
to trip over and no `podman unshare` needed.
## Rolling back
Deploy the previous tag — `./deploy.sh <previous-tag>` — which rebuilds the Site
at that version. `podman logs menu-<site>` is where a container that will not
start says why.
The Docker-era container was left on `monoco`, stopped and not removed, so the
last resort is to start it again and point Caddy back at it.