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

7.4 KiB

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.

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:

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:

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:

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.