← 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
M db/schema.sqlM lib/catalog.jsM lib/render.jsM lib/rooms.jsM public/css/site.cssA public/js/adminbar.jsM server.js
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 { '&': '&', '<': '<', '>': '>', '"': '"' }[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 →