From cd7f99f59dad70ca071deea13df715e87341cd5b Mon Sep 17 00:00:00 2001 From: Richie Date: Mon, 20 Apr 2026 17:22:40 +1000 Subject: [PATCH] Wire demo for production deploy + add server config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes images 404'ing under /arrangement/ — Vite's publicDir copies assets to the build root, but the base prefix is only applied to bundled assets (JS/CSS), not to runtime URL strings. assetUrl() helper resolves paths against import.meta.env.BASE_URL so '/images/foo.png' becomes '/arrangement/images/foo.png' in production while staying '/images/foo.png' in dev. - src/demo/shared/assets.ts — assetUrl() helper - providers.ts + DemoNav.tsx — wrap all public asset paths - nginx/parsons-demos.conf — swag site-conf for parsons.tensordesign.com.au (asset cache regex above SPA fallback regex per nginx first-match rule) - docs/reference/client-demo-deploy.md — server runbook (DNS, swag SUBDOMAINS, mount, htpasswd, deploy loop) Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/reference/client-demo-deploy.md | 177 ++++++++++++++++++++++++++ nginx/parsons-demos.conf | 81 ++++++++++++ src/demo/apps/arrangement/DemoNav.tsx | 5 +- src/demo/shared/assets.ts | 17 +++ src/demo/shared/fixtures/providers.ts | 21 +-- 5 files changed, 289 insertions(+), 12 deletions(-) create mode 100644 docs/reference/client-demo-deploy.md create mode 100644 nginx/parsons-demos.conf create mode 100644 src/demo/shared/assets.ts diff --git a/docs/reference/client-demo-deploy.md b/docs/reference/client-demo-deploy.md new file mode 100644 index 0000000..ae9f98c --- /dev/null +++ b/docs/reference/client-demo-deploy.md @@ -0,0 +1,177 @@ +# Client demo deploy — runbook + +How to set up `parsons.tensordesign.com.au` for the first time, and how to push updates after. + +Companion to [`client-demo-hosting-plan.md`](./client-demo-hosting-plan.md), which has the why and the architecture. This file is the actionable how. + +--- + +## One-time server setup + +You'll do this once. After that, deploys are a single script. + +### 1. DNS + +Point `parsons.tensordesign.com.au` at your home IP (or DDNS hostname). One A-record, same as your other subdomains on `tensordesign.com.au`. + +Verify after propagation (can take minutes): + +```bash +dig +short parsons.tensordesign.com.au +``` + +Should return your home IP. + +### 2. swag — add `parsons` to SUBDOMAINS + +In your swag container's environment (compose file or `docker run` flags), add `parsons` to the comma-separated `SUBDOMAINS` list. Then restart the container: + +```bash +docker compose restart swag +# or: docker restart swag +``` + +Watch the logs until you see Let's Encrypt issue the cert: + +```bash +docker logs -f swag +# look for: "Certificate for parsons.tensordesign.com.au issued" +``` + +### 3. Document root on the host + +Pick where the static files live on the host filesystem. Suggested: + +```bash +sudo mkdir -p /srv/parsons-demos +sudo chown -R "$USER:$USER" /srv/parsons-demos +``` + +Make sure swag has access to it. Either: + +- **Mount it into swag** at `/config/www/parsons-demos/` (preferred — keeps swag's container view tidy): + + ```yaml + # in your swag compose service: + volumes: + - /srv/parsons-demos:/config/www/parsons-demos:ro + ``` + + Restart swag after editing compose. + +- Or symlink inside the existing swag config volume — works but messier. + +### 4. Drop the nginx config in + +The repo has the conf at `nginx/parsons-demos.conf`. Copy it into swag's `site-confs/` directory: + +```bash +cp nginx/parsons-demos.conf /path/to/swag/config/nginx/site-confs/ +docker compose exec swag nginx -t # syntax check +docker compose exec swag nginx -s reload +``` + +If `nginx -t` complains, fix before reloading (a bad config will take swag down). + +### 5. Create the htpasswd + +Pick a username (suggestion: `client`) and a strong shared password: + +```bash +docker compose exec swag htpasswd -c /config/nginx/.htpasswd-parsons client +# prompts for password +``` + +For additional users later (e.g. one credential per client engagement), drop the `-c`: + +```bash +docker compose exec swag htpasswd /config/nginx/.htpasswd-parsons another-user +``` + +### 6. Verify the auth + 404 + +Visit `https://parsons.tensordesign.com.au/` in a fresh browser. You should see: + +1. SSL is valid (no cert warning) +2. Browser asks for username + password +3. After auth: empty page or 404 (no slices deployed yet — that's fine) + +If you see this far, the server is ready. + +--- + +## Per-deploy workflow + +Once setup is done, the loop is two commands: + +```bash +# 1. Build the slice +npm run demo:build -- --mode arrangement + +# 2. Push it up +./scripts/deploy-demo.sh arrangement +``` + +The deploy script: + +- Verifies `dist-demo/arrangement/` exists and isn't empty (aborts if not — won't rsync a half-built bundle over a working demo) +- `rsync -az --delete` to the server (removes stale asset hashes) +- Prints the URL to visit + +**Before first deploy:** edit `scripts/deploy-demo.sh` and set: + +- `TARGET_HOST="@tensordesign.com.au"` +- `TARGET_BASE="/srv/parsons-demos"` (or wherever you put the document root) + +Make sure SSH key auth works (`ssh "$TARGET_HOST" echo ok` should succeed without a password prompt) so rsync doesn't stall. + +The script lives in `scripts/` which is gitignored, so your server-specific paths won't leak into the repo. + +--- + +## Adding a second slice later + +1. Build the new app under `src/demo/apps//` (mirror `arrangement/`'s structure). +2. `npm run demo:build -- --mode `. +3. `./scripts/deploy-demo.sh `. + +The nginx config catches `//...` automatically — no server changes needed for new slices. + +--- + +## Optional: landing page at `/` + +Until you have one, `https://parsons.tensordesign.com.au/` returns 404. To add a tiny index listing available slices: + +```bash +cat > /srv/parsons-demos/index.html <<'EOF' + + +Parsons demos + +

