← back to Whatsmystyle
yolo tick 17: active-production bar (set-decorator persistent context) + tryOnItem inherits it + expanded pro_grade seed (+6 brands, fuzzy normalize) + GET /api/outfits/:id rehydrates rationale
63402bbc2edd229c21fd4110be20727500b21dc2 · 2026-05-12 08:56:02 -0700 · SteveStudio2
Files touched
M public/css/app.cssM public/index.htmlM public/js/app.jsM server.js
Diff
commit 63402bbc2edd229c21fd4110be20727500b21dc2
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Tue May 12 08:56:02 2026 -0700
yolo tick 17: active-production bar (set-decorator persistent context) + tryOnItem inherits it + expanded pro_grade seed (+6 brands, fuzzy normalize) + GET /api/outfits/:id rehydrates rationale
---
public/css/app.css | 30 ++++++++++++++++++++++++++++++
public/index.html | 9 +++++++++
public/js/app.js | 34 ++++++++++++++++++++++++++++------
server.js | 50 +++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 110 insertions(+), 13 deletions(-)
diff --git a/public/css/app.css b/public/css/app.css
index fd9181e..8026868 100644
--- a/public/css/app.css
+++ b/public/css/app.css
@@ -545,6 +545,36 @@ h2 { font-family: var(--font-display); font-weight: 600; font-size: 36px; margin
.tag-prod-status { font-size: 13px; }
.tag-prod a { color: #1d1d1f; }
+/* ---- Active production bar (tick 17) — set-decorator persistent context ---- */
+.active-prod-bar {
+ position: sticky; top: 0; z-index: 49;
+ background: #f5edd9;
+ border-bottom: 1px solid #d6c89c;
+ color: #4a3a14;
+ padding: 8px 20px;
+ display: flex; align-items: center; gap: 10px;
+ font-size: 13px;
+}
+.active-prod-bar label {
+ font-weight: 600;
+ color: #6e4f0d;
+}
+.active-prod-bar select {
+ font: inherit;
+ padding: 4px 12px;
+ border: 1px solid #c8b78e;
+ border-radius: 10px;
+ background: #fff;
+ color: #1d1d1f;
+}
+.active-prod-bar .active-prod-edit {
+ margin-left: auto;
+ color: #6e4f0d;
+ text-decoration: none;
+ font-size: 12px;
+}
+.active-prod-bar .active-prod-edit:hover { color: #1d1d1f; }
+
/* ---- Set-grade toggle (tick 16) — set-decorator catalog filter ---- */
.set-grade-toggle {
display: inline-flex;
diff --git a/public/index.html b/public/index.html
index e882dac..38bc162 100644
--- a/public/index.html
+++ b/public/index.html
@@ -10,6 +10,15 @@
<body>
<a href="#main" class="skip-link">Skip to main content</a>
<div id="role-banner" class="role-banner" hidden></div>
+<!-- Active production selector — set-decorator only. Every closet upload + try-on
+ inherits this id. Live across page sessions (localStorage). -->
+<div id="active-prod-bar" class="active-prod-bar" data-role-only="set_decorator" hidden>
+ <label for="active-prod-select">Active production</label>
+ <select id="active-prod-select" name="production_id">
+ <option value="">— untagged —</option>
+ </select>
+ <a href="#" data-screen="productions" class="active-prod-edit">manage →</a>
+</div>
<header class="topbar" role="banner">
<button class="logo" aria-label="Home — WhatsMyStyle" onclick="route('home')">WhatsMyStyle<span class="bullet" aria-hidden="true">·</span><em>placeholder</em></button>
<button class="hamburger" aria-label="Open menu" aria-expanded="false" aria-controls="menu" onclick="toggleMenu()">
diff --git a/public/js/app.js b/public/js/app.js
index fc010ec..2abc4c3 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -127,9 +127,10 @@ const Role = {
};
window.Role = Role;
-// Populate every <select> that's a production picker (closet upload, future
-// try-on submit) with the current user's productions. Idempotent — wipes &
-// re-fills.
+// Populate every <select> that's a production picker (closet upload, try-on
+// submit, header active-production bar). Idempotent — wipes & re-fills.
+// Reads / writes localStorage 'wms.activeProduction' so the choice persists
+// across pages and is auto-applied to all writes.
async function populateProductionSelects() {
const selects = $$('select[name="production_id"]');
if (!selects.length) return;
@@ -143,13 +144,30 @@ async function populateProductionSelects() {
).join('');
}
} catch {}
+ const active = localStorage.getItem('wms.activeProduction') || '';
selects.forEach(s => {
- const cur = s.value;
s.innerHTML = opts;
- if (cur) s.value = cur;
+ s.value = active;
});
}
+// Active-production helper — single source of truth for tryOnItem + any form
+// that wants the current set-decorator project context.
+function getActiveProductionId() {
+ return localStorage.getItem('wms.activeProduction') || '';
+}
+
+// The header bar select drives the global. Other selects mirror via the
+// `wms.activeProduction` localStorage value on populate().
+document.addEventListener('change', e => {
+ if (e.target.id !== 'active-prod-select') return;
+ const v = e.target.value || '';
+ if (v) localStorage.setItem('wms.activeProduction', v);
+ else localStorage.removeItem('wms.activeProduction');
+ // Mirror to any other production-picker selects on the page.
+ $$('select[name="production_id"]').forEach(s => { if (s !== e.target) s.value = v; });
+});
+
// ---- Set-grade only toggle (tick 16) — set-decorator role ----
$('#set-grade-only')?.addEventListener('change', e => {
localStorage.setItem('wms.proOnly', e.target.checked ? '1' : '0');
@@ -976,13 +994,17 @@ async function pollAvatar() {
}
// ---------- Try-on (from duel / recs / closet) ----------------------------
-async function tryOnItem({ item_id, closet_id, mode = 'avatar', occasion, source_photo_file }) {
+async function tryOnItem({ item_id, closet_id, mode = 'avatar', occasion, source_photo_file, production_id }) {
const fd = new FormData();
if (item_id) fd.append('item_id', item_id);
if (closet_id) fd.append('closet_id', closet_id);
fd.append('mode', mode);
if (occasion) fd.append('occasion', occasion);
if (source_photo_file) fd.append('source_photo', source_photo_file);
+ // Auto-inherit the active production for set-decorator context unless the
+ // caller passed one explicitly.
+ const pid = production_id || getActiveProductionId();
+ if (pid) fd.append('production_id', pid);
const r = await fetch('/api/tryon', { method: 'POST', body: fd }).then(r => r.json());
if (r.error) { alert(r.error + (r.hint ? ' — ' + r.hint : '')); return null; }
return r.job_id;
diff --git a/server.js b/server.js
index a53fd26..35c52fa 100644
--- a/server.js
+++ b/server.js
@@ -274,26 +274,40 @@ try {
// Idempotent seed pass — tag items as pro_grade where brand is a known
// multi-stock retailer (mass + DTC contemporary). Resale-only brands stay
// untagged (single-piece inventory). Run once-per-boot; quick.
+ // PRO_BRANDS — multi-stocked, restockable, set-decorator-friendly. Tick 17:
+ // broadened to cover seed-catalog brands the original list missed
+ // (Reformation, Saint Laurent line-extension basics, Burberry trench, etc.)
+ // and added a normalize() pass so 'L.L. Bean' matches 'L.L.Bean'.
const PRO_BRANDS = new Set([
'Everlane', 'COS', 'Uniqlo', 'Madewell', 'J.Crew', 'Banana Republic',
- 'Theory', 'AGOLDE', 'Mother', 'Frame', 'Levi\'s', "Levi's",
+ 'Theory', 'AGOLDE', 'Mother', 'Frame', "Levi's",
'Vince', 'rag & bone', 'Marine Layer', 'L.L.Bean', 'Carhartt',
'Citizens of Humanity', 'Saint James', 'Tradlands', 'Mott & Bow',
'Patagonia', 'Allbirds', 'Veja', 'Naadam', 'Boody', 'Pact',
'Aritzia', 'Anthropologie', 'Free People', 'Express', 'Gap', 'Old Navy',
'Nike', 'Adidas',
+ // tick 17 additions — restockable basics that landed in seed catalog
+ 'Reformation', 'Outerknown', 'Cuyana', 'Kotn', 'Mate the Label',
+ 'Christy Dawn', 'Boyish', 'Boyish Jeans', 'For Days', 'Knickey',
+ 'Sezane', 'Mara Hoffman', 'Frank And Oak', 'DL1961', 'Toteme',
+ 'Burberry', // classic trench coats are multi-stocked
+ 'G.H. Bass', // classic loafers — Weejuns are stock
+ 'L.L. Bean', // alias for L.L.Bean
+ 'Common Projects', // staple white sneaker
]);
- const RESALE_SOURCES = new Set(['realreal', 'poshmark', 'vestiaire', 'grailed']);
+ const RESALE_SOURCES = new Set(['realreal', 'poshmark', 'vestiaire', 'grailed', 'depop']);
+ // Normalize for fuzzy matching: lowercase, collapse whitespace, drop dots.
+ const norm = s => (s || '').toLowerCase().replace(/[.\s]+/g, '').trim();
+ const PRO_BRANDS_NORM = new Set(Array.from(PRO_BRANDS).map(norm));
const all = db.prepare("SELECT id, source, brand, pro_grade FROM items").all();
const upd = db.prepare('UPDATE items SET pro_grade = ? WHERE id = ?');
let tagged = 0;
for (const r of all) {
- if (r.pro_grade !== null) continue; // respect any prior manual override
+ if (r.pro_grade !== null && r.pro_grade !== 0) continue; // keep manual 1, re-run 0/null
const isResale = RESALE_SOURCES.has((r.source || '').toLowerCase());
- const isProBrand = PRO_BRANDS.has((r.brand || '').trim());
- const grade = isResale ? 0 : (isProBrand ? 1 : 0);
- upd.run(grade, r.id);
- tagged++;
+ const isProBrand = PRO_BRANDS_NORM.has(norm(r.brand));
+ const grade = isResale ? 0 : (isProBrand ? 1 : (r.pro_grade ?? 0));
+ if (grade !== r.pro_grade) { upd.run(grade, r.id); tagged++; }
}
if (tagged > 0) console.log(`[pro_grade seed] tagged ${tagged} items`);
} catch (e) { console.error('[pro_grade seed] error', e.message); }
@@ -1212,6 +1226,28 @@ app.post('/api/outfits', (req, res) => {
res.json({ ok: true, pick_id: lastInsertRowid, outfits });
});
+// Tick 17: GET a stored pick + rehydrate the rationale text per outfit.
+// We don't persist `rationale` on outfit_picks (deterministic template, cheap
+// to recompute), so re-run rationaleFor() before returning.
+app.get('/api/outfits/:id', (req, res) => {
+ const u = currentUser(req);
+ const row = db.prepare('SELECT * FROM outfit_picks WHERE id=? AND user_id=?').get(req.params.id, u.id);
+ if (!row) return res.status(404).json({ error: 'pick not found' });
+ let outfits;
+ try { outfits = JSON.parse(row.outfits || '[]'); }
+ catch { outfits = []; }
+ for (const o of outfits) o.rationale = rationaleFor(o);
+ res.json({
+ pick_id: row.id,
+ anchor_kind: row.anchor_kind,
+ anchor_id: row.anchor_id,
+ picked_index: row.picked_index,
+ created_at: row.created_at,
+ picked_at: row.picked_at,
+ outfits,
+ });
+});
+
app.post('/api/outfits/:id/pick', (req, res) => {
const u = currentUser(req);
const idx = Number(req.body?.index);
← 202347c feat(catalog-apis): adapter framework for Shopify/Etsy/eBay
·
back to Whatsmystyle
·
yolo tick 18: embed-items --batch + boot-time drainer (5min/ b0fcb62 →