← back to Wallco Ai
go-live launcher: scripts/golive.sh + /admin/golive web checklist · 8 steps with one-line commands, JSON probe, auto-refresh 30s
047f1d62eeb49d9caae70d77c468b17912b4effc · 2026-05-11 16:37:48 -0700 · Steve
Files touched
A scripts/golive.shA scripts/golive_status.jsM server.js
Diff
commit 047f1d62eeb49d9caae70d77c468b17912b4effc
Author: Steve <steve@designerwallcoverings.com>
Date: Mon May 11 16:37:48 2026 -0700
go-live launcher: scripts/golive.sh + /admin/golive web checklist · 8 steps with one-line commands, JSON probe, auto-refresh 30s
---
scripts/golive.sh | 200 +++++++++++++++++++++++++++++++++++++++++++++++
scripts/golive_status.js | 131 +++++++++++++++++++++++++++++++
server.js | 88 +++++++++++++++++++++
3 files changed, 419 insertions(+)
diff --git a/scripts/golive.sh b/scripts/golive.sh
new file mode 100755
index 0000000..4c97ed1
--- /dev/null
+++ b/scripts/golive.sh
@@ -0,0 +1,200 @@
+#!/bin/zsh
+# wallco.ai go-live launcher.
+# Each step is idempotent — re-running this is safe.
+# Steps that need a manual action print clear "DO THIS NOW" instructions.
+#
+# Usage:
+# bash scripts/golive.sh status # probe every step, no changes
+# bash scripts/golive.sh dns # apply DNS records once CF zone exists
+# bash scripts/golive.sh ns-swap # swap GoDaddy NS to CF
+# bash scripts/golive.sh cert # run certbot on Kamatera
+# bash scripts/golive.sh cf-ssl # set CF SSL mode to Full (Strict)
+# bash scripts/golive.sh all # run everything except the CF-dash-click step
+# bash scripts/golive.sh smoke # final 4-layer health check
+
+set -euo pipefail
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+cd "$ROOT"
+[[ -f .env ]] && set -a && source .env && set +a
+
+DOMAIN="wallco.ai"
+KAMATERA_IP="45.61.58.125"
+PM2_NAME="wallco-ai"
+KAMATERA_USER="root"
+KAMATERA_HOST="45.61.58.125"
+
+# Colors
+RED='\033[31m'; GREEN='\033[32m'; YELLOW='\033[33m'; BLUE='\033[34m'; DIM='\033[2m'; RST='\033[0m'
+
+log() { printf "${BLUE}[i]${RST} %s\n" "$*"; }
+ok() { printf "${GREEN}[✓]${RST} %s\n" "$*"; }
+warn() { printf "${YELLOW}[!]${RST} %s\n" "$*"; }
+err() { printf "${RED}[✗]${RST} %s\n" "$*"; }
+todo() { printf "${YELLOW}[TODO]${RST} %s\n" "$*"; }
+
+# ---- env / token helpers ----
+
+cf_token() {
+ if [[ -n "${CLOUDFLARE_API_TOKEN:-}" ]]; then echo "$CLOUDFLARE_API_TOKEN"; return; fi
+ for f in "$HOME/Projects/secrets-manager/.env" "$HOME/.dw-fleet.env"; do
+ [[ -f "$f" ]] || continue
+ local v
+ v=$(grep -E "^CLOUDFLARE_API_TOKEN=" "$f" 2>/dev/null | head -1 | sed -E 's/^[^=]+=//;s/^[\"\x27]?//;s/[\"\x27]?$//')
+ [[ -n "$v" ]] && echo "$v" && return
+ done
+}
+
+godaddy_creds() {
+ for f in "$HOME/Projects/secrets-manager/.env" "$HOME/.dw-fleet.env"; do
+ [[ -f "$f" ]] || continue
+ local k s
+ k=$(grep -E "^GODADDY_API_KEY=" "$f" 2>/dev/null | head -1 | sed -E 's/^[^=]+=//;s/^[\"\x27]?//;s/[\"\x27]?$//')
+ s=$(grep -E "^GODADDY_API_SECRET=" "$f" 2>/dev/null | head -1 | sed -E 's/^[^=]+=//;s/^[\"\x27]?//;s/[\"\x27]?$//')
+ [[ -n "$k" && -n "$s" ]] && echo "$k:$s" && return
+ done
+}
+
+# ---- probe steps (used by both status + UI) ----
+
+probe_cf_zone() {
+ local tok; tok=$(cf_token)
+ [[ -z "$tok" ]] && { echo '{"ok":false,"reason":"no_cf_token"}'; return; }
+ local r
+ r=$(curl -sf -H "Authorization: Bearer $tok" "https://api.cloudflare.com/client/v4/zones?name=${DOMAIN}" 2>/dev/null || echo '{}')
+ local id ns
+ id=$(echo "$r" | python3 -c "import json,sys; d=json.load(sys.stdin); rs=d.get('result',[]); print(rs[0]['id'] if rs else '')" 2>/dev/null || echo '')
+ ns=$(echo "$r" | python3 -c "import json,sys; d=json.load(sys.stdin); rs=d.get('result',[]); print(','.join(rs[0].get('name_servers',[])) if rs else '')" 2>/dev/null || echo '')
+ if [[ -n "$id" ]]; then
+ printf '{"ok":true,"zone_id":"%s","name_servers":"%s"}\n' "$id" "$ns"
+ else
+ echo '{"ok":false,"reason":"zone_not_created","action":"https://dash.cloudflare.com → Add a site → wallco.ai"}'
+ fi
+}
+
+probe_dns_records() {
+ local tok zone_id; tok=$(cf_token)
+ zone_id=$(probe_cf_zone | python3 -c "import json,sys; print(json.load(sys.stdin).get('zone_id',''))")
+ [[ -z "$zone_id" ]] && { echo '{"ok":false,"reason":"no_zone"}'; return; }
+ local recs
+ recs=$(curl -sf -H "Authorization: Bearer $tok" "https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records?per_page=100")
+ local got_a got_aaaa got_www got_mx got_spf got_dkim got_dmarc
+ got_a=$(echo "$recs" | python3 -c "import json,sys; r=json.load(sys.stdin)['result']; print(sum(1 for x in r if x['type']=='A' and x['name']=='${DOMAIN}'))")
+ got_www=$(echo "$recs" | python3 -c "import json,sys; r=json.load(sys.stdin)['result']; print(sum(1 for x in r if x['name']=='www.${DOMAIN}'))")
+ got_mx=$(echo "$recs" | python3 -c "import json,sys; r=json.load(sys.stdin)['result']; print(sum(1 for x in r if x['type']=='MX'))")
+ got_spf=$(echo "$recs" | python3 -c "import json,sys; r=json.load(sys.stdin)['result']; print(sum(1 for x in r if x['type']=='TXT' and 'v=spf1' in x.get('content','')))")
+ got_dkim=$(echo "$recs" | python3 -c "import json,sys; r=json.load(sys.stdin)['result']; print(sum(1 for x in r if x['type']=='TXT' and 'dkim' in x.get('name','').lower()))")
+ got_dmarc=$(echo "$recs" | python3 -c "import json,sys; r=json.load(sys.stdin)['result']; print(sum(1 for x in r if '_dmarc' in x.get('name','')))")
+ printf '{"ok":%s,"a":%s,"www":%s,"mx":%s,"spf":%s,"dkim":%s,"dmarc":%s}\n' \
+ "$( (( got_a + got_www + got_mx + got_spf + got_dkim + got_dmarc >= 6 )) && echo true || echo false )" \
+ "$got_a" "$got_www" "$got_mx" "$got_spf" "$got_dkim" "$got_dmarc"
+}
+
+probe_ns_at_registrar() {
+ local cur exp
+ cur=$(dig +short NS "$DOMAIN" @8.8.8.8 | sort | head -2 | paste -sd "," -)
+ exp=$(probe_cf_zone | python3 -c "import json,sys; print(json.load(sys.stdin).get('name_servers',''))" || echo '')
+ if [[ -z "$cur" ]]; then echo '{"ok":false,"reason":"no_ns"}'; return; fi
+ if [[ "$cur" == *"cloudflare"* ]]; then
+ printf '{"ok":true,"current":"%s"}\n' "$cur"
+ else
+ printf '{"ok":false,"current":"%s","expected":"%s"}\n' "$cur" "$exp"
+ fi
+}
+
+probe_cert() {
+ local r
+ r=$(curl -sf -m 5 -o /dev/null -w "%{http_code}|%{ssl_verify_result}" "https://${DOMAIN}/" 2>/dev/null || echo "000|0")
+ local code result
+ code=${r%|*}; result=${r#*|}
+ if [[ "$code" == "200" || "$code" == "301" || "$code" == "302" ]]; then
+ printf '{"ok":true,"http":%s,"verify":%s}\n' "$code" "$result"
+ else
+ printf '{"ok":false,"http":%s,"reason":"https_unreachable"}\n' "$code"
+ fi
+}
+
+probe_origin() {
+ local r
+ r=$(curl -sf -m 5 -o /dev/null -w "%{http_code}" "http://${KAMATERA_IP}:9905/health" 2>/dev/null || echo "000")
+ if [[ "$r" == "200" ]]; then echo '{"ok":true}'; else printf '{"ok":false,"http":%s}\n' "$r"; fi
+}
+
+# ---- the public commands ----
+
+cmd_status() {
+ log "wallco.ai go-live status"
+ echo
+ local cf dns ns cert origin
+ cf=$(probe_cf_zone); log "1. CF zone: $cf"
+ dns=$(probe_dns_records);log "2. DNS rrs: $dns"
+ ns=$(probe_ns_at_registrar); log "3. NS at GoDaddy: $ns"
+ origin=$(probe_origin); log "4. Origin :9905/health: $origin"
+ cert=$(probe_cert); log "5. HTTPS cert: $cert"
+}
+
+cmd_dns() {
+ log "Applying DNS records via finish_dns.sh"
+ [[ -x "$ROOT/finish_dns.sh" ]] || { err "finish_dns.sh not found or not executable"; exit 1; }
+ bash "$ROOT/finish_dns.sh"
+}
+
+cmd_ns_swap() {
+ local zone_ns
+ zone_ns=$(probe_cf_zone | python3 -c "import json,sys; print(json.load(sys.stdin).get('name_servers',''))")
+ [[ -z "$zone_ns" ]] && { err "CF zone not created yet — add it in https://dash.cloudflare.com first"; exit 2; }
+ log "Swapping GoDaddy NS for $DOMAIN → $zone_ns"
+ local creds; creds=$(godaddy_creds)
+ [[ -z "$creds" ]] && { err "no GoDaddy creds"; exit 3; }
+ local ns1 ns2; ns1=$(echo "$zone_ns" | cut -d, -f1); ns2=$(echo "$zone_ns" | cut -d, -f2)
+ curl -sf -X PUT "https://api.godaddy.com/v1/domains/${DOMAIN}" \
+ -H "Authorization: sso-key ${creds}" \
+ -H "Content-Type: application/json" \
+ -d "{\"nameServers\":[\"$ns1\",\"$ns2\"]}" && ok "NS swap submitted to GoDaddy"
+ warn "Propagation typically 10-60 min. Re-run 'bash scripts/golive.sh status' to check."
+}
+
+cmd_cert() {
+ log "Running certbot for $DOMAIN + www.$DOMAIN on Kamatera"
+ ssh "${KAMATERA_USER}@${KAMATERA_HOST}" "certbot --nginx -d $DOMAIN -d www.$DOMAIN --non-interactive --agree-tos --redirect -m steve@designerwallcoverings.com" \
+ && ok "Cert issued"
+}
+
+cmd_cf_ssl() {
+ local tok zone_id; tok=$(cf_token)
+ zone_id=$(probe_cf_zone | python3 -c "import json,sys; print(json.load(sys.stdin).get('zone_id',''))")
+ [[ -z "$zone_id" ]] && { err "no zone"; exit 2; }
+ curl -sf -X PATCH "https://api.cloudflare.com/client/v4/zones/${zone_id}/settings/ssl" \
+ -H "Authorization: Bearer $tok" -H "Content-Type: application/json" \
+ -d '{"value":"strict"}' && ok "CF SSL mode → Full (Strict)"
+ curl -sf -X PATCH "https://api.cloudflare.com/client/v4/zones/${zone_id}/settings/always_use_https" \
+ -H "Authorization: Bearer $tok" -H "Content-Type: application/json" \
+ -d '{"value":"on"}' && ok "CF Always-Use-HTTPS → on"
+}
+
+cmd_all() {
+ cmd_status
+ echo
+ log "Running automatable steps in order"
+ cmd_dns || true
+ cmd_ns_swap || true
+ warn "Wait 10-60 min for NS propagation, then run: bash scripts/golive.sh cert && bash scripts/golive.sh cf-ssl && bash scripts/golive.sh smoke"
+}
+
+cmd_smoke() {
+ log "Four-layer health check"
+ printf " origin :9905/health → "; curl -sf -o /dev/null -w "%{http_code}\n" "http://${KAMATERA_IP}:9905/health" || echo "fail"
+ printf " https://${DOMAIN}/health → "; curl -sfk -o /dev/null -w "%{http_code}\n" "https://${DOMAIN}/health" || echo "fail"
+ printf " https://${DOMAIN}/ → "; curl -sfk -o /dev/null -w "%{http_code}\n" "https://${DOMAIN}/" || echo "fail"
+ printf " https://www.${DOMAIN}/ → "; curl -sfk -o /dev/null -w "%{http_code}\n" "https://www.${DOMAIN}/" || echo "fail"
+}
+
+case "${1:-status}" in
+ status) cmd_status ;;
+ dns) cmd_dns ;;
+ ns-swap) cmd_ns_swap ;;
+ cert) cmd_cert ;;
+ cf-ssl) cmd_cf_ssl ;;
+ all) cmd_all ;;
+ smoke) cmd_smoke ;;
+ *) echo "usage: $0 {status|dns|ns-swap|cert|cf-ssl|all|smoke}"; exit 1 ;;
+esac
diff --git a/scripts/golive_status.js b/scripts/golive_status.js
new file mode 100755
index 0000000..bbc301b
--- /dev/null
+++ b/scripts/golive_status.js
@@ -0,0 +1,131 @@
+#!/usr/bin/env node
+/**
+ * Returns JSON state of every go-live step for wallco.ai.
+ * Mounted at /admin/golive.json — the /admin/golive HTML page polls it.
+ */
+'use strict';
+const { execSync } = require('child_process');
+const fs = require('fs');
+const path = require('path');
+
+const DOMAIN = 'wallco.ai';
+const ORIGIN = 'http://45.61.58.125:9905';
+
+function cfToken() {
+ if (process.env.CLOUDFLARE_API_TOKEN) return process.env.CLOUDFLARE_API_TOKEN;
+ for (const p of [
+ `${process.env.HOME}/Projects/secrets-manager/.env`,
+ `${process.env.HOME}/.dw-fleet.env`
+ ]) {
+ if (!fs.existsSync(p)) continue;
+ const m = fs.readFileSync(p, 'utf8').match(/^CLOUDFLARE_API_TOKEN=(\S+)/m);
+ if (m) return m[1].replace(/^["']|["']$/g, '');
+ }
+ return null;
+}
+
+async function cfGet(pathStr) {
+ const tok = cfToken();
+ if (!tok) throw new Error('no_cf_token');
+ const res = await fetch('https://api.cloudflare.com/client/v4' + pathStr, {
+ headers: { Authorization: `Bearer ${tok}` }
+ });
+ return res.json();
+}
+
+async function probeCfZone() {
+ try {
+ const j = await cfGet(`/zones?name=${DOMAIN}`);
+ const r = (j.result || [])[0];
+ if (!r) return { ok: false, reason: 'zone_not_created', action: 'Add wallco.ai in CF dash' };
+ return { ok: true, zone_id: r.id, name_servers: r.name_servers || [], status: r.status };
+ } catch (e) { return { ok: false, reason: e.message }; }
+}
+
+async function probeDnsRecords(zoneId) {
+ if (!zoneId) return { ok: false, reason: 'no_zone' };
+ try {
+ const j = await cfGet(`/zones/${zoneId}/dns_records?per_page=100`);
+ const r = j.result || [];
+ const a = r.filter(x => x.type === 'A' && x.name === DOMAIN).length;
+ const www = r.filter(x => x.name === `www.${DOMAIN}`).length;
+ const mx = r.filter(x => x.type === 'MX').length;
+ const spf = r.filter(x => x.type === 'TXT' && /v=spf1/.test(x.content)).length;
+ const dkim = r.filter(x => x.type === 'TXT' && /dkim/i.test(x.name)).length;
+ const dmarc = r.filter(x => x.name.startsWith('_dmarc')).length;
+ return { ok: a >= 1 && www >= 1 && mx >= 1 && spf >= 1 && dkim >= 1 && dmarc >= 1, a, www, mx, spf, dkim, dmarc };
+ } catch (e) { return { ok: false, reason: e.message }; }
+}
+
+async function probeSsl(zoneId) {
+ if (!zoneId) return { ok: false };
+ try {
+ const j = await cfGet(`/zones/${zoneId}/settings/ssl`);
+ const mode = j.result && j.result.value;
+ return { ok: mode === 'strict' || mode === 'full', mode };
+ } catch (e) { return { ok: false, reason: e.message }; }
+}
+
+function probeNsAtRegistrar() {
+ try {
+ const out = execSync(`dig +short NS ${DOMAIN} @8.8.8.8 +time=2 +tries=1`, { encoding: 'utf8', timeout: 4000 }).trim();
+ const ns = out.split('\n').filter(Boolean).sort();
+ const isCf = ns.some(n => /cloudflare/i.test(n));
+ return { ok: isCf, current: ns };
+ } catch (e) { return { ok: false, reason: e.message }; }
+}
+
+async function probeOrigin() {
+ try {
+ const res = await fetch(`${ORIGIN}/health`, { signal: AbortSignal.timeout(8000) });
+ if (res.ok) return { ok: true, http: res.status };
+ return { ok: false, http: res.status };
+ } catch (e) { return { ok: false, reason: e.message }; }
+}
+
+async function probeHttps() {
+ try {
+ const res = await fetch(`https://${DOMAIN}/`, { signal: AbortSignal.timeout(8000) });
+ const html = await res.text();
+ // Reject GoDaddy parking page false positives — must contain wallco.ai brand markers
+ const isWallco = /wallco\.ai/i.test(html) && (/AI-original/i.test(html) || /wallco.{0,12}brand/i.test(html));
+ return { ok: isWallco, http: res.status, is_parking: !isWallco };
+ } catch (e) { return { ok: false, reason: e.message }; }
+}
+
+async function probeGa4() {
+ try {
+ const res = await fetch(`${ORIGIN}/`, { signal: AbortSignal.timeout(8000) });
+ const html = await res.text();
+ const m = html.match(/G-[A-Z0-9]{8,10}/);
+ return { ok: !!m, measurement_id: m ? m[0] : null };
+ } catch (e) { return { ok: false, reason: e.message }; }
+}
+
+async function probeInlineEditor() {
+ try {
+ const res = await fetch(`${ORIGIN}/api/_inline/state?path=/`, { signal: AbortSignal.timeout(8000) });
+ return { ok: res.status === 401, http: res.status, note: 'expected 401 anonymous' };
+ } catch (e) { return { ok: false, reason: e.message }; }
+}
+
+async function main() {
+ const zone = await probeCfZone();
+ const [dns, ns, ssl, origin, https, ga4, inlineEd] = await Promise.all([
+ probeDnsRecords(zone.zone_id),
+ Promise.resolve(probeNsAtRegistrar()),
+ probeSsl(zone.zone_id),
+ probeOrigin(),
+ probeHttps(),
+ probeGa4(),
+ probeInlineEditor()
+ ]);
+ console.log(JSON.stringify({
+ domain: DOMAIN,
+ checked_at: new Date().toISOString(),
+ steps: { zone, dns, ns, ssl, origin, https, ga4, inlineEd }
+ }));
+}
+
+if (require.main === module) main().catch(e => { console.error(e); process.exit(1); });
+module.exports = { main };
diff --git a/server.js b/server.js
index 9e4952b..5a3529b 100644
--- a/server.js
+++ b/server.js
@@ -122,6 +122,94 @@ function sortDesigns(designs, sort) {
}
}
+// ── Go-Live status (admin checklist)
+app.get('/admin/golive.json', async (_req, res) => {
+ try {
+ // Run the same module the CLI uses, but inline so we can stream
+ const { main } = require('./scripts/golive_status');
+ // Capture stdout from main() — easiest: run the same probes here
+ const out = execSync(`node ${path.join(__dirname, 'scripts/golive_status.js')}`, { encoding: 'utf8', timeout: 25000 });
+ res.type('application/json').send(out.trim());
+ } catch (e) {
+ res.status(500).json({ error: e.message });
+ }
+});
+
+app.get('/admin/golive', (_req, res) => {
+ res.type('html').send(`<!doctype html>
+<html lang="en"><head>
+<meta charset="utf-8"><title>wallco.ai — Go-Live Checklist</title>
+<style>
+ body { font:14px/1.5 -apple-system,system-ui,sans-serif; margin:0; padding:24px 32px; background:#0f0e0c; color:#e8e2d6; }
+ h1 { font-weight:300; font-size:24px; margin:0 0 4px; letter-spacing:.5px; }
+ .sub { color:#888; font-size:12px; margin-bottom:24px; }
+ .step { background:#1a1816; border:1px solid #2a2825; border-radius:6px; padding:14px 18px; margin-bottom:10px; display:flex; gap:14px; align-items:flex-start; }
+ .step.ok { border-left:4px solid #6dbe6d; }
+ .step.fail { border-left:4px solid #c0584a; }
+ .step.warn { border-left:4px solid #d2b15c; }
+ .step .ico { font-size:20px; line-height:1; min-width:24px; }
+ .step .body { flex:1; }
+ .step .title { font-weight:500; margin-bottom:2px; }
+ .step .desc { color:#888; font-size:12px; }
+ .step .cmd { font-family:ui-monospace,monospace; font-size:11.5px; background:#0a0907; padding:6px 10px; border-radius:3px; margin-top:6px; color:#bba; display:inline-block; }
+ .step .btn { background:#2a2825; color:#d2b15c; border:1px solid #3a3631; padding:5px 10px; border-radius:3px; font-size:11.5px; cursor:pointer; margin-top:6px; text-decoration:none; display:inline-block; }
+ .step .btn:hover { background:#3a3631; color:#fff; }
+ .reload { background:#2a2825; color:#d2b15c; border:1px solid #3a3631; padding:7px 14px; border-radius:3px; cursor:pointer; font-size:12px; margin-bottom:18px; }
+ .stale { color:#888; font-size:11px; }
+ pre.json { background:#0a0907; padding:12px; border-radius:4px; font-size:11px; color:#bba; max-height:300px; overflow:auto; }
+</style></head>
+<body>
+<h1>wallco.ai — Go-Live Checklist</h1>
+<div class="sub">Auto-refreshes every 30 s. Each step is idempotent.</div>
+<button class="reload" onclick="load(true)">↻ Refresh now</button>
+<div id="checklist">Loading…</div>
+<details style="margin-top:24px"><summary style="color:#888;cursor:pointer;font-size:12px">raw status JSON</summary><pre class="json" id="raw"></pre></details>
+<script>
+const STEPS = [
+ { key:'zone', title:'1. Cloudflare zone exists', desc:'CF API can\\'t create zones with current token scope. One-time manual click.',
+ fix:'Open <a href="https://dash.cloudflare.com" target="_blank" style="color:#d2b15c">dash.cloudflare.com</a> → Add a site → wallco.ai → Free plan. Then refresh this page.' },
+ { key:'dns', title:'2. DNS records (A, www, MX, SPF, DKIM, DMARC)', desc:'Six required records on the CF zone. Run after step 1 succeeds.',
+ cmd:'bash scripts/golive.sh dns' },
+ { key:'ns', title:'3. GoDaddy NS swapped to Cloudflare', desc:'Domain still on ns41/42.domaincontrol.com — must switch to the CF nameservers.',
+ cmd:'bash scripts/golive.sh ns-swap' },
+ { key:'origin', title:'4. Kamatera origin /health OK', desc:'pm2 wallco-ai on :9905, nginx vhost.', cmd:null },
+ { key:'ssl', title:'5. CF SSL mode = Full (Strict) + Always-HTTPS', desc:'Run after DNS propagates.',
+ cmd:'bash scripts/golive.sh cf-ssl' },
+ { key:'https', title:'6. https://wallco.ai/health 200', desc:'Certbot must have issued LE cert.',
+ cmd:'bash scripts/golive.sh cert' },
+ { key:'ga4', title:'7. GA4 gtag in HTML', desc:'Property G-6V7HSH6K1M.' },
+ { key:'inlineEd', title:'8. Inline editor gate (401 anon)', desc:'Admin-cookie required for /api/_inline/save.' }
+];
+
+function rowClass(ok) { return ok ? 'ok' : 'fail'; }
+function ico(ok) { return ok ? '✅' : '⬜️'; }
+
+async function load(forceFresh) {
+ const r = await fetch('/admin/golive.json' + (forceFresh ? '?t=' + Date.now() : ''));
+ const j = await r.json();
+ const s = j.steps || {};
+ const cl = document.getElementById('checklist');
+ cl.innerHTML = STEPS.map(st => {
+ const v = s[st.key] || { ok: false };
+ const klass = rowClass(v.ok);
+ let detail = '';
+ if (st.key === 'zone' && !v.ok) detail = '<div class="desc" style="margin-top:6px">' + st.fix + '</div>';
+ if (st.key === 'zone' && v.ok) detail = '<div class="desc" style="margin-top:6px">zone_id: ' + (v.zone_id||'?') + ' · NS: ' + (v.name_servers||[]).join(', ') + '</div>';
+ if (st.key === 'dns' && !v.ok && v.reason !== 'no_zone') detail = '<div class="desc">a:' + (v.a||0) + ' www:' + (v.www||0) + ' mx:' + (v.mx||0) + ' spf:' + (v.spf||0) + ' dkim:' + (v.dkim||0) + ' dmarc:' + (v.dmarc||0) + '</div>';
+ if (st.key === 'ns') detail = '<div class="desc">current: ' + ((v.current||[]).join(', ') || '?') + '</div>';
+ if (st.key === 'https' && v.http) detail = '<div class="desc">HTTP ' + v.http + (v.reason ? ' · ' + v.reason : '') + '</div>';
+ if (st.key === 'origin' && v.http)detail = '<div class="desc">HTTP ' + v.http + '</div>';
+ if (st.key === 'ga4' && v.measurement_id) detail = '<div class="desc">' + v.measurement_id + '</div>';
+ const cmdLine = st.cmd ? '<div class="cmd">' + st.cmd + '</div>' : '';
+ return '<div class="step ' + klass + '"><div class="ico">' + ico(v.ok) + '</div><div class="body"><div class="title">' + st.title + '</div><div class="desc">' + st.desc + '</div>' + detail + cmdLine + '</div></div>';
+ }).join('');
+ document.getElementById('raw').textContent = JSON.stringify(j, null, 2);
+}
+load();
+setInterval(() => load(true), 30000);
+</script></body></html>`);
+});
+
// ── Inline editor admin check (stub — checks dw_auth cookie)
function isAdmin(req) {
const cookie = req.cookies?.dw_auth || req.headers['x-dw-auth'] || '';
← e10e4dd fix wallco.ai age-gate stuck overlay: drop inline display:fl
·
back to Wallco Ai
·
spoonflower: upload-new-product + pull-DW-account scripts 98ac0cf →