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.
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.