[object Object]

← back to Whatsmystyle

yolo tick 32: copy-URL button on /share/recs/:id page (clipboard with input-select fallback) + admin-only inline 'X embedded of Y catalog · pct%' status pill above recs grid (mini progress bar, polls /api/admin/catalog-status, hides for non-admin or view_as) + brand-deep-link screen routing (boot reads ?brand=, hoists __brandDeepLink flag, falls through to route('recs') instead of route('home') so filter pill is immediately visible)

24e6d95996ae1ec6016cf207709997fa27967ee4 · 2026-05-12 17:10:39 -0700 · SteveStudio2

Files touched

Diff

commit 24e6d95996ae1ec6016cf207709997fa27967ee4
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Tue May 12 17:10:39 2026 -0700

    yolo tick 32: copy-URL button on /share/recs/:id page (clipboard with input-select fallback) + admin-only inline 'X embedded of Y catalog · pct%' status pill above recs grid (mini progress bar, polls /api/admin/catalog-status, hides for non-admin or view_as) + brand-deep-link screen routing (boot reads ?brand=, hoists __brandDeepLink flag, falls through to route('recs') instead of route('home') so filter pill is immediately visible)
---
 data/waitlist.csv        |   2 ++
 data/whatsmystyle.db-shm | Bin 32768 -> 32768 bytes
 data/whatsmystyle.db-wal | Bin 4610312 -> 4610312 bytes
 public/css/app.css       |  12 ++++++++++++
 public/index.html        |   5 +++++
 public/js/app.js         |  27 ++++++++++++++++++++++++++-
 public/share-recs.html   |  20 ++++++++++++++++++++
 7 files changed, 65 insertions(+), 1 deletion(-)

diff --git a/data/waitlist.csv b/data/waitlist.csv
index fe5d598..2331d33 100644
--- a/data/waitlist.csv
+++ b/data/waitlist.csv
@@ -69,3 +69,5 @@ e2e-1778628530526@example.com,e2e,2026-05-12T23:28:50.526Z
 e2e-1778628742953@example.com,e2e,2026-05-12T23:32:22.953Z
 e2e-1778629716355@example.com,e2e,2026-05-12T23:48:36.377Z
 e2e-1778630034615@example.com,e2e,2026-05-12T23:53:54.615Z
+e2e-1778630719170@example.com,e2e,2026-05-13T00:05:19.172Z
+e2e-1778630998656@example.com,e2e,2026-05-13T00:09:58.661Z
diff --git a/data/whatsmystyle.db-shm b/data/whatsmystyle.db-shm
index dfb6320..6ef09d7 100644
Binary files a/data/whatsmystyle.db-shm and b/data/whatsmystyle.db-shm differ
diff --git a/data/whatsmystyle.db-wal b/data/whatsmystyle.db-wal
index 771dad8..f821608 100644
Binary files a/data/whatsmystyle.db-wal and b/data/whatsmystyle.db-wal differ
diff --git a/public/css/app.css b/public/css/app.css
index 6bc96d2..9c959b4 100644
--- a/public/css/app.css
+++ b/public/css/app.css
@@ -167,6 +167,18 @@ h2 { font-family: var(--font-display); font-weight: 600; font-size: 36px; margin
   background: var(--paper-2); border-radius: 16px; border: 1px dashed var(--line);
 }
 .recs-empty p { margin: 0 0 16px; }
