Files
hops/README.md
T
tanshu 843c8ba78a Build with Podman and deploy as a rootless Quadlet on emily
- Makefile: podman build/push from the committed tree (git archive HEAD),
  tagged :<commit> + :latest; PLATFORM defaults to linux/arm64 for emily.
- deploy.sh: push the commit first, refuse a dirty tracked tree or
  HEAD != origin/main, then build and deploy that exact commit via
  -e image_tag (rollback = an older commit).
- ansible: target emily with no become. The register role writes hops.network
  (NetworkName=hops_net) plus the caddy.container.d drop-in; the hops role
  installs ~/podman-data/hops and hops.container under systemctl --user with a
  podman healthcheck gate; the caddy role appends the site blocks to the shared
  Caddyfile and reloads.
- Drop the Docker-era network role, community.docker requirements,
  ansible/files templates and docker-compose.yml.
- .containerignore replaces .dockerignore.
- README: Podman build, rootless Quadlet deploy on emily.
2026-10-16 10:35:57 +05:30

176 lines
8.2 KiB
Markdown

# Hops n Grains Microbrewery
A modern, full-stack craft microbrewery web application built with **TanStack Start** for the frontend and **PocketBase** for the backend, faithfully implementing the Google Stitch **Heritage & Harvest** tactile industrial design system.
---
## Architecture Overview
```
hops/
├── backend/
│ ├── pocketbase # Linux AMD64 executable (WSL)
│ ├── pocketbase.exe # Windows AMD64 executable (PowerShell)
│ ├── pb_migrations/ # Database schema migrations & initial seeds
│ │ └── 1788363300_initial_schema_and_seed.js
│ └── pb_data/ # SQLite database & data storage
├── frontend/ # TanStack Start application
│ ├── src/
│ │ ├── routes/
│ │ │ ├── __root.tsx # Root layout, fonts, nav, footer, reservation context
│ │ │ ├── index.tsx # Home page (Google Stitch cinematic hero & craft overview)
│ │ │ ├── brews.tsx # Craft Brews page (Google Stitch menu with IBU/ABV filters)
│ │ │ ├── food.tsx # Global Comfort Food menu with beer pairing chips
│ │ │ ├── brewery.tsx # The Brewery & Heritage story and locations
│ │ │ └── live.tsx # Live On Tap draft board with keg telemetry
│ │ ├── components/
│ │ │ ├── Navbar.tsx # Frosted glass top bar with mobile menu drawer
│ │ │ ├── Footer.tsx # Dark industrial footer with newsletter signup
│ │ │ ├── BeerCard.tsx # Tactile craft beer card with IBU/ABV meters & pairings
│ │ │ ├── FoodCard.tsx # Food card with veg/non-veg tags & beer pairing
│ │ │ ├── ReservationModal.tsx # Interactive table reservation modal connected to PocketBase
│ │ │ └── TapHandle.tsx # Custom copper brewing handle slider from DESIGN.md
│ │ ├── lib/
│ │ │ └── pocketbase.ts # PocketBase SDK client with fallback data resilience
│ │ └── styles.css # Tailwind theme tokens, Playfair Display, Montserrat, textures
│ └── package.json
├── ansible/ # Deployment playbook (roles: register, hops, caddy)
├── Dockerfile # Multi-stage production container build
├── .containerignore # Build-context excludes (Podman/Buildah)
├── .gitignore # Git repository ignore rules
├── docker-entrypoint.sh # Process supervisor running PB & Nitro
├── Makefile # Podman build targets (build-production / build-check)
├── deploy.sh # Build & push image, then run the Ansible deploy
├── package.json # Monorepo management scripts
└── README.md
```
---
## Running Locally
### 1. Start Both Frontend & Backend Concurrently
```bash
npm run dev
```
Or start them individually:
**Backend (PocketBase):**
```bash
# In WSL / Linux:
cd backend && ./pocketbase serve --http 127.0.0.1:8090
# In Windows PowerShell:
cd backend ; .\pocketbase.exe serve --http 127.0.0.1:8090
```
- API Endpoint: `http://127.0.0.1:8090`
- PocketBase Admin UI: `http://127.0.0.1:8090/_/`
- **Email:** `admin@hopsngrains.com`
- **Password:** `HopsPassword1234!`
**Frontend (TanStack Start):**
```bash
npm run dev:frontend
```
- Web Application: `http://localhost:3000`
---
## Deploying
The image is built with **Podman/Buildah** (not Docker) from the committed tree
(`git archive HEAD`) and tagged with its **commit id**, so what runs on the host
is exactly what git knows about. `deploy.sh` pushes the commit first, refuses a
dirty tracked tree and requires `HEAD == origin/main`. Log in to the registry
once per machine: `podman login registry.tanshu.com`.
```bash
./deploy.sh # push, build <commit>, deploy it to emily
./deploy.sh --no-push # skip the push (you already pushed)
./deploy.sh --force # skip the guards
```
Or run the pieces yourself:
```bash
make build-production # podman build HEAD, push :<commit> + :latest
make build-check # compile check from HEAD, no push
cd ansible && ansible-playbook -u tanshu playbook.yml -e "image_tag=$(git rev-parse --short HEAD)"
```
The image is built for **linux/arm64** by default because `emily` is aarch64.
Podman has no built-in QEMU, so cross-building from an x86_64 controller needs
binfmt there (`qemu-user-binfmt`); override with `PLATFORM=linux/amd64`.
To run the production image locally instead:
```bash
podman build -t localhost/hops:dev .
podman run --rm -p 3000:3000 -p 8090:8090 \
-e PB_ADMIN_EMAIL=admin@hopsngrains.com -e PB_ADMIN_PASSWORD=HopsPassword1234! \
-v hops_dev_data:/app/pb_data localhost/hops:dev
```
### Ansible deployment
Requirements on the controller: `ansible` (no collections — the play uses
built-in modules plus the `podman` / `systemctl --user` CLI). The target host
must already run the fleet's shared Caddy **rootless Quadlet** setup: the
`caddy/playbook.yml` from the `ansible-playbooks` repo owns the Caddy unit, the
rootless prerequisites (Podman, `loginctl enable-linger`, the low-port sysctl)
and the Caddyfile this play appends to.
The play targets the inventory host `emily` (present in `/etc/ansible/hosts`),
connects as the unprivileged container user and **does not `become`**: the
Quadlet units belong to that user, and the SSH connection's PAM session is what
makes `systemctl --user` work.
```bash
cd ansible
ansible-playbook -u tanshu playbook.yml -e "image_tag=<commit>"
```
All deployment variables live in `ansible/vars/default.yml` (registry, image
tag, domains, PocketBase admin credentials). The play:
1. Writes `~/.config/containers/systemd/hops.network`
(`NetworkName=hops_net`) and a drop-in that attaches the shared Caddy unit to
it, so Caddy can resolve `hops` by container name.
2. Logs in to the private registry, pulls `registry.tanshu.com/hops:{image_tag}`
(rollback is the same play with an older commit), writes
`~/podman-data/hops/.env`, installs
`~/.config/containers/systemd/hops.container`, starts it as a **systemd user
service** and waits for the image's healthcheck (PocketBase `/api/health` +
the site).
3. Appends the site blocks to the shared Caddyfile
(`~/podman-data/caddy/conf/Caddyfile`) and reloads Caddy:
- `www.hopsngrains.com` (+ apex `hopsngrains.com`) — frontend on port 3000,
PocketBase API/admin proxied under `/api/*` and `/_/*`
- `admin.hopsngrains.com` — redirects to the PocketBase admin UI
Nothing is published on the host; Caddy reaches the container over `hops_net`.
The container runs as uid 0, so the unit maps it onto the podman user with
`UserNS=keep-id:uid=0,gid=0`: `~/podman-data/hops` stays owned by `tanshu`, and
PocketBase's files need no `podman unshare` to inspect or back up.
DNS records for all three hosts must point at `emily`; Caddy obtains TLS
certificates automatically on first request. This deploy starts from empty
data — the schema migration in `backend/pb_migrations/` is applied on first
start, so the seed beers and food are recreated automatically.
---
## Features & Pages
| Route | Name | Highlights |
|---|---|---|
| `/` | **Home** | Cinematic hero matching Google Stitch mockup, brewery pillars, signature craft beer preview, call-to-action buttons. |
| `/brews` | **Our Brews** | Interactive craft beer catalog matching Stitch design, style filters (Hefeweizen, Lager, IPA, Dark Ale, Cider), and custom copper **Tap Handle** IBU slider. |
| `/food` | **Global Comfort Food** | Handcrafted dining menu (Wood-fired pizzas, smash burgers, truffle fries) categorized with recommended craft beer pairings and veg toggles. |
| `/brewery` | **The Brewery** | 4-step industrial brewing science walkthrough (Mashing, Boiling, Fermentation, Cellar Pouring), and taproom details for Chandigarh & Panchkula. |
| `/live` | **Live On Tap** | Real-time draft board tracking active tap numbers, ABV, IBU, and live remaining keg volume percentages. |
| **Modal** | **Table Reservation** | Interactive booking form for Sector 9 Chandigarh or Sector 5 Panchkula, submitting live to PocketBase. |