Files
greatbear/README.md
T
tanshu bea6710d38 Menu schema v3: variants as separate products, POS-ready fields
- variants split into their own menu_items whenever price differs
  (name + variant fields; 164 products, 24 variant groups)
- price back to number (whole rupees); no more '449/549' strings
- diet select (veg/nonveg/mixed) instead of nullable bool — PocketBase
  serializes empty bools as false, which misfiled mixed dishes
- new fields for POS: sku (unique GB-CAT-NNN), available (86 toggle),
  tax_category (default food-5-gst)
- migration 1788500100_menu_items_v3 replaces menu_items; resync via
  scripts/update-menu.mjs
- cards render 'Dish – Variant', badges from diet, hide unavailable
2026-09-03 18:39:00 +05:30

8.9 KiB

The Great Bear — Microbrewery Website

Full-stack site for The Great Bear microbrewery (SCO 32, Sector 26, Chandigarh), built from the Stitch "Industrial Hospitality" design.

Layer Tech
Frontend TanStack Start (React 19, Vite 8, Tailwind CSS 4, SSR)
Backend PocketBase v0.40 — CMS content, form intake, admin UI
Runtime Single Docker image running both services
Proxy Caddy (automatic HTTPS) on the target host
Deploys deploy.sh [tag] (Makefile buildx push) → Ansible playbook

Pages

  • / — hero, stats, featured brews, experiences, testimonials, quick table booking
  • /our-brews — flagship banner, core lineup, seasonal releases, tasting-flight builder
  • /menu — filterable gastropub menu with beer pairings
  • /visit — reservation form, contact info, map

All structured content (brews, menu, testimonials, stats, contact info) is served from PocketBase and editable in the admin UI. If PocketBase is unreachable, the site falls back to bundled seed content so it never renders empty.

Repository layout

app/                 TanStack Start application
  src/routes/        Pages (/, /our-brews, /menu, /visit)
  src/components/    Shared UI + client forms
  src/lib/           Server functions & PocketBase client
  src/data/content.json   Single source of truth for seed + fallback content
  public/images/     Design assets (downloaded from the Stitch export)
pb/migrations/       PocketBase schema (snapshot migration, auto-applied)
scripts/
  seed.mjs           Idempotent content seeder (create-if-missing by uid)
  create-collections.sh   Dev helper used to build the schema migration
ansible/             Deployment playbook (roles: network, greatbear, caddy)
Makefile             Multi-arch buildx targets (build-production / build-check)
Dockerfile           Multi-stage image build (deps → builder → pb-downloader → runner)
docker-entrypoint.sh Container supervisor: PocketBase + site server
deploy.sh            Build & push image, then run the Ansible deploy

Local development

Frontend only (renders from bundled seed content; forms return errors):

cd app
npm install
npm run dev          # http://localhost:3000

Full stack with PocketBase (recommended — from the repo root):

# Terminal 1 — PocketBase on :8090 (data in ./pb_data, schema auto-applied
# from pb/migrations on first boot)
./.tmp-pb/pocketbase serve --http=127.0.0.1:8090 --dir=./pb_data --migrationsDir=./pb/migrations

# Terminal 2 — one-time setup: create a local admin and seed the content
./.tmp-pb/pocketbase superuser upsert dev@greatbear.in devpass12345 --dir=./pb_data --migrationsDir=./pb/migrations
PB_URL=http://127.0.0.1:8090 PB_SUPERUSER_EMAIL=dev@greatbear.in PB_SUPERUSER_PASSWORD=devpass12345 \
  node scripts/seed.mjs

# Terminal 2 — the site (talks to PocketBase on 127.0.0.1:8090 automatically)
cd app && npm run dev
  • PocketBase admin UI: http://127.0.0.1:8090/_/ (dev@greatbear.in) — edits show up on the site on the next page load
  • Dev data lives in ./pb_data (git-ignored); delete it for a factory reset
  • The PB binary in .tmp-pb/ was fetched during setup — if it's missing, grab a release from https://github.com/pocketbase/pocketbase/releases (or run PocketBase however you like; only the port matters)

npm run build produces .output/server/index.mjs, started with npm start. To run the production image locally instead:

docker build -t greatbear:dev .
docker run --rm -p 3000:3000 -p 8090:8090 \
  -e PB_SUPERUSER_EMAIL=dev@greatbear.in -e PB_SUPERUSER_PASSWORD=devpass12345 \
  -v greatbear_dev_data:/app/pb_data greatbear:dev

Deploying

