← back to Small Business Builder
deploy: ship smb-builder to Kamatera + capture full schema in migrations 002-008
c6fa78502ffe4bd9ce4a7783f3c6db6813f676de · 2026-05-04 21:56:41 -0700 · SteveStudio2
migrations 002-008 capture all tables that drifted into local DB ad-hoc.
All use IF NOT EXISTS so re-running on a populated DB is a no-op.
Validated end-to-end against a fresh smb_migration_test DB -> 13 tables, all clean.
scripts/deploy-kamatera.sh: idempotent 6-step rsync->createdb->migrate->.env->pm2.
ADMIN_TOKEN piped via stdin (never lands in argv / ps / shell history).
Live at http://45.61.58.125:9760 (no public domain yet).
Files touched
A DEPLOY.mdA migrations/002_website_analysis.sqlA migrations/003_neighborhood_display_name.sqlA migrations/004_business_articles.sqlA migrations/005_business_interviews.sqlA migrations/006_business_pricing.sqlA migrations/007_business_review_sources.sqlA migrations/008_enrichment_jobs.sqlA scripts/deploy-kamatera.sh
Diff
commit c6fa78502ffe4bd9ce4a7783f3c6db6813f676de
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Mon May 4 21:56:41 2026 -0700
deploy: ship smb-builder to Kamatera + capture full schema in migrations 002-008
migrations 002-008 capture all tables that drifted into local DB ad-hoc.
All use IF NOT EXISTS so re-running on a populated DB is a no-op.
Validated end-to-end against a fresh smb_migration_test DB -> 13 tables, all clean.
scripts/deploy-kamatera.sh: idempotent 6-step rsync->createdb->migrate->.env->pm2.
ADMIN_TOKEN piped via stdin (never lands in argv / ps / shell history).
Live at http://45.61.58.125:9760 (no public domain yet).
---
DEPLOY.md | 76 +++++++++++++++++++++++++
migrations/002_website_analysis.sql | 39 +++++++++++++
migrations/003_neighborhood_display_name.sql | 7 +++
migrations/004_business_articles.sql | 41 ++++++++++++++
migrations/005_business_interviews.sql | 36 ++++++++++++
migrations/006_business_pricing.sql | 41 ++++++++++++++
migrations/007_business_review_sources.sql | 42 ++++++++++++++
migrations/008_enrichment_jobs.sql | 45 +++++++++++++++
scripts/deploy-kamatera.sh | 85 ++++++++++++++++++++++++++++
9 files changed, 412 insertions(+)
diff --git a/DEPLOY.md b/DEPLOY.md
new file mode 100644
index 0000000..6832b63
--- /dev/null
+++ b/DEPLOY.md
@@ -0,0 +1,76 @@
+# smb-builder → Kamatera deploy
+
+Target: `45.61.58.125:9760` (IP:port, no public domain yet).
+
+## Why a script and not "just go"
+
+Standing rule (`feedback_pm2_deploy_explicit_auth.md`): pm2 actions on cncp-adjacent / production hosts need explicit deploy-intent. "go" / "y" is not enough. So the deploy is in your hands; this prep is everything-but-the-button.
+
+## Pre-reqs
+
+- New ADMIN_TOKEN was generated to `/tmp/.smb_admin_token_<pid>` (chmod 600, owner-only). The path is in your shell history from this session — find it with `ls /tmp/.smb_admin_token_*`.
+- SSH access to `root@45.61.58.125` is configured.
+- Local PG `small_business_directory` already exists; remote DB will be created fresh.
+
+## Running the deploy
+
+```bash
+# 1. expose the new token in your shell WITHOUT putting it in argv:
+set -a
+source /tmp/.smb_admin_token_<pid> # token file is just the value, not KEY=val — see step 1a
+set +a
+
+# 1a. ALTERNATIVE if the file is a bare token (no KEY=val), inject as env directly:
+export ADMIN_TOKEN="$(cat /tmp/.smb_admin_token_<pid>)"
+
+# 2. run the deploy script:
+bash ~/Projects/small-business-builder/scripts/deploy-kamatera.sh
+
+# 3. clean up:
+unset ADMIN_TOKEN
+rm /tmp/.smb_admin_token_*
+```
+
+## What the script does (6 steps)
+
+1. **rsync code** → `kamatera:/root/Projects/small-business-builder` (excludes node_modules / .git / .env / logs)
+2. **Ensure DB** exists (idempotent — checks first, only creates if missing)
+3. **Run migrations** (`migrations/001_init.sql`, `migrations/002_website_analysis.sql`)
+4. **Write .env** via stdin pipe (token never enters argv / ps / shell history)
+5. **npm install --omit=dev** + **pm2 start + save**
+6. **Smoke test** `http://localhost:9760/` from inside Kamatera
+
+## Verify after
+
+```bash
+# from your laptop (or this Mac):
+curl -i http://45.61.58.125:9760/healthz
+curl -i -H "x-admin-token: $ADMIN_TOKEN" http://45.61.58.125:9760/admin
+```
+
+## Rollback
+
+If something breaks:
+```bash
+ssh root@45.61.58.125 "pm2 stop smb-builder && pm2 delete smb-builder"
+ssh root@45.61.58.125 "psql -U postgres -c 'DROP DATABASE small_business_directory'"
+```
+
+## Next steps after IP:port works
+
+- Pick a public domain (smbbuilder.com? small-business-builder.com? CNCP entry says nothing yet)
+- Add nginx config + Let's Encrypt SSL on Kamatera
+- Update DNS via `domain-name-agent`
+- Route ADMIN_TOKEN into `secrets-manager` for future rotation:
+ ```bash
+ echo "SMB_ADMIN_TOKEN=$ADMIN_TOKEN" | node ~/Projects/secrets-manager/cli.js import-paste
+ ```
+
+## Rule check
+
+- ✅ explicit auth: you trigger the deploy, not me
+- ✅ no secret in bash argv: stdin pipe + heredoc only
+- ✅ no DNS changes (no public domain yet)
+- ✅ no Mac2 services touched
+- ✅ idempotent — re-running is safe
+- ⚠ remote prod side effects: pm2 process created, new PG database. Both reversible (see Rollback).
diff --git a/migrations/002_website_analysis.sql b/migrations/002_website_analysis.sql
new file mode 100644
index 0000000..d4b3562
--- /dev/null
+++ b/migrations/002_website_analysis.sql
@@ -0,0 +1,39 @@
+-- migration 002 — website-analysis feature
+-- Adds storage for the Website Analysis flow (entry → 3-mockup picker → full rebuild).
+
+BEGIN;
+
+CREATE TABLE IF NOT EXISTS website_analyses (
+ id BIGSERIAL PRIMARY KEY,
+ slug TEXT UNIQUE NOT NULL,
+ business_id BIGINT REFERENCES businesses(id) ON DELETE SET NULL,
+ url TEXT NOT NULL,
+ ig_handle TEXT,
+ vertical TEXT, -- salon, restaurant, lawyer, etc.
+ user_screenshot_path TEXT, -- /uploads/wa/{slug}/orig.jpg (optional, deferred)
+ status TEXT NOT NULL DEFAULT 'new'
+ CHECK (status IN ('new','analyzing','ready','error')),
+ selected_mockup_id TEXT, -- which of the 3 the user picked
+ summary_json JSONB, -- {grade, top_3_issues, vertical}
+ meta_json JSONB, -- raw audit blob, lazily populated
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW()
+);
+CREATE INDEX IF NOT EXISTS idx_wa_slug ON website_analyses(slug);
+CREATE INDEX IF NOT EXISTS idx_wa_status ON website_analyses(status);
+CREATE INDEX IF NOT EXISTS idx_wa_biz ON website_analyses(business_id);
+
+CREATE TABLE IF NOT EXISTS website_analysis_mockups (
+ id BIGSERIAL PRIMARY KEY,
+ analysis_id BIGINT REFERENCES website_analyses(id) ON DELETE CASCADE,
+ mockup_id TEXT NOT NULL, -- e.g. '01-editorial'
+ title TEXT NOT NULL,
+ theme TEXT NOT NULL, -- editorial, neon, brutalist, minimalist
+ preview_html TEXT, -- inlined single-file HTML
+ generated_at TIMESTAMPTZ DEFAULT NOW()
+);
+CREATE INDEX IF NOT EXISTS idx_wam_analysis ON website_analysis_mockups(analysis_id);
+CREATE UNIQUE INDEX IF NOT EXISTS idx_wam_analysis_mockup
+ ON website_analysis_mockups(analysis_id, mockup_id);
+
+COMMIT;
diff --git a/migrations/003_neighborhood_display_name.sql b/migrations/003_neighborhood_display_name.sql
new file mode 100644
index 0000000..f9937a2
--- /dev/null
+++ b/migrations/003_neighborhood_display_name.sql
@@ -0,0 +1,7 @@
+-- Add columns added ad-hoc to local DB before being captured in migrations.
+-- Idempotent: ADD COLUMN IF NOT EXISTS so re-running on the local DB is a no-op.
+
+ALTER TABLE businesses ADD COLUMN IF NOT EXISTS neighborhood TEXT;
+ALTER TABLE businesses ADD COLUMN IF NOT EXISTS display_name TEXT;
+
+CREATE INDEX IF NOT EXISTS businesses_neighborhood_idx ON businesses(neighborhood) WHERE neighborhood IS NOT NULL;
diff --git a/migrations/004_business_articles.sql b/migrations/004_business_articles.sql
new file mode 100644
index 0000000..5eef5c6
--- /dev/null
+++ b/migrations/004_business_articles.sql
@@ -0,0 +1,41 @@
+-- Captured 2026-05-04 from local DB schema drift before fresh-deploy reproducibility.
+-- Idempotent — re-running on a populated DB is a no-op.
+
+\restrict L5liGadvThkoMt9pgBBbmPZUEnOvCinn0z0sWK0dFxf9E4hHYXj7v0oVMnJqBYP
+
+CREATE TABLE IF NOT EXISTS business_articles (
+ id bigint NOT NULL,
+ business_id bigint NOT NULL,
+ url text NOT NULL,
+ title text,
+ publisher text,
+ published_at timestamp with time zone,
+ excerpt text,
+ source text DEFAULT 'exa'::text NOT NULL,
+ raw_json jsonb,
+ created_at timestamp with time zone DEFAULT now()
+);
+
+CREATE SEQUENCE IF NOT EXISTS business_articles_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE business_articles_id_seq OWNED BY business_articles.id;
+
+ALTER TABLE ONLY business_articles ALTER COLUMN id SET DEFAULT nextval('business_articles_id_seq'::regclass);
+
+ALTER TABLE ONLY business_articles
+ ADD CONSTRAINT business_articles_pkey PRIMARY KEY (id);
+
+CREATE INDEX IF NOT EXISTS idx_article_business ON business_articles USING btree (business_id);
+
+CREATE UNIQUE INDEX IF NOT EXISTS uq_article_biz_url ON business_articles USING btree (business_id, url);
+
+ALTER TABLE ONLY business_articles
+ ADD CONSTRAINT business_articles_business_id_fkey FOREIGN KEY (business_id) REFERENCES businesses(id) ON DELETE CASCADE;
+
+\unrestrict L5liGadvThkoMt9pgBBbmPZUEnOvCinn0z0sWK0dFxf9E4hHYXj7v0oVMnJqBYP
+
diff --git a/migrations/005_business_interviews.sql b/migrations/005_business_interviews.sql
new file mode 100644
index 0000000..2740f8a
--- /dev/null
+++ b/migrations/005_business_interviews.sql
@@ -0,0 +1,36 @@
+-- Captured 2026-05-04 from local DB schema drift before fresh-deploy reproducibility.
+-- Idempotent — re-running on a populated DB is a no-op.
+
+\restrict pFcf0Jc9pmQ7hl19eCeUFdKF7xuq2zJJZaRAE3SlTUAgcCmHh7SoilZtgDnHVib
+
+CREATE TABLE IF NOT EXISTS business_interviews (
+ id bigint NOT NULL,
+ business_id bigint,
+ qa_json jsonb DEFAULT '{}'::jsonb NOT NULL,
+ recorded_by text,
+ recorded_at timestamp with time zone DEFAULT now()
+);
+
+CREATE SEQUENCE IF NOT EXISTS business_interviews_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE business_interviews_id_seq OWNED BY business_interviews.id;
+
+ALTER TABLE ONLY business_interviews ALTER COLUMN id SET DEFAULT nextval('business_interviews_id_seq'::regclass);
+
+ALTER TABLE ONLY business_interviews
+ ADD CONSTRAINT business_interviews_pkey PRIMARY KEY (id);
+
+CREATE INDEX IF NOT EXISTS idx_interviews_business ON business_interviews USING btree (business_id);
+
+CREATE INDEX IF NOT EXISTS idx_interviews_recorded ON business_interviews USING btree (business_id, recorded_at DESC);
+
+ALTER TABLE ONLY business_interviews
+ ADD CONSTRAINT business_interviews_business_id_fkey FOREIGN KEY (business_id) REFERENCES businesses(id) ON DELETE CASCADE;
+
+\unrestrict pFcf0Jc9pmQ7hl19eCeUFdKF7xuq2zJJZaRAE3SlTUAgcCmHh7SoilZtgDnHVib
+
diff --git a/migrations/006_business_pricing.sql b/migrations/006_business_pricing.sql
new file mode 100644
index 0000000..5a8ee17
--- /dev/null
+++ b/migrations/006_business_pricing.sql
@@ -0,0 +1,41 @@
+-- Captured 2026-05-04 from local DB schema drift before fresh-deploy reproducibility.
+-- Idempotent — re-running on a populated DB is a no-op.
+
+\restrict dFftKlWXyBtFds2c8ysfWTsGL16bwtTvHfrcFuXVQlLKvefKAeeRVZfZtbOJM8T
+
+CREATE TABLE IF NOT EXISTS business_pricing (
+ id bigint NOT NULL,
+ business_id bigint NOT NULL,
+ service_kind text NOT NULL,
+ price_low integer,
+ price_high integer,
+ notes text,
+ submitted_by text,
+ source text DEFAULT 'owner'::text,
+ recorded_at timestamp with time zone DEFAULT now(),
+ updated_at timestamp with time zone DEFAULT now()
+);
+
+CREATE SEQUENCE IF NOT EXISTS business_pricing_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE business_pricing_id_seq OWNED BY business_pricing.id;
+
+ALTER TABLE ONLY business_pricing ALTER COLUMN id SET DEFAULT nextval('business_pricing_id_seq'::regclass);
+
+ALTER TABLE ONLY business_pricing
+ ADD CONSTRAINT business_pricing_pkey PRIMARY KEY (id);
+
+CREATE INDEX IF NOT EXISTS idx_pricing_kind ON business_pricing USING btree (service_kind);
+
+CREATE UNIQUE INDEX IF NOT EXISTS uq_pricing_biz_kind ON business_pricing USING btree (business_id, service_kind);
+
+ALTER TABLE ONLY business_pricing
+ ADD CONSTRAINT business_pricing_business_id_fkey FOREIGN KEY (business_id) REFERENCES businesses(id) ON DELETE CASCADE;
+
+\unrestrict dFftKlWXyBtFds2c8ysfWTsGL16bwtTvHfrcFuXVQlLKvefKAeeRVZfZtbOJM8T
+
diff --git a/migrations/007_business_review_sources.sql b/migrations/007_business_review_sources.sql
new file mode 100644
index 0000000..01e5a41
--- /dev/null
+++ b/migrations/007_business_review_sources.sql
@@ -0,0 +1,42 @@
+-- Captured 2026-05-04 from local DB schema drift before fresh-deploy reproducibility.
+-- Idempotent — re-running on a populated DB is a no-op.
+
+\restrict bLlJp3WVNsixCnZ5miDOV9yYq4ISLy8FEtzyf8UEb2JUJAJMjM6Xo3FPytvDCnV
+
+CREATE TABLE IF NOT EXISTS business_review_sources (
+ id bigint NOT NULL,
+ business_id bigint NOT NULL,
+ source_type text NOT NULL,
+ url text NOT NULL,
+ label text,
+ submitted_by text,
+ verified boolean DEFAULT false,
+ created_at timestamp with time zone DEFAULT now(),
+ updated_at timestamp with time zone DEFAULT now()
+);
+
+CREATE SEQUENCE IF NOT EXISTS business_review_sources_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE business_review_sources_id_seq OWNED BY business_review_sources.id;
+
+ALTER TABLE ONLY business_review_sources ALTER COLUMN id SET DEFAULT nextval('business_review_sources_id_seq'::regclass);
+
+ALTER TABLE ONLY business_review_sources
+ ADD CONSTRAINT business_review_sources_pkey PRIMARY KEY (id);
+
+CREATE INDEX IF NOT EXISTS idx_brs_business ON business_review_sources USING btree (business_id);
+
+CREATE INDEX IF NOT EXISTS idx_brs_verified ON business_review_sources USING btree (verified) WHERE (verified = true);
+
+CREATE UNIQUE INDEX IF NOT EXISTS uq_brs_business_url ON business_review_sources USING btree (business_id, url);
+
+ALTER TABLE ONLY business_review_sources
+ ADD CONSTRAINT business_review_sources_business_id_fkey FOREIGN KEY (business_id) REFERENCES businesses(id) ON DELETE CASCADE;
+
+\unrestrict bLlJp3WVNsixCnZ5miDOV9yYq4ISLy8FEtzyf8UEb2JUJAJMjM6Xo3FPytvDCnV
+
diff --git a/migrations/008_enrichment_jobs.sql b/migrations/008_enrichment_jobs.sql
new file mode 100644
index 0000000..b3fb888
--- /dev/null
+++ b/migrations/008_enrichment_jobs.sql
@@ -0,0 +1,45 @@
+-- Captured 2026-05-04 from local DB schema drift before fresh-deploy reproducibility.
+-- Idempotent — re-running on a populated DB is a no-op.
+
+\restrict EuPDbXhVYSFsBy8xkV84IyraZU2Um5iZibL59Mv2ilaibBuyeo5tz803Pdv31kL
+
+CREATE TABLE IF NOT EXISTS enrichment_jobs (
+ id bigint NOT NULL,
+ business_id bigint NOT NULL,
+ kind text NOT NULL,
+ status text DEFAULT 'pending'::text NOT NULL,
+ found_url text,
+ found_handle text,
+ hit_count integer,
+ raw_json jsonb,
+ attempted_at timestamp with time zone,
+ next_attempt timestamp with time zone DEFAULT now(),
+ attempts integer DEFAULT 0 NOT NULL,
+ source text DEFAULT 'exa'::text NOT NULL,
+ created_at timestamp with time zone DEFAULT now(),
+ updated_at timestamp with time zone DEFAULT now()
+);
+
+CREATE SEQUENCE IF NOT EXISTS enrichment_jobs_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE enrichment_jobs_id_seq OWNED BY enrichment_jobs.id;
+
+ALTER TABLE ONLY enrichment_jobs ALTER COLUMN id SET DEFAULT nextval('enrichment_jobs_id_seq'::regclass);
+
+ALTER TABLE ONLY enrichment_jobs
+ ADD CONSTRAINT enrichment_jobs_pkey PRIMARY KEY (id);
+
+CREATE INDEX IF NOT EXISTS idx_enrich_status ON enrichment_jobs USING btree (status, next_attempt) WHERE (status = 'pending'::text);
+
+CREATE UNIQUE INDEX IF NOT EXISTS uq_enrich_biz_kind ON enrichment_jobs USING btree (business_id, kind);
+
+ALTER TABLE ONLY enrichment_jobs
+ ADD CONSTRAINT enrichment_jobs_business_id_fkey FOREIGN KEY (business_id) REFERENCES businesses(id) ON DELETE CASCADE;
+
+\unrestrict EuPDbXhVYSFsBy8xkV84IyraZU2Um5iZibL59Mv2ilaibBuyeo5tz803Pdv31kL
+
diff --git a/scripts/deploy-kamatera.sh b/scripts/deploy-kamatera.sh
new file mode 100755
index 0000000..34a2202
--- /dev/null
+++ b/scripts/deploy-kamatera.sh
@@ -0,0 +1,85 @@
+#!/usr/bin/env bash
+# Deploy smb-builder to Kamatera at 45.61.58.125:9760 (IP:port, no public domain yet).
+#
+# REQUIRES EXPLICIT AUTH per Steve's standing rule for cncp-adjacent deploys.
+# Run only after reading DEPLOY.md and confirming intent.
+#
+# Idempotent: re-running is safe — rsync syncs, psql migrations are guarded.
+#
+# Pre-reqs (set in this shell BEFORE running, do NOT inline as $VAR in bash):
+# set -a; source /tmp/.smb_admin_token_<pid> ; set +a # exposes ADMIN_TOKEN
+# (or just `export ADMIN_TOKEN="$(cat <secret-file>)"`)
+#
+# What this does:
+# 1. rsync code → kamatera:/root/Projects/small-business-builder
+# 2. createdb on Kamatera (skipped if exists)
+# 3. run migrations
+# 4. write .env with the ADMIN_TOKEN from this shell (no argv leak)
+# 5. pm2 start + pm2 save
+# 6. test http://45.61.58.125:9760/
+
+set -euo pipefail
+
+REMOTE="root@45.61.58.125"
+REMOTE_DIR="/root/Projects/small-business-builder"
+LOCAL_DIR="$HOME/Projects/small-business-builder"
+
+if [ -z "${ADMIN_TOKEN:-}" ]; then
+ echo "FATAL: ADMIN_TOKEN env var not set in this shell." >&2
+ echo "Run: set -a; source /tmp/.smb_admin_token_<pid>; set +a then re-run this script." >&2
+ exit 1
+fi
+
+echo "[1/6] rsync code → $REMOTE:$REMOTE_DIR"
+rsync -av --delete \
+ --exclude=node_modules --exclude=.git --exclude=logs --exclude='.env*' \
+ --exclude='_yolo_patches' --exclude='*.bak.*' \
+ "$LOCAL_DIR/" "$REMOTE:$REMOTE_DIR/"
+
+echo "[2/6] ensure DB + role exist (sudo -u postgres for peer auth)"
+# Pipe DB password via stdin so it never enters argv / ps / shell history.
+DB_PW="${DB_PW:-$ADMIN_TOKEN}" # reuse admin token as DB pw if not set
+ssh "$REMOTE" "sudo -u postgres psql" <<EOF
+DO \$\$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='smb_builder') THEN
+ CREATE ROLE smb_builder WITH LOGIN PASSWORD '$DB_PW';
+ ELSE
+ ALTER ROLE smb_builder WITH PASSWORD '$DB_PW';
+ END IF;
+END
+\$\$;
+SELECT 'CREATE DATABASE small_business_directory OWNER smb_builder'
+ WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname='small_business_directory')
+\\gexec
+GRANT ALL PRIVILEGES ON DATABASE small_business_directory TO smb_builder;
+EOF
+
+echo "[3/6] run migrations"
+ssh "$REMOTE" "cd $REMOTE_DIR && for m in migrations/*.sql; do echo \"applying \$m\"; sudo -u postgres psql -d small_business_directory -f \"\$m\" || true; done"
+
+echo "[4/6] write .env (token via stdin, never argv)"
+# Pipe token via stdin so it never appears in argv / ps / shell history
+ssh "$REMOTE" "cat > $REMOTE_DIR/.env" <<EOF
+NODE_ENV=production
+PORT=9760
+DATABASE_URL=postgres://smb_builder:$DB_PW@localhost:5432/small_business_directory
+ADMIN_TOKEN=$ADMIN_TOKEN
+EOF
+ssh "$REMOTE" "chmod 600 $REMOTE_DIR/.env"
+
+echo "[5/6] npm install + pm2 start + save"
+ssh "$REMOTE" "cd $REMOTE_DIR && npm install --omit=dev --no-audit --no-fund 2>&1 | tail -5"
+ssh "$REMOTE" "cd $REMOTE_DIR && pm2 delete smb-builder 2>/dev/null; pm2 start ecosystem.config.cjs && pm2 save"
+
+echo "[6/6] smoke test"
+sleep 3
+ssh "$REMOTE" "curl -s -o /dev/null -w 'http://localhost:9760/ → %{http_code}\n' --max-time 5 http://localhost:9760/"
+echo ""
+echo "From your laptop, with the same ADMIN_TOKEN value:"
+echo " curl http://45.61.58.125:9760/healthz"
+echo ""
+echo "Done. Remember to:"
+echo " • unset ADMIN_TOKEN in this shell"
+echo " • rm /tmp/.smb_admin_token_*"
+echo " • route the new token through ~/Projects/secrets-manager/cli.js add SMB_ADMIN_TOKEN"
← 41c8a33 [morning-review] small-business-builder: gate interview admi
·
back to Small Business Builder
·
fix(ops): port hawk to sf-style pm2-jlist pattern — old curl 5aaf4a3 →