Parsons demos

+ + + +EOF +``` + +Add `
  • ` entries as new slices ship. + +--- + +## Troubleshooting + +**Cert didn't issue.** Check swag logs (`docker logs swag`). Common causes: DNS not propagated yet, port 80 blocked by router, `SUBDOMAINS` value missing or misspelled. + +**`nginx -t` fails after dropping the conf.** Most likely a path mismatch — `/config/www/parsons-demos` doesn't exist inside the container because the bind mount isn't set. Check `docker compose config` to confirm the mount is in effect. + +**Auth prompt loops / 401s after correct password.** htpasswd file path mismatch between conf and `htpasswd -c` location. Both must agree. + +**Demo loads but assets 404.** Vite `base` path mismatch. The build must use `--mode ` so assets are prefixed with `//`. Re-run the build and check `dist-demo//index.html` — script src should look like `//assets/index-XXX.js`. + +**`rsync` stalls or asks for password.** SSH key auth not set up. Run `ssh-copy-id ` once. + +**Want to roll back a deploy.** rsync with `--delete` is irreversible — there's no built-in undo. Either keep the previous build locally and re-deploy, or rebuild the previous git commit. For demo-grade work this is fine; if you need versioned deploys later, switch to dated subfolders + a symlink swap. diff --git a/nginx/parsons-demos.conf b/nginx/parsons-demos.conf new file mode 100644 index 0000000..c487dc9 --- /dev/null +++ b/nginx/parsons-demos.conf @@ -0,0 +1,81 @@ +# Parsons demo host — drop into swag's /config/nginx/site-confs/ directory. +# +# Serves static demo slices at parsons.tensordesign.com.au// behind +# basic auth. One server block, one cert (Let's Encrypt via swag), one +# htpasswd covering all slices. +# +# Document root layout (host filesystem): +# /parsons-demos/ +# index.html ← optional landing page listing slices +# arrangement/ +# index.html +# assets/... +# / +# +# Bind-mount that directory into swag at /config/www/parsons-demos/ — the +# `root` directive below assumes that path. Adjust if you mount elsewhere. + +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + + server_name parsons.*; + + # swag manages the cert chain via SUBDOMAINS — make sure `parsons` is in + # the SUBDOMAINS env var of the swag container so this resolves. + include /config/nginx/ssl.conf; + + root /config/www/parsons-demos; + index index.html; + + # One credential file covering every slice. Create with: + # docker exec -it swag htpasswd -c /config/nginx/.htpasswd-parsons client + auth_basic "Parsons demos"; + auth_basic_user_file /config/nginx/.htpasswd-parsons; + + # Optional: don't auth the root listing if you want it publicly visible. + # (Currently auth covers it too — change to `auth_basic off;` to expose.) + + # Root path serves the optional landing index.html if present, else 404. + location = / { + try_files /index.html =404; + } + + # Long cache for fingerprinted assets — Vite produces hashed filenames so + # this is safe. HTML is short-cache so updates land on next refresh. + # NOTE: asset + html regex locations must come BEFORE the slice fallback + # below, because nginx uses the first matching regex location. + location ~* \.(?:js|css|woff2?|ttf|otf|eot|png|jpg|jpeg|gif|svg|webp|ico)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + } + + location ~* \.html$ { + expires -1; + add_header Cache-Control "no-cache, must-revalidate"; + } + + # SPA fallback per slice. // resolves to that + # slice's index.html so React Router handles the rest. Static assets + # (.js/.css/.png/etc.) are handled by the regex blocks above. + location ~ ^/(?[^/]+)/ { + try_files $uri $uri/ /$slice/index.html; + } + + # Hide hidden files (e.g. .htpasswd if it ever ends up in webroot) + location ~ /\. { + deny all; + } +} + +# HTTP → HTTPS redirect — swag's default server already covers this for +# wildcard subdomains, but include explicitly here in case the default is +# customised. +server { + listen 80; + listen [::]:80; + server_name parsons.*; + return 301 https://$host$request_uri; +} diff --git a/src/demo/apps/arrangement/DemoNav.tsx b/src/demo/apps/arrangement/DemoNav.tsx index cb79bb8..a9c1f2d 100644 --- a/src/demo/apps/arrangement/DemoNav.tsx +++ b/src/demo/apps/arrangement/DemoNav.tsx @@ -1,17 +1,18 @@ import Box from '@mui/material/Box'; import { Navigation } from '../../../components/organisms/Navigation'; +import { assetUrl } from '../../shared/assets'; const FALogo = () => ( diff --git a/src/demo/shared/assets.ts b/src/demo/shared/assets.ts new file mode 100644 index 0000000..e05e305 --- /dev/null +++ b/src/demo/shared/assets.ts @@ -0,0 +1,17 @@ +/** + * Resolve a public-asset path against Vite's base URL. + * + * In dev `import.meta.env.BASE_URL === '/'`, so `assetUrl('/images/foo.png')` + * returns `/images/foo.png` unchanged. In production the build sets base to + * `/arrangement/` (or whatever `--mode ` was passed), and the same + * call returns `/arrangement/images/foo.png` so the bundled assets resolve + * correctly under the slice subpath. + * + * Always pass leading-slash paths — they're relative to the publicDir root. + */ +export const assetUrl = (path: string): string => { + const base = import.meta.env.BASE_URL; + const cleanBase = base.endsWith('/') ? base.slice(0, -1) : base; + const cleanPath = path.startsWith('/') ? path : `/${path}`; + return `${cleanBase}${cleanPath}`; +}; diff --git a/src/demo/shared/fixtures/providers.ts b/src/demo/shared/fixtures/providers.ts index 80a639c..0040e22 100644 --- a/src/demo/shared/fixtures/providers.ts +++ b/src/demo/shared/fixtures/providers.ts @@ -1,5 +1,6 @@ import type { ProviderData } from '../../../components/pages/ProvidersStep'; import type { PackagesStepProvider, ProviderTier } from '../../../components/pages/PackagesStep'; +import { assetUrl } from '../assets'; export interface DemoProvider extends ProviderData { id: string; @@ -13,8 +14,8 @@ export const providers: DemoProvider[] = [ location: 'Wentworth, NSW', verified: true, tier: 'verified', - imageUrl: '/images/venues/hparsons-funeral-home-wollongong/01.jpg', - logoUrl: '/images/providers/hparsons-funeral-directors/logo.png', + imageUrl: assetUrl('/images/venues/hparsons-funeral-home-wollongong/01.jpg'), + logoUrl: assetUrl('/images/providers/hparsons-funeral-directors/logo.png'), rating: 4.6, reviewCount: 7, startingPrice: 1800, @@ -28,8 +29,8 @@ export const providers: DemoProvider[] = [ location: 'Wollongong, NSW', verified: true, tier: 'verified', - imageUrl: '/images/venues/rankins-funeral-home-warrawong/01.jpg', - logoUrl: '/images/providers/rankins-funerals/logo.png', + imageUrl: assetUrl('/images/venues/rankins-funeral-home-warrawong/01.jpg'), + logoUrl: assetUrl('/images/providers/rankins-funerals/logo.png'), rating: 4.8, reviewCount: 23, startingPrice: 2450, @@ -52,8 +53,8 @@ export const providers: DemoProvider[] = [ location: 'Kingaroy, QLD', verified: true, tier: 'verified', - imageUrl: '/images/venues/killick-family-funerals-chapel-kingaroy/01.jpg', - logoUrl: '/images/providers/killick-family-funerals/logo.png', + imageUrl: assetUrl('/images/venues/killick-family-funerals-chapel-kingaroy/01.jpg'), + logoUrl: assetUrl('/images/providers/killick-family-funerals/logo.png'), rating: 4.9, reviewCount: 15, startingPrice: 3100, @@ -65,8 +66,8 @@ export const providers: DemoProvider[] = [ location: 'Ourimbah, NSW', verified: true, tier: 'verified', - imageUrl: '/images/venues/mackay-family-garden-estate/01.jpg', - logoUrl: '/images/providers/mackay-family-funerals/logo.webp', + imageUrl: assetUrl('/images/venues/mackay-family-garden-estate/01.jpg'), + logoUrl: assetUrl('/images/providers/mackay-family-funerals/logo.webp'), rating: 4.6, reviewCount: 87, startingPrice: 2800, @@ -78,8 +79,8 @@ export const providers: DemoProvider[] = [ location: 'Bega, NSW', verified: true, tier: 'verified', - imageUrl: '/images/venues/mannings-chapel/01.jpg', - logoUrl: '/images/providers/mannings-funerals/logo.png', + imageUrl: assetUrl('/images/venues/mannings-chapel/01.jpg'), + logoUrl: assetUrl('/images/providers/mannings-funerals/logo.png'), rating: 4.7, reviewCount: 31, startingPrice: 2600,