The image is always built from the committed state of the git remote — commit and push before deploying. Registry credentials: docker login registry.tanshu.com (once per machine).

./deploy.sh              # build + push :latest, then deploy that tag via Ansible
./deploy.sh v1.2.3       # same, with an explicit version tag

Or run the pieces yourself:

make build-production TAG=v1.2.3   # multi-arch (amd64+arm64) buildx build & push
make build-check                   # multi-arch compile check (no push)
make build-check-local             # build the committed local tree (git archive) and load it

cd ansible && ansible-playbook playbook.yml -e "tag=v1.2.3"

Ansible deployment

Requirements on the controller: ansible + community.docker collection (see ansible/requirements.yml). Requirements on the host: Docker with a Caddy container named caddy (config at /var/lib/caddy/conf/Caddyfile).

cd ansible
ansible-galaxy install -r requirements.yml
ansible-playbook playbook.yml    # targets inventory host `monoco` — edit playbook.yml if needed

All deployment variables live in ansible/vars/default.yml (registry, tag, domains, PocketBase admin credentials). The playbook:

  1. Ensures the greatbear_net Docker network exists with the Caddy container attached.

  2. Pulls registry.tanshu.com/tanshu/greatbear:{tag}, uploads /var/lib/greatbear/.env, and (re)creates the greatbear container on that network with /var/lib/greatbear/pb_data bind-mounted to /app/pb_data, then waits for its healthcheck.

  3. Inserts a managed snippet into the shared Caddyfile and reloads Caddy via docker exec:

    • www.greatbear.in (+ apex greatbear.in) — frontend on port 3000, PocketBase API/admin proxied under /api/* and /_/*
    • admin.greatbear.in — redirects to the PocketBase admin UI

No host ports are published; Caddy reaches the containers over the Docker network.

DNS records for all three hosts must point at the server; Caddy obtains TLS certificates automatically on first request.

PocketBase admin

  • URL: https://admin.greatbear.in/_/
  • First container start bootstraps the superuser from PB_SUPERUSER_EMAIL / PB_SUPERUSER_PASSWORD (templated from pb_admin_email / pb_admin_password in ansible/vars/default.yml, idempotent).

Content collections: brews, menu_categories, menu_items, experiences, testimonials, stats, pairings, settings. Reservation/contact submissions land in reservations (public create only — readable by admins).

The seed only creates records that are missing (matched by uid), so edits made in the admin are never overwritten by redeploys. Schema changes belong in pb/migrations/.

Updating the food menu

The food menu is managed in code: app/src/data/menu-data.mjs (11 categories, ~137 items, single source for the PocketBase sync and the site's offline fallback).

# 1. Edit app/src/data/menu-data.mjs, commit & push, deploy (if the image changed)
# 2. Sync the menu into PocketBase — from anywhere with admin credentials:
PB_URL=https://admin.greatbear.in \
PB_SUPERUSER_EMAIL=admin@greatbear.in \
PB_SUPERUSER_PASSWORD=... \
node scripts/update-menu.mjs

The script replaces all menu_categories / menu_items records (deletes old, inserts new) and touches nothing else.

menu_items schema (POS-oriented)

  • uid — stable slug identity
  • category — slug → menu_categories
  • name / variant — variants are separate products whenever their price differs ("Penne Arrabiata" + variant "Veg" ₹589 / "Chicken" ₹639); the POS can group by name
  • description
  • price — number, whole rupees (449 = ₹449)
  • diet — select: veg / nonveg / mixed (a bool can't be null in PocketBase, so mixed items like Caesar Salad use mixed; the site renders no badge for it)
  • pairing — optional beer pairing
  • tag — badge chip; also carries same-price choice lists (e.g. "Malai / Angara")
  • image — optional URL
  • sku — unique POS code, GB-<CAT>-<NNN> (e.g. GB-MNC-014)
  • available — false hides the item from the site (86'd) without deleting it
  • tax_category — default food-5-gst; adjust per item for POS GST mapping

Admin edits to menu records are overwritten by the next sync — treat menu-data.mjs as the menu's source of truth.

Container environment

Variable Default Purpose
PORT 3000 Site server port
HOST 0.0.0.0 Site server bind address
PB_URL http://127.0.0.1:8090 PocketBase URL used by the site server
PB_DATA_DIR /app/pb_data PocketBase data directory (volume)
PB_SUPERUSER_EMAIL Initial admin (bootstrapped at start)
PB_SUPERUSER_PASSWORD Initial admin password