[object Object]

← back to Interiordesignershowroom

auto-save: 2026-08-03T08:52:19 (7 files) — db/schema.sql lib/catalog.js lib/render.js lib/rooms.js public/css/site.css

99f7508f9872db725f2aa24aa5cb75bb8d723523 · 2026-08-03 08:52:29 -0700 · Steve Abrams

Files touched

Diff

commit 99f7508f9872db725f2aa24aa5cb75bb8d723523
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 3 08:52:29 2026 -0700

    auto-save: 2026-08-03T08:52:19 (7 files) — db/schema.sql lib/catalog.js lib/render.js lib/rooms.js public/css/site.css
---
 db/schema.sql         |  2 ++
 lib/catalog.js        |  4 ++-
 lib/render.js         |  2 ++
 lib/rooms.js          |  4 +--
 public/css/site.css   | 25 +++++++++++++++++
 public/js/adminbar.js | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++
 server.js             |  4 +--
 7 files changed, 113 insertions(+), 5 deletions(-)

diff --git a/db/schema.sql b/db/schema.sql
index a0e4737..13af964 100644
--- a/db/schema.sql
+++ b/db/schema.sql
@@ -21,6 +21,7 @@ CREATE TABLE IF NOT EXISTS products (
   affiliate_url  TEXT NOT NULL,                -- the tracked deep link (the whole point)
   in_stock       BOOLEAN DEFAULT TRUE,
   featured       BOOLEAN DEFAULT FALSE,        -- hand-picked hero pieces
+  suppressed     BOOLEAN NOT NULL DEFAULT FALSE, -- admin "hide this product/brand" flag (kept in DB, hidden from every browse surface). Distinct from affiliate_settings (source-level).
   price_checked_at TIMESTAMPTZ,               -- Amazon TOS: never show stale prices
   created_at     TIMESTAMPTZ DEFAULT now(),
   updated_at     TIMESTAMPTZ DEFAULT now(),
@@ -31,6 +32,7 @@ CREATE INDEX IF NOT EXISTS idx_products_room  ON products (room);
 CREATE INDEX IF NOT EXISTS idx_products_style ON products (style);
 CREATE INDEX IF NOT EXISTS idx_products_color ON products (color);
 CREATE INDEX IF NOT EXISTS idx_products_created ON products (created_at DESC);
+CREATE INDEX IF NOT EXISTS idx_products_suppressed ON products (suppressed) WHERE suppressed = TRUE;
 
 -- Editorial buying guides / shop-the-look articles (the SEO + helpful-content layer).
 CREATE TABLE IF NOT EXISTS guides (
diff --git a/lib/catalog.js b/lib/catalog.js
index 0b51e6c..acb016c 100644
--- a/lib/catalog.js
+++ b/lib/catalog.js
@@ -15,7 +15,9 @@ const PRICE_BUCKETS = [
 // Build a parameterized WHERE from active filters, optionally EXCLUDING one
 // dimension (so a facet's own counts reflect the rest of the query, not itself).
 function buildWhere(f, exclude) {
-  const where = ['in_stock', AFFILIATE_ENABLED];
+  // `NOT suppressed` = the product-level hide flag (a brand/item switched OFF in
+  // admin, e.g. Honiture). Distinct from AFFILIATE_ENABLED, which hides by source.
+  const where = ['in_stock', 'NOT suppressed', AFFILIATE_ENABLED];
   const params = [];
   for (const d of DIMENSIONS) {
     if (f[d] && d !== exclude) { params.push(f[d]); where.push(`${d} = $${params.length}`); }
diff --git a/lib/render.js b/lib/render.js
index 7714994..d6a5138 100644
--- a/lib/render.js
+++ b/lib/render.js
@@ -59,6 +59,7 @@ function productCard(p) {
   return `
   <article class="card" data-room="${esc(p.room)}" data-style="${esc(p.style)}" data-color="${esc(p.color)}"
            data-price="${p.sale_price ?? p.price ?? 0}" data-title="${esc(p.title)}" data-network="${esc(p.network)}"
+           data-advertiser="${esc(p.advertiser || '')}" data-featured="${p.featured ? 1 : 0}" data-instock="${p.in_stock === false ? 0 : 1}"
            data-id="${p.id}">
     <a class="card-img" href="/go/${p.id}" rel="nofollow sponsored" target="_blank" tabindex="0"
        aria-label="${esc(p.title)} — shop at ${esc(p.advertiser || p.network || '')}">
@@ -212,6 +213,7 @@ ${disclosureBar()}
   <p class="fine">© ${new Date().getFullYear()} ${esc(SITE.name)}. Prices and availability are accurate as of the date/time shown and are subject to change. As an Amazon Associate we earn from qualifying purchases.</p>
 </footer>
 <script src="/js/moodboard.js" defer></script>
+<script src="/js/adminbar.js" defer></script>
 </body>
 </html>`;
 }
diff --git a/lib/rooms.js b/lib/rooms.js
index 095a25f..f45a4b5 100644
--- a/lib/rooms.js
+++ b/lib/rooms.js
@@ -15,7 +15,7 @@ const AFFILIATE_ENABLED = `NOT EXISTS (
       AND (a.advertiser = '' OR a.advertiser = COALESCE(products.advertiser, '')))`;
 
 async function searchProducts({ q, room, style, color, colors, advertiser, cat, max, limit = 40, offset = 0 } = {}) {
-  const where = ['in_stock', 'is_wall_paint = FALSE', 'image_url IS NOT NULL', AFFILIATE_ENABLED];
+  const where = ['in_stock', 'NOT suppressed', 'is_wall_paint = FALSE', 'image_url IS NOT NULL', AFFILIATE_ENABLED];
   const params = [];
   if (room) { params.push(room); where.push(`room = $${params.length}`); }
   if (style) { params.push(style); where.push(`style = $${params.length}`); }
@@ -89,7 +89,7 @@ async function listRooms({ limit = 60 } = {}) {
 async function listBrands(limit = 40) {
   const { rows } = await db.query(
     `SELECT advertiser, count(*) n FROM products WHERE advertiser IS NOT NULL AND NOT is_wall_paint AND image_url IS NOT NULL
-     AND ${AFFILIATE_ENABLED}
+     AND NOT suppressed AND ${AFFILIATE_ENABLED}
      GROUP BY advertiser HAVING count(*) >= 3 ORDER BY n DESC LIMIT $1`, [limit]);
   return rows;
 }
diff --git a/public/css/site.css b/public/css/site.css
index 81cdbf6..ccd659f 100644
--- a/public/css/site.css
+++ b/public/css/site.css
@@ -1716,3 +1716,28 @@ body:has(.rb-right:not(.collapsed)) .rb-ham { opacity: 0; pointer-events: none;
   /* Keep guide grid readable */
   .guide-grid { grid-template-columns: 1fr; }
 }
+
+/* ---- Admin "see-more" bar (front-end, shown only to signed-in admins) ---- */
+.admin-bar{position:fixed;left:0;right:0;bottom:0;z-index:9999;display:flex;align-items:center;gap:14px;
+  padding:8px 16px;background:#221e19;color:#f3ede3;font:500 13px/1 -apple-system,Segoe UI,Helvetica,Arial,sans-serif;
+  box-shadow:0 -2px 12px rgba(0,0,0,.25)}
+.admin-bar .ab-tag{color:#e6c98a;font-weight:700;letter-spacing:.03em}
+.admin-bar .ab-toggle{display:flex;align-items:center;gap:6px;cursor:pointer}
+.admin-bar .ab-toggle input{width:15px;height:15px}
+.admin-bar .ab-link{color:#e6c98a;text-decoration:none;font-size:12px}
+.admin-bar .ab-link:hover{text-decoration:underline}
+.admin-bar .ab-exit{margin-left:auto;background:transparent;color:#c9bfb2;border:1px solid #4a443c;border-radius:5px;padding:3px 10px;font-size:12px;cursor:pointer}
+.admin-bar .ab-exit:hover{color:#fff;border-color:#8a7250}
+body.admin-view{padding-bottom:44px}
+/* Per-card admin chip — hidden until admin view is toggled on */
+.admin-chip{display:none}
+body.admin-view .card .admin-chip{display:block;margin:0 12px 12px;padding:8px 10px;border-top:1px dashed #d9d1c4;
+  background:#faf7f1;border-radius:0 0 6px 6px;font:500 12px/1.4 -apple-system,Segoe UI,Helvetica,Arial,sans-serif;color:#6b6259}
+body.admin-view .card{outline:2px solid #e6c98a;outline-offset:-2px}
+.admin-chip .ac-src{color:#8a7250}
+.admin-chip .ac-id{color:#a99f92}
+.admin-chip .ac-flags{margin-top:5px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
+.admin-chip .ac-out{color:#b1483c;font-weight:600}
+.admin-chip .ac-feat{background:#fff;border:1px solid #d9d1c4;border-radius:5px;padding:3px 9px;font-size:12px;cursor:pointer;color:#6b6259}
+.admin-chip .ac-feat.is-on{background:#221e19;color:#e6c98a;border-color:#221e19}
+.admin-chip .ac-feat:hover{border-color:#8a7250}
diff --git a/public/js/adminbar.js b/public/js/adminbar.js
new file mode 100644
index 0000000..ef714dc
--- /dev/null
+++ b/public/js/adminbar.js
@@ -0,0 +1,77 @@
+// Front-end admin "see-more" bar. Shown ONLY when the server has set the
+// `ids_admin_ui` hint cookie (after a successful /admin Basic-auth). This cookie
+// is a UI hint, NOT a security boundary — every mutation below still POSTs to the
+// Basic-auth-gated /admin route, so a forged cookie reveals only the low-value
+// metadata the storefront already carries and can change nothing.
+(function () {
+  function cookie(n) { var m = document.cookie.match('(?:^|; )' + n + '=([^;]*)'); return m ? m[1] : ''; }
+  if (cookie('ids_admin_ui') !== '1') return;
+
+  var KEY = 'ids_adminview';
+  var on = localStorage.getItem(KEY) === '1';
+
+  function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]; }); }
+
+  var bar = document.createElement('div');
+  bar.className = 'admin-bar';
+  bar.innerHTML =
+    '<span class="ab-tag">🔧 Admin</span>'
+    + '<label class="ab-toggle"><input type="checkbox" id="ab-on"' + (on ? ' checked' : '') + '><span>Admin view</span></label>'
+    + '<a class="ab-link" href="/admin">Dashboard ↗</a>'
+    + '<a class="ab-link" href="/admin/affiliates">Affiliates ↗</a>'
+    + '<button type="button" class="ab-exit" id="ab-exit" title="Leave admin view on this device">Exit</button>';
+  document.body.appendChild(bar);
+  apply(on);
+
+  document.getElementById('ab-on').addEventListener('change', function (e) {
+    on = e.target.checked; localStorage.setItem(KEY, on ? '1' : '0'); apply(on);
+  });
+  document.getElementById('ab-exit').addEventListener('click', function () {
+    document.cookie = 'ids_admin_ui=; Max-Age=0; path=/';
+    document.body.classList.remove('admin-view');
+    Array.prototype.forEach.call(document.querySelectorAll('.admin-chip'), function (c) { c.remove(); });
+    bar.remove();
+  });
+
+  function apply(show) {
+    document.body.classList.toggle('admin-view', show);
+    if (show) decorate();
+  }
+
+  // Build a per-card admin chip from the data-* the server already emits — no extra
+  // request. Idempotent: skips a card that already has one.
+  function decorate() {
+    Array.prototype.forEach.call(document.querySelectorAll('.card[data-id]'), function (card) {
+      if (card.querySelector('.admin-chip')) return;
+      var id = card.getAttribute('data-id');
+      var net = card.getAttribute('data-network') || '';
+      var adv = card.getAttribute('data-advertiser') || '';
+      var feat = card.getAttribute('data-featured') === '1';
+      var inStock = card.getAttribute('data-instock') !== '0';
+      var chip = document.createElement('div');
+      chip.className = 'admin-chip';
+      chip.innerHTML =
+        '<div class="ac-src">' + esc(net) + (adv ? ' · ' + esc(adv) : '') + ' <span class="ac-id">#' + esc(id) + '</span></div>'
+        + '<div class="ac-flags">' + (inStock ? '' : '<span class="ac-out">out of stock</span> ')
+        + '<button type="button" class="ac-feat' + (feat ? ' is-on' : '') + '" data-id="' + esc(id) + '">' + (feat ? '★ Featured' : '☆ Feature') + '</button></div>';
+      card.appendChild(chip);
+      var btn = chip.querySelector('.ac-feat');
+      btn.addEventListener('click', function () {
+        btn.disabled = true;
+        // Browser auto-attaches the cached /admin Basic-auth creds; same-origin POST
+        // satisfies the admin CSRF guard.
+        fetch('/admin/products/' + id + '/featured', {
+          method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: ''
+        }).then(function (r) {
+          if (!r.ok) throw new Error(r.status);
+          var nowOn = !btn.classList.contains('is-on');
+          btn.classList.toggle('is-on', nowOn);
+          btn.textContent = nowOn ? '★ Featured' : '☆ Feature';
+          card.setAttribute('data-featured', nowOn ? '1' : '0');
+        }).catch(function () {
+          alert('Feature toggle failed — make sure you are signed in to /admin first.');
+        }).finally(function () { btn.disabled = false; });
+      });
+    });
+  }
+})();
diff --git a/server.js b/server.js
index 8711312..b01e7f3 100644
--- a/server.js
+++ b/server.js
@@ -78,8 +78,8 @@ app.get('/healthz', (_req, res) => res.status(200).send('ok'));
 app.get('/', async (_req, res, next) => {
   try {
     const [{ rows: featured }, { rows: latest }, { rows: guides }] = await Promise.all([
-      db.query(`SELECT ${COLS.PRODUCT} FROM products WHERE featured=TRUE AND in_stock ORDER BY created_at DESC LIMIT 8`),
-      db.query(`SELECT ${COLS.PRODUCT} FROM products WHERE in_stock ORDER BY created_at DESC LIMIT 12`),
+      db.query(`SELECT ${COLS.PRODUCT} FROM products WHERE featured=TRUE AND in_stock AND NOT suppressed ORDER BY created_at DESC LIMIT 8`),
+      db.query(`SELECT ${COLS.PRODUCT} FROM products WHERE in_stock AND NOT suppressed ORDER BY created_at DESC LIMIT 12`),
       db.query(`SELECT slug,title,dek,hero_image FROM guides WHERE published ORDER BY created_at DESC LIMIT 3`),
     ]);
     const roomTiles = ROOMS.slice(0, 6).map(([slug, label]) =>

← 95495fd IDS admin: 'Suggested lines to join' affiliate tab (TK-10171  ·  back to Interiordesignershowroom  ·  admin: brand-level line selector — hide/show a whole brand a dcba584 →