← 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 →