← back to Muralsource Internal
contrarian FIX-FIRST: purge dead astek files (scraper/monthly-refresh/2 launchd plists), fix .deploy.conf (safe path, cannot overwrite live astek), honest package.json; replace lying 15-min refresh countdown with truthful static-snapshot-age pill (no cron on this line)
87ab73056a7bb23b5e88e808307af67da24f7a52 · 2026-07-26 19:02:16 -0700 · Steve Abrams
Files touched
M .deploy.confD launchd/com.steve.astek-data-refresh.plistD launchd/com.steve.astek-monthly-refresh.plistM package.jsonM public/index.htmlD scripts/monthly-refresh.shD scripts/scrape-astek.jsM server.js
Diff
commit 87ab73056a7bb23b5e88e808307af67da24f7a52
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Jul 26 19:02:16 2026 -0700
contrarian FIX-FIRST: purge dead astek files (scraper/monthly-refresh/2 launchd plists), fix .deploy.conf (safe path, cannot overwrite live astek), honest package.json; replace lying 15-min refresh countdown with truthful static-snapshot-age pill (no cron on this line)
---
.deploy.conf | 14 +-
launchd/com.steve.astek-data-refresh.plist | 17 --
launchd/com.steve.astek-monthly-refresh.plist | 23 ---
package.json | 6 +-
public/index.html | 45 +++--
scripts/monthly-refresh.sh | 41 ----
scripts/scrape-astek.js | 263 --------------------------
server.js | 10 +-
8 files changed, 39 insertions(+), 380 deletions(-)
diff --git a/.deploy.conf b/.deploy.conf
index 2755777..85ecfca 100644
--- a/.deploy.conf
+++ b/.deploy.conf
@@ -1,6 +1,8 @@
-# astek-landing deploy config (GATED — do not run without Steve's approval)
-PROJECT_NAME="astek-landing"
-DEPLOY_PATH="/root/Projects/astek-landing"
-# Live Kamatera port is 9960 (pm2 id 272, deploy 08fa699). 9944 was the local dev port.
-HEALTH_URL="http://localhost:9960/healthz"
-PORT="9960"
+# muralsource-internal deploy config (GATED — do not run without Steve's approval)
+# LOCAL-ONLY for now: internal line viewer on Mac2 pm2 :9947. No Kamatera target
+# provisioned yet — DEPLOY_PATH is a NEW path so a stray /deploy can NEVER overwrite
+# the live astek site (the clone-origin). Provision the Kamatera port before deploying.
+PROJECT_NAME="muralsource-internal"
+DEPLOY_PATH="/root/public-projects/muralsource-internal"
+HEALTH_URL="http://localhost:9947/healthz"
+PORT="9947"
diff --git a/launchd/com.steve.astek-data-refresh.plist b/launchd/com.steve.astek-data-refresh.plist
deleted file mode 100644
index dae3868..0000000
--- a/launchd/com.steve.astek-data-refresh.plist
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0"><dict>
- <key>Label</key><string>com.steve.astek-data-refresh</string>
- <key>ProgramArguments</key><array><string>/opt/homebrew/bin/node</string><string>/Users/macstudio3/Projects/astek-landing/scripts/build-products-json.js</string></array>
- <key>WorkingDirectory</key><string>/Users/macstudio3/Projects/astek-landing/scripts</string>
- <key>StartCalendarInterval</key><array>
- <dict><key>Minute</key><integer>0</integer></dict>
- <dict><key>Minute</key><integer>15</integer></dict>
- <dict><key>Minute</key><integer>30</integer></dict>
- <dict><key>Minute</key><integer>45</integer></dict>
- </array>
- <key>RunAtLoad</key><true/>
- <key>StandardOutPath</key><string>/tmp/astek-data-refresh.log</string>
- <key>StandardErrorPath</key><string>/tmp/astek-data-refresh.log</string>
- <key>EnvironmentVariables</key><dict><key>PATH</key><string>/usr/local/bin:/usr/bin:/bin</string></dict>
-</dict></plist>
diff --git a/launchd/com.steve.astek-monthly-refresh.plist b/launchd/com.steve.astek-monthly-refresh.plist
deleted file mode 100644
index 284b4da..0000000
--- a/launchd/com.steve.astek-monthly-refresh.plist
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0"><dict>
- <key>Label</key><string>com.steve.astek-monthly-refresh</string>
- <key>ProgramArguments</key>
- <array>
- <string>/bin/bash</string>
- <string>/Users/macstudio3/Projects/astek-landing/scripts/monthly-refresh.sh</string>
- </array>
- <key>WorkingDirectory</key><string>/Users/macstudio3/Projects/astek-landing</string>
- <!-- 1st of every month, 04:30 local -->
- <key>StartCalendarInterval</key>
- <dict>
- <key>Day</key><integer>1</integer>
- <key>Hour</key><integer>4</integer>
- <key>Minute</key><integer>30</integer>
- </dict>
- <key>RunAtLoad</key><false/>
- <key>StandardOutPath</key><string>/tmp/astek-monthly-refresh.log</string>
- <key>StandardErrorPath</key><string>/tmp/astek-monthly-refresh.log</string>
- <key>EnvironmentVariables</key>
- <dict><key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string></dict>
-</dict></plist>
diff --git a/package.json b/package.json
index b2c2ffd..0a0ad49 100644
--- a/package.json
+++ b/package.json
@@ -2,11 +2,11 @@
"name": "muralsource-internal",
"version": "0.2.1",
"private": true,
- "description": "Editorial vendor landing for Astek (INTERNAL dw_unified data — not on Shopify)",
+ "description": "Internal basic-auth line viewer for Mural Source (dw_unified.muralsource_catalog — off Shopify, DTD verdict C)",
"scripts": {
- "scrape": "node scripts/scrape-astek.js",
"build-products": "node scripts/build-products-json.js",
- "start": "node server.js"
+ "start": "node server.js",
+ "build": "node scripts/build-products-json.js"
},
"dependencies": {
"compression": "^1.8.1",
diff --git a/public/index.html b/public/index.html
index 1b0ec4a..dffae1f 100644
--- a/public/index.html
+++ b/public/index.html
@@ -414,37 +414,36 @@ async function loadAll(){
}
loadAll();
-/* ── Corner refresh countdown ──────────────────────────────────────────────
- Data is rebuilt from dw_unified every 15 min by a cron. This pill counts down
- to the next refresh (synced to the real data-file timestamp via /api/meta) and
- pulls fresh data in-place when it lands — no manual reload. */
+/* ── Corner snapshot-age pill ───────────────────────────────────────────────
+ HONEST: this internal line has NO auto-refresh cron. data/products.json is a
+ STATIC snapshot rebuilt only when someone runs `npm run build`
+ (node scripts/build-products-json.js) from dw_unified.muralsource_catalog.
+ The pill shows the snapshot's age; it polls /api/meta and hot-swaps ONLY if
+ the file is actually rebuilt (advanced mtime). No fake countdown. */
(function(){
- var meta={lastRefresh:null,intervalSec:900}, target=0, waiting=false;
+ var last=null;
var pill=document.createElement('div');
pill.className='muralsource-refresh-pill';
- pill.title='Mural Source catalog data auto-refreshes from Designer Wallcoverings every 15 minutes';
- pill.innerHTML='<span class="rp-dot"></span><span class="rp-txt">syncing…</span>';
+ pill.title='Static snapshot of the Mural Source catalog (dw_unified.muralsource_catalog). Rebuilt manually — no auto-refresh cron on this internal line.';
+ pill.innerHTML='<span class="rp-dot"></span><span class="rp-txt">snapshot…</span>';
document.body.appendChild(pill);
var txt=pill.querySelector('.rp-txt');
- function fmt(s){s=Math.max(0,Math.round(s));var m=Math.floor(s/60);return m+':'+String(s%60).padStart(2,'0');}
- async function syncMeta(){
+ function age(iso){
+ var s=Math.max(0,(Date.now()-new Date(iso).getTime())/1000);
+ if(s<90) return 'just now';
+ if(s<5400) return Math.round(s/60)+'m ago';
+ if(s<172800) return Math.round(s/3600)+'h ago';
+ return Math.round(s/86400)+'d ago';
+ }
+ async function sync(){
try{
var m=await fetch(location.origin+'/api/meta').then(r=>r.json());
- var advanced = meta.lastRefresh && m.lastRefresh!==meta.lastRefresh;
- meta=m;
- var t=new Date(m.lastRefresh).getTime()+m.intervalSec*1000;
- target = (t<=Date.now()) ? Date.now()+m.intervalSec*1000 : t; // clamp stale files to a fresh cycle
- if(advanced) loadAll();
- return advanced;
- }catch(e){ return false; }
- }
- async function tick(){
- var rem=(target-Date.now())/1000;
- if(rem>0){ txt.textContent='Next refresh in '+fmt(rem); pill.classList.remove('rp-live'); }
- else if(!waiting){ txt.textContent='Updating…'; pill.classList.add('rp-live'); waiting=true;
- var adv=await syncMeta(); if(!adv) target=Date.now()+7000; waiting=false; }
+ if(last && m.lastRefresh!==last) loadAll(); // manual rebuild landed → hot-swap
+ last=m.lastRefresh;
+ txt.textContent='📷 Snapshot · '+age(m.lastRefresh);
+ }catch(e){}
}
- syncMeta().then(function(){ tick(); setInterval(tick,1000); });
+ sync(); setInterval(sync,30000);
})();
</script>
</body></html>
diff --git a/scripts/monthly-refresh.sh b/scripts/monthly-refresh.sh
deleted file mode 100755
index 97ef10b..0000000
--- a/scripts/monthly-refresh.sh
+++ /dev/null
@@ -1,41 +0,0 @@
-#!/bin/bash
-# Astek monthly full refresh: re-scrape astek.com -> astek_catalog -> rebuild
-# snapshot -> deploy the fresh data to the live (gated, internal) microsite.
-#
-# The existing com.steve.astek-data-refresh only REBUILDS the local snapshot
-# from the DB every 15 min; it never re-scrapes the vendor and never deploys.
-# This monthly job closes both gaps. Scrape+rebuild are $0 local; the deploy
-# targets an internal basic-auth microsite (no Shopify, no prices) so an
-# unattended monthly push is low-risk.
-set -uo pipefail
-
-# launchd hands us a minimal PATH — restore node + ssh/rsync (deploy.sh needs them).
-export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
-
-PROJ="/Users/macstudio3/Projects/astek-landing"
-LOG="/tmp/astek-monthly-refresh.log"
-cd "$PROJ" || { echo "$(date '+%F %T') FATAL cd $PROJ failed" >>"$LOG"; exit 1; }
-
-echo "===== $(date '+%F %T') astek monthly refresh START =====" >>"$LOG"
-
-echo "[1/3] scrape astek.com -> astek_catalog" >>"$LOG"
-if ! node scripts/scrape-astek.js >>"$LOG" 2>&1; then
- echo "$(date '+%F %T') SCRAPE FAILED — aborting (snapshot + live left untouched)" >>"$LOG"; exit 1
-fi
-
-echo "[2/3] rebuild data/products.json" >>"$LOG"
-if ! node scripts/build-products-json.js >>"$LOG" 2>&1; then
- echo "$(date '+%F %T') REBUILD FAILED — aborting before deploy" >>"$LOG"; exit 1
-fi
-
-# Commit the refreshed data so the deploy ships a tracked snapshot.
-git add data/products.json data/astek-raw.json 2>/dev/null
-git -c user.email="steve@designerwallcoverings.com" \
- commit -q -m "data: astek monthly refresh $(date '+%F')" 2>/dev/null || true
-
-echo "[3/3] deploy fresh snapshot to Kamatera" >>"$LOG"
-if ! bash /Users/macstudio3/Projects/_shared/scripts/deploy.sh >>"$LOG" 2>&1; then
- echo "$(date '+%F %T') DEPLOY FAILED — local snapshot is fresh, live is stale" >>"$LOG"; exit 1
-fi
-
-echo "===== $(date '+%F %T') astek monthly refresh DONE =====" >>"$LOG"
diff --git a/scripts/scrape-astek.js b/scripts/scrape-astek.js
deleted file mode 100644
index 42d5d69..0000000
--- a/scripts/scrape-astek.js
+++ /dev/null
@@ -1,263 +0,0 @@
-#!/usr/bin/env node
-/**
- * Workstream A — Astek catalog scrape (INTERNAL DATA ONLY, no Shopify).
- * Feed-first: paginate https://www.astek.com/products.json?limit=250&page=N
- * PostgreSQL-FIRST: raw snapshot -> astek_catalog in dw_unified -> register DW SKUs.
- *
- * Cost: $0 (local HTTP). No Browserbase.
- */
-const fs = require('fs');
-const path = require('path');
-const { Pool } = require('pg');
-
-const VENDOR = 'Astek';
-const VENDOR_CODE = 'astek'; // existing canonical vendor_registry row
-const DW_PREFIX = 'DWPX'; // pre-assigned to Astek in vendor_registry (compliant: P,X allowed; 0 SKUs minted)
-const SKU_RANGE_START = 400000; // Astek block (kept distinct from legacy ranges)
-const FEED = 'https://www.astek.com/products.json';
-const RAW_OUT = path.join(__dirname, '..', 'data', 'astek-raw.json');
-
-const PW = (() => {
- try {
- const env = fs.readFileSync(require('os').homedir() + '/Projects/secrets-manager/.env', 'utf8');
- const m = env.match(/^DW_ADMIN_DB_PASSWORD=(.*)$/m);
- return m ? m[1].replace(/^["']|["']$/g, '').trim() : (process.env.PGPASSWORD || '');
- } catch { return process.env.PGPASSWORD || ''; }
-})();
-
-const pool = new Pool({
- host: '127.0.0.1', port: 5432, user: 'dw_admin', database: 'dw_unified', password: PW,
-});
-
-const BANNED = s => (s || '')
- .replace(/\bWallpapers\b/gi, 'Wallcoverings')
- .replace(/\bWallpaper\b/gi, 'Wallcovering');
-
-function toTitleCase(str) {
- const small = new Set(['a','an','the','and','but','or','for','nor','on','at','to','from','by','in','of','with']);
- return (str || '').split(/\s+/).map((w, i) => {
- const lw = w.toLowerCase();
- if (i !== 0 && small.has(lw)) return lw;
- return w.charAt(0).toUpperCase() + w.slice(1);
- }).join(' ').trim();
-}
-
-// Parse specs out of the body_html prose / spec block.
-function parseSpecs(html) {
- const text = (html || '').replace(/<[^>]+>/g, ' ').replace(/&/g, '&').replace(/ /g, ' ').replace(/\s+/g, ' ');
- const grab = (re) => { const m = text.match(re); return m ? m[1].trim() : null; };
- return {
- width: grab(/(?:Roll\s*Width|Width)[:\s]*([0-9][0-9.\s\/"]*(?:in(?:ches)?|"|cm|mm)?)/i),
- length: grab(/(?:Roll\s*Length|Length)[:\s]*([0-9][0-9.\s\/"]*(?:ft|feet|yd|yard|in(?:ches)?|m)?)/i),
- repeat: grab(/(?:Pattern\s*Repeat|Repeat)[:\s]*([0-9][0-9.\s\/"]*(?:in(?:ches)?|"|cm)?)/i),
- material: grab(/(?:Material|Substrate|Type|Content)[:\s]*([A-Za-z][A-Za-z0-9 ,\/\-]{2,40})/i),
- match: grab(/(?:Match)[:\s]*([A-Za-z][A-Za-z ]{2,30})/i),
- };
-}
-
-const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124 Safari/537.36';
-
-// Shopify/CF occasionally returns a transient 503 on the products.json feed
-// (seen 2026-07-13). Retry with backoff so the monthly unattended job doesn't
-// die on a single blip. Raises only after all attempts are exhausted.
-async function fetchFeedPage(url, attempts = 4) {
- let lastStatus = 0;
- for (let i = 0; i < attempts; i++) {
- const res = await fetch(url, { headers: { 'User-Agent': UA } });
- if (res.ok) return res;
- lastStatus = res.status;
- if (i < attempts - 1) await new Promise(r => setTimeout(r, 1500 * (i + 1))); // 1.5s, 3s, 4.5s
- }
- throw new Error(`feed HTTP ${lastStatus} after ${attempts} attempts`);
-}
-
-async function fetchAllProducts() {
- let page = 1, all = [];
- while (true) {
- const url = `${FEED}?limit=250&page=${page}`;
- const res = await fetchFeedPage(url);
- const json = await res.json();
- const products = json.products || [];
- if (products.length === 0) break;
- all.push(...products);
- process.stdout.write(`\r fetched page ${page} (total ${all.length}) `);
- page++;
- if (page > 40) break; // hard safety cap
- await new Promise(r => setTimeout(r, 120)); // polite
- }
- process.stdout.write('\n');
- return all;
-}
-
-async function ensureSchema(client) {
- await client.query(`
- CREATE TABLE IF NOT EXISTS astek_catalog (
- id BIGSERIAL PRIMARY KEY,
- dw_sku TEXT UNIQUE,
- mfr_sku TEXT NOT NULL,
- shopify_product_id BIGINT,
- handle TEXT,
- pattern_name TEXT,
- color_name TEXT,
- title TEXT, -- Pattern Color | Astek (title-cased, banned-word clean)
- vendor TEXT DEFAULT 'Astek',
- product_type TEXT,
- body_html TEXT,
- description_text TEXT,
- price NUMERIC,
- cost NUMERIC,
- width TEXT,
- length TEXT,
- repeat TEXT,
- material TEXT,
- match_type TEXT,
- color_tags TEXT[],
- style_tags TEXT[],
- all_images TEXT[],
- image_url TEXT, -- primary image
- variant_image TEXT,
- product_url TEXT,
- raw JSONB,
- created_at TIMESTAMPTZ DEFAULT NOW(),
- updated_at TIMESTAMPTZ DEFAULT NOW(),
- UNIQUE (mfr_sku)
- );
- CREATE INDEX IF NOT EXISTS astek_catalog_pattern_idx ON astek_catalog (pattern_name);
- CREATE INDEX IF NOT EXISTS astek_catalog_handle_idx ON astek_catalog (handle);
- `);
-}
-
-// registry helpers (inline; mirror lib/sku-registry semantics but with our pooled client)
-async function checkDup(client, mfrSku) {
- const r = await client.query(
- `SELECT dw_sku FROM dw_sku_registry WHERE vendor_prefix=$1 AND mfr_sku=$2`, [DW_PREFIX, mfrSku]);
- return r.rows[0]?.dw_sku || null;
-}
-async function nextSkuNum(client) {
- const r = await client.query(
- `SELECT MAX(CAST(SUBSTRING(dw_sku FROM '[0-9]+$') AS INTEGER)) mx FROM dw_sku_registry WHERE vendor_prefix=$1`,
- [DW_PREFIX]);
- return r.rows[0]?.mx ? (r.rows[0].mx + 1) : SKU_RANGE_START;
-}
-
-async function main() {
- console.log(`\n[Astek scrape] prefix=${DW_PREFIX} range_start=${SKU_RANGE_START}`);
- console.log('Fetching products.json feed ($0 local HTTP)...');
- const products = await fetchAllProducts();
- console.log(`Products: ${products.length}`);
- fs.writeFileSync(RAW_OUT, JSON.stringify({ scraped_at: new Date().toISOString(), count: products.length, products }, null, 0));
- console.log(`Raw snapshot -> ${RAW_OUT}`);
-
- const client = await pool.connect();
- let rows = 0, variants = 0, images = 0, registered = 0, dups = 0, skipped = 0;
- try {
- await ensureSchema(client);
-
- // Seed the sku counter once, then increment locally to avoid N round-trips.
- let counter = await nextSkuNum(client);
-
- for (const p of products) {
- const specs = parseSpecs(p.body_html);
- const descText = BANNED((p.body_html || '').replace(/<[^>]+>/g, ' ').replace(/&/g, '&').replace(/ /g, ' ').replace(/\s+/g, ' ').trim());
- const patternName = BANNED(toTitleCase((p.title || '').replace(/\s*Wallcovering\s*$/i, '').replace(/\s*Wallpaper\s*$/i, '').trim()));
- const colorTags = (p.tags || []).filter(t => /^color_/.test(t)).map(t => t.replace(/^color_/, ''));
- // ONLY style_ tags are real styles — feature_/design_ carry ops junk (instock, pricing, color)
- const styleTags = (p.tags || []).filter(t => /^style_/.test(t)).map(t => t.replace(/^style_/, ''));
- const allImages = (p.images || []).map(i => i.src);
- images += allImages.length;
-
- // Map variant image ids -> src
- const imgById = {};
- for (const im of (p.images || [])) for (const vid of (im.variant_ids || [])) imgById[vid] = im.src;
-
- const vlist = (p.variants || []);
- for (const v of vlist) {
- variants++;
- // MFR SKU: prefer clean variant SKU; fall back to handle+position.
- let mfrSku = (v.sku && v.sku.trim()) ? v.sku.trim() : `${p.handle}-${v.position || v.id}`;
- // Real color name from option/variant title (NEVER "Unknown").
- let colorName = (v.option1 && v.option1.trim()) || (v.title && v.title.trim()) || '';
- if (/^unknown$/i.test(colorName)) colorName = '';
-
- // Title: Pattern Color | Astek (fallback order: color -> mfr sku -> none; never "Unknown")
- let titleColor = colorName ? toTitleCase(colorName) : '';
- let title = titleColor ? `${patternName} ${titleColor} | Astek` : `${patternName} | Astek`;
- if (!patternName) { // last-resort pattern fallback: mfr sku
- title = `${mfrSku}${titleColor ? ' ' + titleColor : ''} | Astek`;
- }
- title = BANNED(title);
-
- const priceNum = (v.price != null && parseFloat(v.price) > 0) ? parseFloat(v.price) : null;
- const variantImg = imgById[v.id] || v.featured_image?.src || allImages[0] || null;
-
- // ---- PostgreSQL FIRST: register DW SKU ----
- // dw_sku_registry.mfr_sku is varchar(50); some Astek feed skus jam color tags in
- // and overflow it — register those under a deterministic hash-truncated key while
- // astek_catalog (TEXT) keeps the full sku.
- const regSku = mfrSku.length <= 50
- ? mfrSku
- : mfrSku.slice(0, 41) + '~' + require('crypto').createHash('sha1').update(mfrSku).digest('hex').slice(0, 8);
- let dwSku = await checkDup(client, regSku);
- if (dwSku) { dups++; }
- else {
- dwSku = `${DW_PREFIX}-${counter}`;
- try {
- await client.query(
- `INSERT INTO dw_sku_registry (dw_sku, vendor_prefix, vendor_name, mfr_sku) VALUES ($1,$2,$3,$4)`,
- [dwSku, DW_PREFIX, VENDOR, regSku]);
- registered++; counter++;
- } catch (e) {
- if (e.code === '23505') { dwSku = await checkDup(client, regSku); dups++; }
- else { console.error('reg err', mfrSku, e.message); skipped++; continue; }
- }
- }
-
- // ---- upsert catalog row ----
- await client.query(`
- INSERT INTO astek_catalog
- (dw_sku, mfr_sku, handle, pattern_name, color_name, title, product_type,
- body_html, description_text, price, cost, width, length, repeat, material, match_type,
- color_tags, style_tags, all_images, image_url, variant_image, product_url, raw, updated_at)
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,NOW())
- ON CONFLICT (mfr_sku) DO UPDATE SET
- dw_sku=EXCLUDED.dw_sku, pattern_name=EXCLUDED.pattern_name, color_name=EXCLUDED.color_name,
- title=EXCLUDED.title, body_html=EXCLUDED.body_html, description_text=EXCLUDED.description_text,
- price=EXCLUDED.price, width=EXCLUDED.width, length=EXCLUDED.length, repeat=EXCLUDED.repeat,
- material=EXCLUDED.material, match_type=EXCLUDED.match_type, color_tags=EXCLUDED.color_tags,
- style_tags=EXCLUDED.style_tags, all_images=EXCLUDED.all_images, image_url=EXCLUDED.image_url,
- variant_image=EXCLUDED.variant_image, product_url=EXCLUDED.product_url, raw=EXCLUDED.raw,
- updated_at=NOW()
- `, [
- dwSku, mfrSku, p.handle, patternName, colorName ? toTitleCase(colorName) : null, title, p.product_type,
- p.body_html, descText, priceNum, null, specs.width, specs.length, specs.repeat, specs.material, specs.match,
- colorTags, styleTags, allImages, allImages[0] || null, variantImg,
- `https://www.astek.com/products/${p.handle}`, JSON.stringify(v),
- ]);
- rows++;
- }
- }
-
- // update the existing canonical Astek vendor_registry row (internal, skip_shopify true)
- await client.query(`
- UPDATE vendor_registry
- SET sku_prefix=$1, sku_range_start=$2, catalog_table='astek_catalog',
- skip_shopify=true, is_active=true,
- notes='INTERNAL ONLY — astek.designerwallcoverings.com landing; deliberately NOT on Shopify',
- updated_at=NOW()
- WHERE vendor_code=$3
- `, [DW_PREFIX + '-', SKU_RANGE_START, VENDOR_CODE]).catch(e => console.warn('vendor_registry update skipped:', e.message));
-
- } finally {
- client.release();
- }
-
- const tot = await pool.query('SELECT COUNT(*) c, COUNT(DISTINCT pattern_name) p FROM astek_catalog');
- console.log(`\n=== DONE ===`);
- console.log(`catalog rows (variants): ${rows} | distinct patterns: ${tot.rows[0].p}`);
- console.log(`variants seen: ${variants} | images seen: ${images}`);
- console.log(`DW SKUs newly registered: ${registered} | dupes reused: ${dups} | skipped: ${skipped}`);
- console.log(`astek_catalog total rows now: ${tot.rows[0].c}`);
- await pool.end();
-}
-
-main().catch(e => { console.error('FATAL', e); process.exit(1); });
diff --git a/server.js b/server.js
index 37135b9..f6bd0bf 100644
--- a/server.js
+++ b/server.js
@@ -34,7 +34,9 @@ app.use(express.json({ limit: '2mb' }));
let SNAP = { products: [], facets: { total: 0, books: [], series: [], colors: [] } };
let LIGHT = []; // grid-index payload — heavy PDP-only fields stripped
let LAST_REFRESH = new Date().toISOString();
-const REFRESH_INTERVAL_SEC = Number(process.env.REFRESH_INTERVAL_SEC || 900); // 15 min, matches the cron
+// STATIC snapshot line: no auto-refresh cron. products.json is rebuilt manually
+// (npm run build). 0 = "no cron" → the corner pill shows snapshot age, not a countdown.
+const REFRESH_INTERVAL_SEC = Number(process.env.REFRESH_INTERVAL_SEC || 0);
function load() {
SNAP = JSON.parse(fs.readFileSync(DATA, 'utf8'));
// the index grid never renders body_html / images[] / eyebrow — keep those PDP-only
@@ -43,7 +45,7 @@ function load() {
console.log(`[muralsource] loaded ${SNAP.products.length} products (data mtime ${LAST_REFRESH})`);
}
load();
-// Hot-reload when the 15-min cron rewrites data/products.json — no restart needed.
+// Hot-reload if data/products.json is rebuilt manually (npm run build) — no restart needed.
fs.watchFile(DATA, { interval: 5000 }, (cur, prev) => {
if (cur.mtimeMs !== prev.mtimeMs) { try { load(); } catch (e) { console.error('[muralsource] reload failed:', e.message); } }
});
@@ -77,9 +79,9 @@ const CONFIG = {
};
// vendorMeta (phone / our account # / discount / pricing model) rides in from
-// data/products.json — the 15-min refresh keeps it current from PG.
+// data/products.json — refreshed when the snapshot is rebuilt manually (npm run build).
app.get('/api/config', (_req, res) => res.json({ ...CONFIG, vendorMeta: SNAP.vendor || null }));
-// Refresh metadata — drives the corner countdown pill on the landing.
+// Snapshot metadata — drives the corner snapshot-age pill (intervalSec 0 = no cron).
app.get('/api/meta', (_req, res) => res.json({
lastRefresh: LAST_REFRESH, intervalSec: REFRESH_INTERVAL_SEC,
count: SNAP.products.length, now: new Date().toISOString(),
← 71f0ef9 auto-save: 2026-07-26T16:44:36 (1 files) — public/index.html
·
back to Muralsource Internal
·
(newest)