← back to Wallco Ai
Shopify: bump all wallco/SP scripts to API 2026-01 · add read_orders scope preflight in fetch_recent_orders_palette.js with admin-link-out helper · admin/_devlogin restored · inspiration→generate flow working (3 designs in ~90s, vendor-anonymous prompts)
6debe836225fce45ab96caccd64e9145746d7328 · 2026-05-11 18:51:00 -0700 · Steve
Files touched
M data/chip-chats.jsonM data/designs.jsonM data/moodboards.jsonM data/pairings.jsonM data/reviews.jsonM data/vision-cache.jsonM public/js/admin.jsM scripts/fetch_recent_orders_palette.jsM server.jsM src/admin.js
Diff
commit 6debe836225fce45ab96caccd64e9145746d7328
Author: Steve <steve@designerwallcoverings.com>
Date: Mon May 11 18:51:00 2026 -0700
Shopify: bump all wallco/SP scripts to API 2026-01 · add read_orders scope preflight in fetch_recent_orders_palette.js with admin-link-out helper · admin/_devlogin restored · inspiration→generate flow working (3 designs in ~90s, vendor-anonymous prompts)
---
data/chip-chats.json | 28 +----
data/designs.json | 195 +++++++++++++++++++++++++++++++++
data/moodboards.json | 12 +-
data/pairings.json | 83 ++------------
data/reviews.json | 13 ++-
data/vision-cache.json | 3 +-
public/js/admin.js | 40 ++++++-
scripts/fetch_recent_orders_palette.js | 41 ++++++-
server.js | 34 ++++++
src/admin.js | 87 +++++++++++++--
10 files changed, 410 insertions(+), 126 deletions(-)
diff --git a/data/chip-chats.json b/data/chip-chats.json
index 98964c0..eec8b78 100644
--- a/data/chip-chats.json
+++ b/data/chip-chats.json
@@ -1,36 +1,14 @@
{
- "11:cat-floral": [
- {
- "role": "user",
- "content": "Why is this floral chip relevant to my collection?",
- "ts": "2026-05-12T00:15:21.709Z"
- },
- {
- "role": "assistant",
- "content": "The \"floral\" chip is relevant as the design features soft, organic floral motifs in a warm, earthy palette (#9c9172), evoking natural textures and botanical elegance. The seamless tile format ensures the floral pattern flows continuously, ideal for creating immersive, nature-inspired wallcoverings. To enhance the chip's impact, consider amplifying the contrast between floral elements and background tones or adjusting scale for bolder visual presence.",
- "ts": "2026-05-12T00:15:21.709Z"
- }
- ],
"13:cat-floral": [
{
"role": "user",
"content": "What scale of room does this work in?",
- "ts": "2026-05-12T00:58:57.673Z"
- },
- {
- "role": "assistant",
- "content": "\"Honey Garden No.13\" works well in medium to large spaces due to its rich, earthy tone (#87774c) and bold floral motifs, which add visual depth without overwhelming smaller areas. For smaller rooms, consider reducing the pattern scale or lightening the dominant color to maintain balance.",
- "ts": "2026-05-12T00:58:57.673Z"
- },
- {
- "role": "user",
- "content": "What scale of room does this work in?",
- "ts": "2026-05-12T01:01:08.389Z"
+ "ts": "2026-05-12T01:38:04.429Z"
},
{
"role": "assistant",
- "content": "\"Honey Garden No.13\" works best in medium to large rooms, where its bold floral motifs and deep earthy tone (#87774c) create a rich, immersive atmosphere. In smaller spaces, reduce the pattern scale or use a lighter variant of the dominant color to avoid visual overcrowding.",
- "ts": "2026-05-12T01:01:08.389Z"
+ "content": "\"Honey Garden No.13\" works well in medium to large rooms due to its floral scale—bold, stylized blooms in #87774c (warm terracotta) add visual depth without overwhelming space. For smaller rooms, consider reducing floral size or lightening the palette to avoid claustrophobia.",
+ "ts": "2026-05-12T01:38:04.430Z"
}
]
}
\ No newline at end of file
diff --git a/data/designs.json b/data/designs.json
index fac2dd9..cc108ed 100644
--- a/data/designs.json
+++ b/data/designs.json
@@ -463,5 +463,200 @@
"seed": 536522826,
"is_published": false,
"created_at": "2026-05-11T23:56:17.861881+00:00"
+ },
+ {
+ "id": 42,
+ "kind": "seamless_tile",
+ "category": "damask",
+ "dominant_hex": "#162f36",
+ "saturation": 0.421,
+ "title": "Marine Heirloom No.42",
+ "handle": "wallco-0042",
+ "image_url": "/designs/img/1778546385937_218100061.png",
+ "filename": "1778546385937_218100061.png",
+ "generator": "Replicate SDXL",
+ "seed": 218100061,
+ "is_published": false,
+ "created_at": "2026-05-12T00:39:53.816852+00:00"
+ },
+ {
+ "id": 43,
+ "kind": "seamless_tile",
+ "category": "floral",
+ "dominant_hex": "#683621",
+ "saturation": 0.518,
+ "title": "Amber Garden No.43",
+ "handle": "wallco-0043",
+ "image_url": "/designs/img/1778546451102_1288400340.png",
+ "filename": "1778546451102_1288400340.png",
+ "generator": "Replicate SDXL",
+ "seed": 1288400340,
+ "is_published": false,
+ "created_at": "2026-05-12T00:40:51.578874+00:00"
+ },
+ {
+ "id": 44,
+ "kind": "seamless_tile",
+ "category": "arts and crafts",
+ "dominant_hex": "#11242c",
+ "saturation": 0.443,
+ "title": "Sapphire Reverie No.44",
+ "handle": "wallco-0044",
+ "image_url": "/designs/img/1778547697906_1698245296.png",
+ "filename": "1778547697906_1698245296.png",
+ "generator": "Replicate SDXL",
+ "seed": 1698245296,
+ "is_published": false,
+ "created_at": "2026-05-12T01:01:45.757758+00:00"
+ },
+ {
+ "id": 45,
+ "kind": "seamless_tile",
+ "category": "damask",
+ "dominant_hex": "#465541",
+ "saturation": 0.133,
+ "title": "Sage Baroque No.45",
+ "handle": "wallco-0045",
+ "image_url": "/designs/img/1778548235154_224020684.png",
+ "filename": "1778548235154_224020684.png",
+ "generator": "Replicate SDXL",
+ "seed": 224020684,
+ "is_published": false,
+ "created_at": "2026-05-12T01:13:12.328157+00:00"
+ },
+ {
+ "id": 46,
+ "kind": "seamless_tile",
+ "category": "damask",
+ "dominant_hex": "#2e403d",
+ "saturation": 0.164,
+ "title": "Marine Damask No.46",
+ "handle": "wallco-0046",
+ "image_url": "/designs/img/1778550250324_1309443231.png",
+ "filename": "1778550250324_1309443231.png",
+ "generator": "Replicate SDXL",
+ "seed": 1309443231,
+ "is_published": false,
+ "created_at": "2026-05-12T01:44:21.390474+00:00"
+ },
+ {
+ "id": 47,
+ "kind": "seamless_tile",
+ "category": "damask",
+ "dominant_hex": "#eee6d3",
+ "saturation": 0.443,
+ "title": "Honey Heirloom No.47",
+ "handle": "wallco-0047",
+ "image_url": "/designs/img/1778550261052_125177861.png",
+ "filename": "1778550261052_125177861.png",
+ "generator": "Replicate SDXL",
+ "seed": 125177861,
+ "is_published": false,
+ "created_at": "2026-05-12T01:44:29.012184+00:00"
+ },
+ {
+ "id": 48,
+ "kind": "seamless_tile",
+ "category": "damask",
+ "dominant_hex": "#2b3535",
+ "saturation": 0.104,
+ "title": "Marine Atelier No.48",
+ "handle": "wallco-0048",
+ "image_url": "/designs/img/1778550269483_2146043558.png",
+ "filename": "1778550269483_2146043558.png",
+ "generator": "Replicate SDXL",
+ "seed": 2146043558,
+ "is_published": false,
+ "created_at": "2026-05-12T01:44:37.051318+00:00"
+ },
+ {
+ "id": 49,
+ "kind": "seamless_tile",
+ "category": "damask",
+ "dominant_hex": "#425256",
+ "saturation": 0.132,
+ "title": "Marine Salon No.49",
+ "handle": "wallco-0049",
+ "image_url": "/designs/img/1778550277437_1181433269.png",
+ "filename": "1778550277437_1181433269.png",
+ "generator": "Replicate SDXL",
+ "seed": 1181433269,
+ "is_published": false,
+ "created_at": "2026-05-12T01:44:45.205345+00:00"
+ },
+ {
+ "id": 50,
+ "kind": "seamless_tile",
+ "category": "damask",
+ "dominant_hex": "#0c0a0a",
+ "saturation": 0.091,
+ "title": "Rose Baroque No.50",
+ "handle": "wallco-0050",
+ "image_url": "/designs/img/1778550285656_1438445289.png",
+ "filename": "1778550285656_1438445289.png",
+ "generator": "Replicate SDXL",
+ "seed": 1438445289,
+ "is_published": false,
+ "created_at": "2026-05-12T01:45:02.91115+00:00"
+ },
+ {
+ "id": 51,
+ "kind": "seamless_tile",
+ "category": "mixed",
+ "dominant_hex": "#a17f5d",
+ "saturation": 0.268,
+ "title": "Amber Studio No.51",
+ "handle": "wallco-0051",
+ "image_url": "/designs/img/1778550287606_1205386950.png",
+ "filename": "1778550287606_1205386950.png",
+ "generator": "Replicate SDXL",
+ "seed": 1205386950,
+ "is_published": false,
+ "created_at": "2026-05-12T01:45:11.421649+00:00"
+ },
+ {
+ "id": 52,
+ "kind": "seamless_tile",
+ "category": "mixed",
+ "dominant_hex": "#9e8ab3",
+ "saturation": 0.212,
+ "title": "Mauve Origin No.52",
+ "handle": "wallco-0052",
+ "image_url": "/designs/img/1778550319969_275538065.png",
+ "filename": "1778550319969_275538065.png",
+ "generator": "Replicate SDXL",
+ "seed": 275538065,
+ "is_published": false,
+ "created_at": "2026-05-12T01:45:45.427434+00:00"
+ },
+ {
+ "id": 53,
+ "kind": "seamless_tile",
+ "category": "mixed",
+ "dominant_hex": "#7e8897",
+ "saturation": 0.107,
+ "title": "Sapphire Folio No.53",
+ "handle": "wallco-0053",
+ "image_url": "/designs/img/1778550352534_1025904412.png",
+ "filename": "1778550352534_1025904412.png",
+ "generator": "Replicate SDXL",
+ "seed": 1025904412,
+ "is_published": false,
+ "created_at": "2026-05-12T01:46:02.437832+00:00"
+ },
+ {
+ "id": 54,
+ "kind": "seamless_tile",
+ "category": "damask",
+ "dominant_hex": "#dfc492",
+ "saturation": 0.546,
+ "title": "Honey Salon No.54",
+ "handle": "wallco-0054",
+ "image_url": "/designs/img/1778550372683_1513008276.png",
+ "filename": "1778550372683_1513008276.png",
+ "generator": "Replicate SDXL",
+ "seed": 1513008276,
+ "is_published": false,
+ "created_at": "2026-05-12T01:46:22.491814+00:00"
}
]
\ No newline at end of file
diff --git a/data/moodboards.json b/data/moodboards.json
index e48cd92..872b901 100644
--- a/data/moodboards.json
+++ b/data/moodboards.json
@@ -1,14 +1,4 @@
{
- "11": [
- {
- "pin_key": "c:#5d73b1",
- "type": "color",
- "hex": "#5d73b1",
- "label": "test",
- "reason": "test",
- "pinned_at": "2026-05-12T00:43:50.526Z"
- }
- ],
"14": [
{
"pin_key": "d:12",
@@ -20,7 +10,7 @@
"hex": "#a09279",
"idx": 0,
"pinned": false,
- "pinned_at": "2026-05-12T01:03:49.316Z"
+ "pinned_at": "2026-05-12T01:39:15.311Z"
}
]
}
\ No newline at end of file
diff --git a/data/pairings.json b/data/pairings.json
index 07bfee2..9c98444 100644
--- a/data/pairings.json
+++ b/data/pairings.json
@@ -1,75 +1,8 @@
{
- "11": {
- "design_id": 11,
- "vision_description": "Floral pattern with pink peonies on a light background; large scale repeat; formal, elegant, and luxurious mood.",
- "generated_at": "2026-05-12T00:47:19.449Z",
- "suggestions": [
- {
- "type": "design",
- "design_id": 24,
- "label": "Sapphire Floret No.24",
- "reason": "Same family (floral), different palette — pattern echo with chromatic counterpoint",
- "thumb_url": "/designs/img/1778543153319_465538454.png",
- "hex": "#0b1730",
- "idx": 0,
- "pin_key": "d:24",
- "pinned": false
- },
- {
- "type": "design",
- "design_id": 36,
- "label": "Honey Studio No.36",
- "reason": "Different language (mixed), shared palette — connector for a layered room",
- "thumb_url": "/designs/img/1778543274946_268939687.png",
- "hex": "#8f8b61",
- "idx": 1,
- "pin_key": "d:36",
- "pinned": false
- },
- {
- "type": "color",
- "hex": "#5d73b1",
- "label": "Complement · #5d73b1",
- "reason": "Direct chromatic opposite — trim, hardware, or a single accent piece",
- "idx": 2,
- "pin_key": "c:#5d73b1",
- "pinned": true
- },
- {
- "type": "color",
- "hex": "#9db15d",
- "label": "Analogous · #9db15d",
- "reason": "Neighboring hue — supporting fabric or upholstery, harmonious not loud",
- "idx": 3,
- "pin_key": "c:#9db15d",
- "pinned": false
- },
- {
- "type": "material",
- "label": "Aged brass hardware",
- "reason": "Warm metallics echo pink peonies and add formal luxury without overpowering the delicate floral motif.",
- "icon": "✦",
- "vision_grounded": true,
- "idx": 4,
- "pin_key": "m:aged-brass-hardware",
- "pinned": false
- },
- {
- "type": "material",
- "label": "Walnut wood paneling",
- "reason": "Rich, textured wood contrasts with light background, enhancing depth and sophistication of the elegant floral design.",
- "icon": "✦",
- "vision_grounded": true,
- "idx": 5,
- "pin_key": "m:walnut-wood-paneling",
- "pinned": false
- }
- ]
- },
"14": {
"design_id": 14,
- "vision_description": "Floral pattern with yellow flowers on a dark background; small-scale repeat; formal mood.",
- "generated_at": "2026-05-12T00:59:35.410Z",
+ "vision_description": "Floral pattern with yellow flowers on a dark background; medium scale repeat; formal mood.",
+ "generated_at": "2026-05-12T01:39:04.080Z",
"suggestions": [
{
"type": "design",
@@ -113,22 +46,22 @@
},
{
"type": "material",
- "label": "Gold leaf trim",
- "reason": "Metallic sheen contrasts with dark background, amplifying yellow flowers' vibrancy.",
+ "label": "Gold leaf accent wall",
+ "reason": "Amplifies yellow flowers' warmth; contrasts with dark base for formal elegance.",
"icon": "✦",
"vision_grounded": true,
"idx": 4,
- "pin_key": "m:gold-leaf-trim",
+ "pin_key": "m:gold-leaf-accent-wall",
"pinned": false
},
{
"type": "material",
- "label": "Walnut wood paneling",
- "reason": "Rich, dark wood complements formal tone and deep hue, adding warmth and sophistication.",
+ "label": "Matte black trim molding",
+ "reason": "Sharpens floral focus against deep background; adds structured sophistication.",
"icon": "✦",
"vision_grounded": true,
"idx": 5,
- "pin_key": "m:walnut-wood-paneling",
+ "pin_key": "m:matte-black-trim-molding",
"pinned": false
}
]
diff --git a/data/reviews.json b/data/reviews.json
index b81fd4a..3815ead 100644
--- a/data/reviews.json
+++ b/data/reviews.json
@@ -5,7 +5,16 @@
"color": 5,
"style": 5,
"decision": "keep",
- "why": "The design is strong but lacks vibrancy and modern flair in color and style, warranting refinement before final use.",
- "updated_at": "2026-05-12T01:04:49.816Z"
+ "why": "The muted, earthy color palette lacks vibrancy and contrast, diminishing the floral design's visual impact despite its structured compositio",
+ "updated_at": "2026-05-12T01:36:34.661Z"
+ },
+ "12": {
+ "id": "12",
+ "design": 5,
+ "color": 5,
+ "style": 5,
+ "decision": "reject",
+ "why": "The design lacks visual vibrancy and distinctiveness, resulting in a flat, unengaging pattern that fails to capture attention or stand out i",
+ "updated_at": "2026-05-12T01:37:16.898Z"
}
}
\ No newline at end of file
diff --git a/data/vision-cache.json b/data/vision-cache.json
index f5e4536..e28a026 100644
--- a/data/vision-cache.json
+++ b/data/vision-cache.json
@@ -1,4 +1,3 @@
{
- "11": "Floral pattern with pink peonies on a light background; large scale repeat; formal, elegant, and luxurious mood.",
- "14": "Floral pattern with yellow flowers on a dark background; small-scale repeat; formal mood."
+ "14": "Floral pattern with yellow flowers on a dark background; medium scale repeat; formal mood."
}
\ No newline at end of file
diff --git a/public/js/admin.js b/public/js/admin.js
index 1489b84..782efe1 100644
--- a/public/js/admin.js
+++ b/public/js/admin.js
@@ -48,7 +48,43 @@
});
});
- // Generate-from-pool button (stub for now — wired to next-tick endpoint)
+ // Generate-from-pool button
const gen = document.getElementById('gen-btn');
- if (gen) gen.addEventListener('click', () => alert('Generate-from-pool — wiring in next tick. Open chat panel meanwhile.'));
+ if (gen) gen.addEventListener('click', async () => {
+ gen.disabled = true;
+ const orig = gen.textContent;
+ gen.textContent = 'Generating 3 designs… (~90s)';
+ try {
+ const r = await fetch('/api/admin/inspiration/generate', {
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
+ credentials: 'include'
+ });
+ const j = await r.json();
+ if (!r.ok) {
+ alert('Generation failed: ' + (j.error || r.status));
+ gen.disabled = false; gen.textContent = orig; return;
+ }
+ const designs = j.designs || [];
+ // Render results above grid
+ const out = document.createElement('div');
+ out.style.cssText = 'margin:20px 0;padding:18px;background:var(--card-bg);border:1px solid var(--gold);border-radius:8px';
+ out.innerHTML = `<h2 style="font:300 22px/1 var(--serif);margin:0 0 10px;color:var(--ink)">${designs.length} new wallco originals from your pool</h2>
+ <div class="p-grid" style="margin-top:10px">
+ ${designs.filter(d => !d.error).map(d => `
+ <a href="/design/${d.id}" class="p-card" style="text-decoration:none;color:inherit">
+ <div class="p-img" style="background-image:url('${d.image_url}')"></div>
+ <div class="p-meta">
+ <div class="p-name">Design #${d.id}</div>
+ <div class="p-coll"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${d.dominant_hex};vertical-align:middle;margin-right:4px"></span>${d.dominant_hex}</div>
+ </div>
+ </a>`).join('')}
+ </div>`;
+ gen.after(out);
+ gen.textContent = `✓ Generated ${designs.length} · click to generate 3 more`;
+ gen.disabled = false;
+ } catch (e) {
+ alert('Generation error: ' + e.message);
+ gen.disabled = false; gen.textContent = orig;
+ }
+ });
})();
diff --git a/scripts/fetch_recent_orders_palette.js b/scripts/fetch_recent_orders_palette.js
index c2b39f1..308b6cf 100644
--- a/scripts/fetch_recent_orders_palette.js
+++ b/scripts/fetch_recent_orders_palette.js
@@ -52,13 +52,49 @@ function sqlStr(v) {
}
async function shopifyJSON(path) {
- const r = await fetch(`${SHOPIFY_BASE}/admin/api/2024-04${path}`, {
+ const r = await fetch(`${SHOPIFY_BASE}/admin/api/2026-01${path}`, {
headers: { 'X-Shopify-Access-Token': ADMIN, 'Accept': 'application/json' },
});
- if (!r.ok) throw new Error(`Shopify ${path} → ${r.status} ${(await r.text()).slice(0, 200)}`);
+ if (!r.ok) {
+ const body = (await r.text()).slice(0, 400);
+ if (r.status === 403 || /requires merchant approval|access_scope|not approved/i.test(body)) {
+ console.error('\n========================================================================');
+ console.error(' Shopify 403 — your custom app is missing the required scope.');
+ console.error('========================================================================');
+ console.error(' This script needs: read_orders');
+ console.error(` Token currently has only: read_products / read_inventory / read_publications`);
+ console.error('');
+ console.error(' FIX (one-time, ~30 seconds):');
+ console.error(` 1. Open https://${SHOPIFY_BASE.replace(/^https?:\/\//,'')}/admin/settings/apps/development`);
+ console.error(' 2. Click your custom app → Configuration → Admin API access scopes');
+ console.error(' 3. Check read_orders → Save → Reinstall the app');
+ console.error(' 4. The SHOPIFY_ADMIN_TOKEN env var stays the same — Shopify keeps it.');
+ console.error('========================================================================\n');
+ }
+ throw new Error(`Shopify ${path} → ${r.status} ${body}`);
+ }
return r.json();
}
+async function preflightScopes() {
+ try {
+ const r = await fetch(`${SHOPIFY_BASE}/admin/oauth/access_scopes.json`, {
+ headers: { 'X-Shopify-Access-Token': ADMIN, 'Accept': 'application/json' }
+ });
+ if (!r.ok) return; // can't pre-check; let the real call surface the error
+ const j = await r.json();
+ const have = (j.access_scopes || []).map(s => s.handle);
+ if (!have.includes('read_orders')) {
+ const msg = `[preflight] Token is missing read_orders scope (have: ${have.join(', ')}).\nAdd it at: https://${SHOPIFY_BASE.replace(/^https?:\/\//,'')}/admin/settings/apps/development\nExpected scope: read_orders`;
+ throw new Error(msg);
+ }
+ console.log('[preflight] Token has read_orders ✓');
+ } catch (e) {
+ if (/read_orders scope/.test(e.message)) throw e;
+ // ignore other preflight failures
+ }
+}
+
async function getImageDominantHex(imageUrl) {
// Download to /tmp + run sips to extract dominant color
const fname = path.join(TMP, path.basename(imageUrl.split('?')[0]));
@@ -90,6 +126,7 @@ async function main() {
const argv = process.argv.slice(2);
const limit = parseInt(argv[argv.indexOf('--limit') + 1] || '50', 10);
+ await preflightScopes();
console.log(`Fetching last ${limit} orders from ${SHOPIFY_BASE}…`);
const orders = (await shopifyJSON(`/orders.json?status=any&limit=${Math.min(limit, 250)}`)).orders || [];
console.log(`Got ${orders.length} orders`);
diff --git a/server.js b/server.js
index a4e8b84..1f3c926 100644
--- a/server.js
+++ b/server.js
@@ -3152,6 +3152,40 @@ app.get('/health', (_req, res) => {
res.json({ ok: true, site: SITE, count: DESIGNS.length, port: PORT });
});
+// ── DEV LOGIN — sets dw_auth HS256 cookie for admin layer + inline editor (dev mode only)
+const __crypto = require('crypto');
+function __b64u(s) { return Buffer.from(s).toString('base64').replace(/=+$/,'').replace(/\+/g,'-').replace(/\//g,'_'); }
+function __sign(secret, payload) {
+ const h = __b64u(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
+ const p = __b64u(JSON.stringify(payload));
+ const sig = __b64u(__crypto.createHmac('sha256', secret).update(h + '.' + p).digest());
+ return `${h}.${p}.${sig}`;
+}
+app.get('/_devlogin', (req, res) => {
+ if ((process.env.AUTH_MODE || 'dev') !== 'dev') return res.status(403).send('dev login disabled in prod');
+ const secret = process.env.AUTH_DEV_SECRET || 'wallco-dev-secret-change-for-prod';
+ const exp = Math.floor(Date.now() / 1000) + 8 * 3600;
+ const token = __sign(secret, {
+ email: 'steve@designerwallcoverings.com',
+ role: 'admin',
+ iss: 'https://auth.agentabrams.com',
+ exp
+ });
+ res.cookie('dw_auth', token, { httpOnly: false, maxAge: 8 * 3600 * 1000, sameSite: 'lax' });
+ const next = req.query.next || '/admin';
+ res.type('text/html').send(`<!doctype html><meta charset="utf-8"><title>Admin login</title>
+<style>body{font:14px/1.5 -apple-system,system-ui,sans-serif;background:#0f0e0c;color:#e8e2d6;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}.card{background:#1a1816;padding:36px 44px;border-radius:10px;border:1px solid #2a2825;max-width:480px}.card h1{font:300 22px/1.2 'Cormorant Garamond',serif;margin:0 0 6px;color:#d2b15c}.card p{color:#bba;margin:6px 0}a{color:#d2b15c;font-weight:500}</style>
+<div class="card">
+ <h1>Admin login set ✓</h1>
+ <p>Logged in as <code>steve@designerwallcoverings.com</code> · expires in 8 hours.</p>
+ <p><a href="${next}">→ Continue to ${next}</a> · <a href="/_devlogout">Log out</a></p>
+</div>`);
+});
+app.get('/_devlogout', (_req, res) => {
+ res.clearCookie('dw_auth');
+ res.type('text/plain').send('Logged out.\n');
+});
+
// ── Start
app.listen(PORT, '0.0.0.0', () => {
console.log(`wallco.ai production server → http://0.0.0.0:${PORT}`);
diff --git a/src/admin.js b/src/admin.js
index c3e8e56..d6ca583 100644
--- a/src/admin.js
+++ b/src/admin.js
@@ -48,7 +48,7 @@ function gate(req, res, next) {
// Top vendors with row count + 4 sample image URLs
function topVendors(limit = 30) {
- const sql = `
+ const inner = `
WITH v AS (
SELECT vendor_code, COUNT(*) AS n
FROM vendor_catalog
@@ -62,26 +62,30 @@ function topVendors(limit = 30) {
SELECT image_url FROM vendor_catalog WHERE vendor_code = v.vendor_code
AND image_url IS NOT NULL AND image_url <> ''
ORDER BY RANDOM() LIMIT 4) sample) AS samples
- FROM v;`;
- return JSON.parse(psql(`SELECT COALESCE(json_agg(t),'[]'::json) FROM (${sql}) t;`) || '[]');
+ FROM v`;
+ return JSON.parse(psql(`SELECT COALESCE(json_agg(t),'[]'::json) FROM (${inner}) t;`) || '[]');
}
function vendorProducts(code, { q, page = 1, per = 60 } = {}) {
+ // Filter cheaply: index on vendor_code first, then narrow string filters.
const where = [`vendor_code=${esc(code)}`, `image_url IS NOT NULL`, `image_url <> ''`];
if (q) {
const qe = esc('%' + q + '%');
- where.push(`(pattern_name ILIKE ${qe} OR composition ILIKE ${qe} OR collection ILIKE ${qe} OR specs::text ILIKE ${qe})`);
+ // Skip specs::text (slow full-doc scan). pattern_name/collection are short text.
+ where.push(`(pattern_name ILIKE ${qe} OR collection ILIKE ${qe} OR product_type ILIKE ${qe})`);
}
const offset = (Math.max(1, parseInt(page, 10)) - 1) * (parseInt(per, 10) || 60);
const sql = `SELECT COALESCE(json_agg(t),'[]'::json) FROM (
SELECT id, vendor_code, mfr_sku, pattern_name, color_name, collection,
- product_type, image_url, product_url, composition, width, pattern_repeat
+ product_type, image_url, product_url, width, pattern_repeat
FROM vendor_catalog WHERE ${where.join(' AND ')}
ORDER BY pattern_name NULLS LAST, mfr_sku
LIMIT ${per | 0} OFFSET ${offset}) t;`;
return JSON.parse(psql(sql) || '[]');
}
+// Aesthetic search — cap to 200 hits, search only pattern_name + collection + product_type
+// (no specs::text scan = ~50× faster on the 183K vendor_catalog).
function aestheticSearch(tag, { limit = 100 } = {}) {
const qe = esc('%' + tag + '%');
const sql = `SELECT COALESCE(json_agg(t),'[]'::json) FROM (
@@ -89,8 +93,8 @@ function aestheticSearch(tag, { limit = 100 } = {}) {
image_url, product_url
FROM vendor_catalog
WHERE image_url IS NOT NULL AND image_url <> ''
- AND (pattern_name ILIKE ${qe} OR composition ILIKE ${qe} OR collection ILIKE ${qe} OR product_type ILIKE ${qe})
- ORDER BY RANDOM() LIMIT ${limit | 0}) t;`;
+ AND (pattern_name ILIKE ${qe} OR collection ILIKE ${qe} OR product_type ILIKE ${qe})
+ LIMIT ${limit | 0}) t;`;
return JSON.parse(psql(sql) || '[]');
}
@@ -281,6 +285,75 @@ function mount(app) {
try { removeInspiration(req.admin.email, parseInt((req.body||{}).id, 10)); res.json({ ok: true }); }
catch (e) { res.status(500).json({ error: e.message }); }
});
+
+ // ── Generate 3 wallco-originals from the admin's inspiration pool.
+ // STRICT NO-LEAK: only pattern_name + collection name are sent to the LLM and Replicate.
+ // Vendor names are NEVER included in the prompt that's stored or sent to Replicate.
+ app.post('/api/admin/inspiration/generate', gate, async (req, res) => {
+ try {
+ const items = inspirationFor(req.admin.email);
+ if (items.length === 0) return res.status(400).json({ error: 'pool_empty' });
+
+ // 1) Ask Ollama qwen3:14b to translate the items into 3 SDXL prompts (scrubbed)
+ const scrubbedList = items.slice(0, 8).map(i => `- ${i.pattern_name || 'untitled pattern'}`).join('\n');
+ const sys = `You are a wallpaper design prompt engineer. Given a short list of pattern names (vendor-anonymous), write 3 distinct SDXL prompts for *new* wallpaper designs that share the same aesthetic family but are NOT copies. Output ONLY a JSON array of 3 strings — each prompt is one paragraph ≤ 200 chars, describing motif + palette + mood + texture. Mention "seamless tile, 24-inch repeat, archival quality". Do NOT mention any vendor name or copy specific patterns.`;
+ const userMsg = `Aesthetic inspiration (names only):\n${scrubbedList}\n\nReturn a JSON array of 3 prompts.`;
+
+ const ollamaUrl = process.env.OLLAMA_URL || 'http://192.168.1.133:11434';
+ const ollResp = await fetch(`${ollamaUrl}/api/chat`, {
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ model: process.env.OLLAMA_MODEL || 'qwen3:14b',
+ messages: [{ role: 'system', content: sys }, { role: 'user', content: userMsg }],
+ stream: false, options: { temperature: 0.7 }
+ }),
+ signal: AbortSignal.timeout(60_000)
+ });
+ const ollText = (await ollResp.json()).message?.content || '';
+ let prompts = [];
+ try {
+ const m = ollText.match(/\[\s*"[\s\S]*?"\s*\]/);
+ if (m) prompts = JSON.parse(m[0]);
+ } catch {}
+ if (!Array.isArray(prompts) || prompts.length === 0) {
+ return res.status(502).json({ error: 'llm_returned_no_prompts', raw: ollText.slice(0, 400) });
+ }
+ prompts = prompts.slice(0, 3).filter(p => typeof p === 'string' && p.length > 20);
+
+ // 2) Fire SDXL via the wallco chat tool (re-uses Replicate path + palette extract + insert)
+ const { toolGenerateDesign } = (() => {
+ // chat.js exports mount(); not the tools. Inline the path here via psql + the same generate_designs.js stub.
+ return {};
+ })();
+ // Simpler: shell out to scripts/generate_designs.js with --prompts flag for each
+ const results = [];
+ for (const prompt of prompts) {
+ try {
+ const out = execSync(`node ${require('path').join(__dirname, '..', 'scripts/generate_designs.js')} --n 1 --kind seamless_tile --category mixed --prompts ${JSON.stringify(prompt)}`,
+ { encoding: 'utf8', timeout: 180_000 });
+ const idMatch = out.match(/IDs?:\s*(\d+)/);
+ if (idMatch) {
+ const id = parseInt(idMatch[1], 10);
+ const r = psql(`SELECT row_to_json(t) FROM (SELECT id, prompt, dominant_hex, local_path FROM spoon_all_designs WHERE id=${id}) t;`);
+ const row = JSON.parse(r);
+ // Attach lineage marker (admin pool inspiration). Note: prompts are scrubbed already.
+ psql(`UPDATE spoon_all_designs SET request_text='admin-pool: ' || ${esc(req.admin.email)} WHERE id=${id};`);
+ results.push({
+ id: row.id,
+ dominant_hex: row.dominant_hex,
+ prompt: row.prompt.slice(0, 200),
+ image_url: '/designs/img/' + (row.local_path || '').split('/').pop()
+ });
+ }
+ } catch (e) {
+ results.push({ error: e.message.slice(0, 200) });
+ }
+ }
+ res.json({ ok: true, generated: results.length, designs: results });
+ } catch (e) {
+ res.status(500).json({ error: e.message });
+ }
+ });
}
module.exports = { mount };
← d506fdf Add 5 Playwright e2e tests for /designs review flow — dual-m
·
back to Wallco Ai
·
wallco.ai: 'Pair this with' block on /design/:id — 3 nearest ccd3f38 →