+.embed-status-pill {
+  display: inline-flex; align-items: center; gap: 10px;
+  font-size: 12px; color: #707070; padding: 6px 12px;
+  background: var(--paper-2); border-radius: 999px;
+  margin: 6px 0 14px; font-variant-numeric: tabular-nums;
+}
+.embed-status-pill .embed-status-bar {
+  width: 80px; height: 4px; background: rgba(0,0,0,.08); border-radius: 999px; overflow: hidden;
+}
+.embed-status-pill .embed-status-fill {
+  height: 100%; background: var(--ok, #2ea44f); width: 0%; transition: width .4s ease;
+}
 @media (hover: none) and (pointer: coarse) {
   .kb-hint { display: none !important; }
 }
diff --git a/public/index.html b/public/index.html
index 21a43e4..08daa6b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -156,6 +156,11 @@
       <p class="muted" id="recs-empty-msg">No matches with your current filters.</p>
       <button type="button" class="ghost" id="recs-empty-clear">Clear filters</button>
     </div>
+    <!-- Tick 32: inline embedded/catalog status pill — admin only -->
+    <div id="recs-embed-status" class="embed-status-pill" data-role-only="admin" hidden aria-live="polite">
+      <span id="recs-embed-text">…</span>
+      <div class="embed-status-bar"><div class="embed-status-fill" id="recs-embed-fill"></div></div>
+    </div>
 
     <!-- Tailor the view — set-decorator only (tick 16). Sliders re-rank
          the grid to match the show's visual language. Persisted to localStorage. -->
diff --git a/public/js/app.js b/public/js/app.js
index 12686a8..1a3e733 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -1143,6 +1143,8 @@ async function loadRecs() {
   } else if (empty) {
     empty.hidden = true;
   }
+  // Tick 32: inline embed-status pill — admin only, surfaces drainer backlog.
+  refreshRecsEmbedStatus();
   items.forEach(it => {
     const a = document.createElement('a');
     a.className = 'card';
@@ -1163,13 +1165,16 @@ $('#sort').addEventListener('change', e => { localStorage.setItem('wms.sort', e.
 $('#density').addEventListener('input', e => { localStorage.setItem('wms.density', e.target.value); loadRecs(); });
 
 // Tick 31: brand filter on boot — read ?brand= and stash; clear-brand-filter pill resets.
+// Tick 32: when ?brand= is set on boot, also force route to /recs so the
+// filter is immediately visible (deep-link from /brands → main app).
+let __brandDeepLink = false;
 (function hydrateBrandFilterFromURL() {
   try {
     const params = new URLSearchParams(location.search);
     const b = (params.get('brand') || '').trim();
     if (b) {
       localStorage.setItem('wms.brand_filter', b);
-      // Strip the param so reloads don't keep re-applying it after manual clear.
+      __brandDeepLink = true;
       params.delete('brand');
       const qs = params.toString();
       history.replaceState(null, '', location.pathname + (qs ? `?${qs}` : '') + location.hash);
@@ -1177,6 +1182,23 @@ $('#density').addEventListener('input', e => { localStorage.setItem('wms.density
   } catch {}
 })();
 
+// Tick 32: admin-only inline embed-status pill above the recs grid.
+async function refreshRecsEmbedStatus() {
+  const el = $('#recs-embed-status');
+  if (!el) return;
+  if (!Role.is_admin || Role.view_as) { el.hidden = true; return; }
+  try {
+    const r = await fetch('/api/admin/catalog-status');
+    if (!r.ok) { el.hidden = true; return; }
+    const j = await r.json();
+    if (!j.total) { el.hidden = true; return; }
+    const pct = Math.round((j.embedded / j.total) * 100);
+    $('#recs-embed-text').textContent = `${j.embedded.toLocaleString()} embedded of ${j.total.toLocaleString()} catalog · ${pct}%`;
+    $('#recs-embed-fill').style.width = pct + '%';
+    el.hidden = false;
+  } catch { el.hidden = true; }
+}
+
 document.addEventListener('click', e => {
   if (e.target.closest('#clear-brand-filter')) {
     localStorage.removeItem('wms.brand_filter');
@@ -2176,5 +2198,8 @@ document.addEventListener('click', e => {
   await refreshFavorites();   // tick 24 — populate star state before card renders
   const me = await fetch('/api/me').then(r => r.json());
   if (!me.onboarded) { show('onboard'); onbRender(); }
+  // Tick 32: if user came in via /brands → ?brand= deep link, jump straight
+  // to the recs grid (which renders the filter pill) instead of duel.
+  else if (__brandDeepLink) route('recs');
   else route('home');
 })();
diff --git a/public/share-recs.html b/public/share-recs.html
index 93f49e3..1c68c73 100644
--- a/public/share-recs.html
+++ b/public/share-recs.html
@@ -23,6 +23,13 @@
 <body>
   <h1 id="title">Shared list</h1>
   <p class="sub" id="sub">Loading…</p>
+  <div style="margin: -6px 0 16px;">
+    <button type="button" id="copy-url-btn"
+            style="font-family: inherit; font-size: 13px; padding: 7px 14px; border: 1px solid #c8c1b3; border-radius: 999px; background: #fff; color: #1d1d1f; cursor: pointer;">
+      Copy share URL
+    </button>
+    <span id="copy-url-out" style="margin-left: 10px; color: #707070; font-size: 13px;"></span>
+  </div>
   <div id="state-pills" class="state-pills" hidden></div>
   <div id="grid" class="grid"></div>
   <p class="cta">Want a list like this? <a href="/">Build yours on WhatsMyStyle →</a></p>
@@ -30,6 +37,19 @@
   <script>
     const $ = s => document.querySelector(s);
     function escapeHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
+    // Copy URL button — try clipboard, fall back to selecting the visible URL.
+    document.getElementById('copy-url-btn').addEventListener('click', async () => {
+      const url = location.href;
+      const out = document.getElementById('copy-url-out');
+      try { await navigator.clipboard.writeText(url); out.textContent = 'Copied'; setTimeout(() => out.textContent = '', 2400); }
+      catch {
+        // Fallback: write the URL into a span and select it
+        out.innerHTML = `<input type="text" readonly value="${url.replace(/"/g, '&quot;')}" style="width: 320px; font-family: ui-monospace, Menlo, monospace; font-size: 12px; padding: 4px 6px;">`;
+        const inp = out.querySelector('input');
+        inp.focus(); inp.select();
+      }
+    });
+
     (async function () {
       const shareId = location.pathname.split('/').pop();
       const r = await fetch(`/api/share/recs/${encodeURIComponent(shareId)}`);

← 91dc0d4 yolo tick 31: /recs empty-state panel (clear-filters button  ·  back to Whatsmystyle  ·  scripts/e2e-random-photos.js — real-photo E2E harness 8af7af1 →