← back to Designer Wallcoverings
appointments: deploy prep — runbook + nginx snippet (edge-routing blocker documented)
11042a14515fff5787ac4ab5c9541e405fe9c04a · 2026-06-29 10:00:59 -0700 · Steve
Files touched
A shopify/quote-api/appointments/deploy/deploy-runbook.shA shopify/quote-api/appointments/deploy/nginx-appointments.snippet.conf
Diff
commit 11042a14515fff5787ac4ab5c9541e405fe9c04a
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Jun 29 10:00:59 2026 -0700
appointments: deploy prep — runbook + nginx snippet (edge-routing blocker documented)
---
.../appointments/deploy/deploy-runbook.sh | 118 +++++++++++++++++++++
.../deploy/nginx-appointments.snippet.conf | 36 +++++++
2 files changed, 154 insertions(+)
diff --git a/shopify/quote-api/appointments/deploy/deploy-runbook.sh b/shopify/quote-api/appointments/deploy/deploy-runbook.sh
new file mode 100644
index 00000000..0e3afe69
--- /dev/null
+++ b/shopify/quote-api/appointments/deploy/deploy-runbook.sh
@@ -0,0 +1,118 @@
+#!/usr/bin/env bash
+# ═══════════════════════════════════════════════════════════════════════════════
+# DW Appointments — DEPLOY RUNBOOK (Steve-gated; read top-to-bottom, run by hand)
+#
+# Target: the LIVE koroseal-quote-api app on Kamatera (/root/Projects/koroseal-quote-api,
+# pm2 name "koroseal-quote-api", PORT=3001). The appointments router folds into it.
+#
+# THIS SCRIPT IS NOT MEANT TO BE RUN BLIND. Each block is a gated action — copy the
+# commands you've decided to run. Recon confirmed (2026-06-29):
+# • koroseal app online on :3001, /health → 200
+# • pg is NOT installed in that app, no DATABASE_URL set → step 1 fixes both
+# • appt_* tables NOT yet on canonical dw_unified → step 2 creates them
+# • dw_admin role exists on dw_unified → DATABASE_URL can use it
+# • nginx vhost designerwallcoverings.com-api proxies /api/* → :3001 on PORT 80
+# • GEORGE_* env already present in the koroseal .env → internal notify works
+#
+# ⚠️ BLOCKER (see memo): live www.designerwallcoverings.com does NOT currently route
+# /api/* to Kamatera. www is a DNS-only CNAME → shops.myshopify.com (Shopify),
+# authoritative NS = GoDaddy (ns71/72.domaincontrol.com), Cloudflare zone is a
+# stale parallel copy. /api/koroseal-quote returns 404 live TODAY. The nginx
+# proxy on origin is correct but nothing in live DNS points /api/* at it. RESOLVE
+# THE EDGE ROUTING (step 3) before this feature can work in production.
+# ═══════════════════════════════════════════════════════════════════════════════
+set -euo pipefail
+SRV="my-server" # ssh alias → root@45.61.58.125
+APP=/root/Projects/koroseal-quote-api
+LOCAL_APPT=~/Projects/Designer-Wallcoverings/shopify/quote-api/appointments
+
+echo "This runbook is reference-only. Run blocks deliberately, not via ./this." ; exit 0
+
+# ─── 0. PRE-FLIGHT (read-only, safe to run) ───────────────────────────────────
+ssh "$SRV" "cd $APP && pm2 describe koroseal-quote-api >/dev/null && curl -s http://127.0.0.1:3001/health"
+ssh "$SRV" "ls $APP/node_modules/pg >/dev/null 2>&1 && echo 'pg PRESENT' || echo 'pg MISSING — step 1 installs it'"
+
+# ─── 1. DEPLOY BACKEND ────────────────────────────────────────────────────────
+# 1a. Copy the appointments/ module into the live app (rsync, excludes dev/local files).
+rsync -av --exclude 'run-local.js' --exclude 'render-preview.js' --exclude 'drive-preview.mjs' \
+ "$LOCAL_APPT/" "$SRV:$APP/appointments/"
+# 1b. Install pg into the live app (node-fetch already present).
+ssh "$SRV" "cd $APP && npm install pg@^8 --omit=dev"
+# 1c. Add the mount line to the live server.js — IMPORTANT: it must go BEFORE the
+# existing catch-all 404 handler (`app.use((req,res)=>res.status(404)...)`).
+# Edit by hand on the box (back up first):
+ssh "$SRV" "cp $APP/server.js $APP/server.js.bak-pre-appointments-\$(date +%Y%m%d-%H%M%S)"
+# Then insert, just after the `app.post('/api/sku-inquiry', handleSkuInquiry);` line:
+# app.use(require('./appointments/router'));
+# 1d. Add env to $APP/.env (see step 5 for the values). At minimum:
+# DATABASE_URL=postgres://dw_admin@127.0.0.1:5432/dw_unified
+# APPT_OWNER_EMAIL=info@designerwallcoverings.com
+# APPT_PUBLIC_BASE=https://www.designerwallcoverings.com
+# DW_SHOWROOM_LOCATION=Designer Wallcoverings Showroom · 15442 Ventura Blvd #102, Sherman Oaks, CA 91403
+# (GOOGLE_OAUTH_* + ADMIN_PASS optional at first; calendar push + admin list degrade gracefully)
+# 1e. Reload (graceful, keeps the existing koroseal/sku-inquiry endpoints).
+ssh "$SRV" "cd $APP && pm2 reload koroseal-quote-api --update-env && sleep 2 && curl -s http://127.0.0.1:3001/api/appointments/health"
+# EXPECT: {"ok":true,"service":"dw-appointments","oauth_configured":false,"availability_rows":5} (after step 2)
+
+# ─── 2. CANONICAL MIGRATION (dw_unified on Kamatera) ──────────────────────────
+# Push the SQL up, then run via the postgres role (plain psql -d dw_unified fails as root).
+scp "$LOCAL_APPT/migrations/appointments.sql" "$SRV:/root/appointments.sql"
+ssh "$SRV" "sudo -u postgres psql -d dw_unified -v ON_ERROR_STOP=1 -f /root/appointments.sql"
+# Verify: 4 appt_ tables + 5 seeded availability rows.
+ssh "$SRV" "sudo -u postgres psql -d dw_unified -tAc \"select count(*) from appt_availability where owner_email='info@designerwallcoverings.com';\"" # → 5
+
+# ─── 3. EDGE ROUTING (THE BLOCKER — pick ONE; all are Steve-gated) ───────────
+# Live www is Shopify (DNS-only CNAME). The origin nginx proxy is NOT in the live
+# path. Until /api/* reaches Kamatera, the storefront section will 404. Options:
+#
+# (A) Dedicated subdomain on Kamatera + absolute api_base [RECOMMENDED, cleanest]
+# - Add DNS A record (in GODADDY — the authoritative NS): api.designerwallcoverings.com → 45.61.58.125
+# - Add a 443 server block on Kamatera for api.designerwallcoverings.com with certbot SSL,
+# carrying all 4 /api/* + /appointments/ location blocks.
+# - Set the section's Theme-Editor "Appointments API base URL" =
+# https://api.designerwallcoverings.com (section already supports absolute api_base)
+# - CORS already permissive in router.js, so cross-origin fetch works.
+# - confirmation/ICS links then use APPT_PUBLIC_BASE=https://api.designerwallcoverings.com
+#
+# (B) Cloudflare-proxy www + Origin Rule routing /api/* to 45.61.58.125
+# - Requires moving authoritative NS from GoDaddy → Cloudflare (per Steve's
+# DNS-Cloudflare-first rule) AND orange-clouding www, AND a Cloudflare
+# Worker/Origin-Rule that forwards path /api/* and /appointments/* to origin
+# while leaving everything else on Shopify. Heavier; touches live storefront DNS.
+# - This is presumably how koroseal-quote was INTENDED to work but currently is NOT.
+#
+# (C) Shopify App Proxy (/apps/appointments → Kamatera)
+# - Configure a Shopify app proxy that maps /apps/appointments/* to the Kamatera URL.
+# - Section api_base + APPT_PUBLIC_BASE = /apps/appointments. No DNS changes.
+# - Adds Shopify HMAC signature on proxied requests (router would need to allow it).
+#
+# DTD this decision before executing. (A) is the least entangled with the live storefront.
+# If (A): also add the nginx blocks from nginx-appointments.snippet.conf to that 443 vhost.
+
+# ─── 4. GOOGLE OAUTH (Steve clicks the consent — agents can't) ────────────────
+# - Google Cloud Console → enable Google Calendar API; create OAuth 2.0 Web client.
+# - Authorized redirect URI must match GOOGLE_OAUTH_REDIRECT_URI (host depends on step 3):
+# https://api.designerwallcoverings.com/api/appointments/oauth/callback (option A)
+# https://www.designerwallcoverings.com/api/appointments/oauth/callback (option B)
+# - Route GOOGLE_OAUTH_CLIENT_ID + GOOGLE_OAUTH_CLIENT_SECRET via /secrets into $APP/.env.
+# - Visit …/api/appointments/oauth/start?owner_email=info@designerwallcoverings.com and consent
+# with the Google account that owns info@'s calendar.
+# - Confirm: curl …/api/appointments/oauth/status?owner_email=info@designerwallcoverings.com → connected:true
+# (Until connected, bookings still work + email info@ — they just don't auto-push to Google.)
+
+# ─── 5. ENVS (route via /secrets where secret) ────────────────────────────────
+# Non-secret (edit $APP/.env directly): DATABASE_URL, APPT_OWNER_EMAIL, APPT_PUBLIC_BASE,
+# DW_SHOWROOM_LOCATION, GOOGLE_OAUTH_REDIRECT_URI.
+# Secret (via /secrets fan-out): GOOGLE_OAUTH_CLIENT_ID, GOOGLE_OAUTH_CLIENT_SECRET,
+# ADMIN_PASS (for /api/appointments/list). GEORGE_BASIC_AUTH_PASS is ALREADY set in $APP/.env.
+
+# ─── 6. SHOPIFY PAGE (Online Store → Pages) ──────────────────────────────────
+# Add page "Book an Appointment", template = book-appointment (already on dev theme 5.9.0).
+# Add to a nav menu. If using option A, set the section's api_base to the api. host.
+
+# ─── 7. PUBLISH THEME 5.9.0 → live (separate gate, after 1–6 verified) ───────
+
+# ─── 8. VERIFY (after 1–3 live) ───────────────────────────────────────────────
+# curl https://<api-host>/api/appointments/health
+# curl 'https://<api-host>/api/appointments/availability?owner_email=info@designerwallcoverings.com&date=<next-weekday>'
+# Then on the dev theme: /pages/book-appointment?preview_theme_id=144070803507
diff --git a/shopify/quote-api/appointments/deploy/nginx-appointments.snippet.conf b/shopify/quote-api/appointments/deploy/nginx-appointments.snippet.conf
new file mode 100644
index 00000000..a65a4ec2
--- /dev/null
+++ b/shopify/quote-api/appointments/deploy/nginx-appointments.snippet.conf
@@ -0,0 +1,36 @@
+# ─────────────────────────────────────────────────────────────────────────────
+# DW Appointments — nginx location blocks to ADD to the existing origin vhost
+# File on Kamatera: /etc/nginx/sites-available/designerwallcoverings.com-api
+# (already enabled via symlink in sites-enabled; serves the koroseal app on :3001)
+#
+# Add these TWO location blocks ALONGSIDE the existing /api/koroseal-quote and
+# /api/sku-inquiry blocks (same proxy target, port 3001). Place them BEFORE the
+# catch-all `location / { return 301 ...; }` block.
+#
+# NOTE: /appointments/ (no /api prefix) is the customer-facing confirmation page
+# + .ics. It must ALSO be proxied so confirmation links resolve on the storefront
+# domain. If /appointments/ collides with any future Shopify page slug, switch the
+# confirmation links to an absolute APPT_PUBLIC_BASE on a dedicated host instead.
+# ─────────────────────────────────────────────────────────────────────────────
+
+location /api/appointments/ {
+ proxy_pass http://127.0.0.1:3001;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_connect_timeout 10s;
+ proxy_send_timeout 30s;
+ proxy_read_timeout 30s;
+}
+
+location /appointments/ {
+ proxy_pass http://127.0.0.1:3001;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_connect_timeout 10s;
+ proxy_send_timeout 30s;
+ proxy_read_timeout 30s;
+}
← 991e88d0 appointments: omit blank api_base default (Shopify schema re
·
back to Designer Wallcoverings
·
appointments: target api.designerwallcoverings.com (Option A 52917463 →