[object Object]

← back to Interiordesignershowroom

admin: keyword line-hider with preview — catch a line across reseller-mislabeled brands (Honiture gap)

770915b7c5d56cbbf9a9afcbe7ce3a26e3e78e3b · 2026-08-03 09:13:31 -0700 · Steve Abrams

Files touched

Diff

commit 770915b7c5d56cbbf9a9afcbe7ce3a26e3e78e3b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 3 09:13:31 2026 -0700

    admin: keyword line-hider with preview — catch a line across reseller-mislabeled brands (Honiture gap)
---
 routes/admin.js | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 98 insertions(+)

diff --git a/routes/admin.js b/routes/admin.js
index 41919b6..d498601 100644
--- a/routes/admin.js
+++ b/routes/admin.js
@@ -295,9 +295,28 @@ router.get('/admin/brands', async (req, res, next) => {
       <button style="padding:6px 12px">Search</button>
       ${q ? ` <a href="/admin/brands" class="subtle" style="margin-left:6px">clear</a>` : ''}</form>`;
 
+    // Green confirmation banner after a keyword hide/unhide (carried via ?done=).
+    const banner = req.query.done
+      ? `<p style="background:#e7f3e7;border:1px solid #bcd9bc;border-radius:6px;padding:8px 12px;margin:0 0 14px">✓ ${esc(String(req.query.done).slice(0, 200))}</p>`
+      : '';
+
+    // Keyword line-hider: matches title OR brand OR advertiser, so it catches a line
+    // even when a reseller stamps its own name in the brand column (the Honiture gap).
+    // Always routes through a preview first — no free-text write without a count.
+    const keywordBox = `<div style="border:1px solid var(--line);background:#fff;border-radius:8px;padding:14px 16px;margin:0 0 18px">
+      <div style="font-weight:600;margin-bottom:4px">Hide an entire line by keyword</div>
+      <p class="subtle" style="margin:0 0 10px">Matches <b>title</b>, <b>brand</b> and <b>advertiser</b> — so it catches a line even when a reseller mislabels the brand column (the Honiture case). You'll see an exact preview and count before anything changes.</p>
+      <form method="get" action="/admin/brands/keyword" style="margin:0">
+        <input name="kw" placeholder="e.g. honiture" required minlength="2" style="padding:6px 10px;border:1px solid var(--line);border-radius:5px;font-size:.85rem;min-width:240px">
+        <button style="padding:6px 12px">Preview matches →</button>
+      </form>
+    </div>`;
+
     res.send(shell(`
       <h1 style="font-size:1.15rem;margin:0 0 4px">Brands — hide or show a whole line</h1>
       ${intro}
+      ${banner}
+      ${keywordBox}
       ${searchForm}
       <table>
         <tr><th>Brand</th><th>Advertisers</th><th>In-stock / total</th><th>Hidden</th><th>Newest item</th><th>Status</th><th>Action</th></tr>
@@ -322,6 +341,85 @@ router.post('/admin/brands/suppress', async (req, res, next) => {
   } catch (e) { next(e); }
 });
 
+// Keyword line-hider — PREVIEW. Shows exactly what a keyword would hide (matching
+// title OR brand OR advertiser) BEFORE any write: total, already-hidden, a
+// brand×advertiser breakdown, and a thumbnail sample. No mutation happens here.
+router.get('/admin/brands/keyword', async (req, res, next) => {
+  try {
+    const kw = String(req.query.kw || '').trim().slice(0, 60);
+    if (kw.length < 2) return res.redirect('/admin/brands');
+    const like = '%' + kw + '%';
+    const MATCH = `(title ILIKE $1 OR brand ILIKE $1 OR advertiser ILIKE $1)`;
+    const [tot, brk, sample] = await Promise.all([
+      db.query(`SELECT count(*) AS total, count(*) FILTER (WHERE suppressed) AS hidden FROM products WHERE ${MATCH}`, [like]),
+      db.query(`SELECT COALESCE(NULLIF(brand,''),'(no brand)') AS brand, COALESCE(NULLIF(advertiser,''),'(none)') AS advertiser, network,
+          count(*) AS n, count(*) FILTER (WHERE suppressed) AS hid
+        FROM products WHERE ${MATCH} GROUP BY 1,2,3 ORDER BY n DESC LIMIT 100`, [like]),
+      db.query(`SELECT id,title,brand,advertiser,image_url,suppressed FROM products WHERE ${MATCH} ORDER BY suppressed, title LIMIT 24`, [like]),
+    ]);
+    const total = Number(tot.rows[0].total), hidden = Number(tot.rows[0].hidden), visible = total - hidden;
+
+    if (!total) {
+      return res.send(shell(`<h1 style="font-size:1.15rem">No matches for “${esc(kw)}”</h1>
+        <p class="subtle">Nothing in the catalog matches that keyword in title, brand, or advertiser.</p>
+        <p><a href="/admin/brands">← Back to Brands</a></p>`));
+    }
+
+    const brkRows = brk.rows.map((r) => `<tr class="${Number(r.hid) === Number(r.n) ? 'row-off' : ''}">
+      <td>${esc(r.brand)}</td><td>${esc(r.advertiser)}<div class="net">${esc(r.network)}</div></td>
+      <td>${r.n}</td><td>${Number(r.hid) ? `<b style="color:#b1483c">${r.hid}</b>` : '0'}</td></tr>`).join('');
+
+    const sampleCards = sample.rows.map((p) => `<div style="border:1px solid var(--line);border-radius:6px;padding:6px;background:#fff;${p.suppressed ? 'opacity:.55' : ''}">
+      ${p.image_url ? `<img src="${esc(p.image_url)}" alt="" style="width:100%;height:80px;object-fit:cover;border-radius:4px">` : ''}
+      <div style="font-size:.72rem;margin-top:4px;line-height:1.25">${esc(String(p.title).slice(0, 60))}</div>
+      <div class="net" style="font-size:.62rem">${esc(p.advertiser || '')}${p.suppressed ? ' · hidden' : ''}</div></div>`).join('');
+
+    const big = total > 200
+      ? `<p style="background:#faf1e0;border:1px solid #e5cf9e;border-radius:6px;padding:8px 12px;color:#9a6a1c;margin:0 0 12px">⚠︎ This matches <b>${total}</b> products — a broad keyword. Check the breakdown below before applying.</p>`
+      : '';
+
+    const confirm = (action, label, disabled) => disabled
+      ? `<button type="button" disabled style="opacity:.4;cursor:not-allowed;padding:8px 16px">${label}</button>`
+      : `<form method="post" action="/admin/brands/keyword/apply" style="display:inline;margin:0 6px 0 0">
+          <input type="hidden" name="kw" value="${esc(kw)}"><input type="hidden" name="action" value="${action}">
+          <button style="padding:8px 16px;${action === 'hide' ? 'background:#221e19;color:#e6c98a;border-color:#221e19' : ''}">${label}</button></form>`;
+
+    res.send(shell(`
+      <p style="margin:0 0 4px"><a href="/admin/brands" class="subtle">← Brands</a></p>
+      <h1 style="font-size:1.15rem;margin:0 0 6px">Preview — line “${esc(kw)}”</h1>
+      ${big}
+      <p style="margin:0 0 14px"><b style="font-size:1.4rem">${total}</b> product${total === 1 ? '' : 's'} match
+        <span class="subtle">· ${visible} visible · ${hidden} already hidden</span></p>
+      <div style="margin:0 0 20px">
+        ${confirm('hide', `Hide all ${total} →`, visible === 0)}
+        ${confirm('unhide', `Un-hide all ${total}`, hidden === 0)}
+        <a href="/admin/brands" style="margin-left:10px;color:#8a8178">Cancel</a>
+      </div>
+      <h2>Breakdown — where these come from</h2>
+      <table><tr><th>Brand</th><th>Advertiser</th><th>Matches</th><th>Hidden</th></tr>${brkRows}</table>
+      <h2>Sample (${sample.rows.length} of ${total})</h2>
+      <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:8px">${sampleCards}</div>
+    `));
+  } catch (e) { next(e); }
+});
+
+// Keyword line-hider — APPLY. Flips `suppressed` for every product matching the
+// keyword in title/brand/advertiser. Reversible; kept in DB. Reached only from the
+// preview's confirm button (same-origin POST, CSRF-guarded above).
+router.post('/admin/brands/keyword/apply', async (req, res, next) => {
+  try {
+    const kw = String(req.body.kw || '').trim().slice(0, 60);
+    const hide = req.body.action !== 'unhide';
+    if (kw.length < 2) return res.status(400).send('Keyword too short.');
+    const like = '%' + kw + '%';
+    const r = await db.query(
+      `UPDATE products SET suppressed=$2, updated_at=now()
+       WHERE (title ILIKE $1 OR brand ILIKE $1 OR advertiser ILIKE $1)`, [like, hide]);
+    const msg = `${hide ? 'Hid' : 'Un-hid'} ${r.rowCount} product${r.rowCount === 1 ? '' : 's'} matching “${kw}”.`;
+    res.redirect('/admin/brands?done=' + encodeURIComponent(msg));
+  } catch (e) { next(e); }
+});
+
 // --- Affiliates: see every link source & switch it on/off -----------------
 // An affiliate is either a whole NETWORK (cj/amazon/...) or a specific ADVERTISER
 // (merchant) on that network. OFF here => that source's products vanish from the

← dcba584 admin: brand-level line selector — hide/show a whole brand a  ·  back to Interiordesignershowroom  ·  IDS admin: record CJ account identifiers on Affiliates tab ( 8fb23cd →