← back to Whatsmystyle
yolo tick 30: recs grid server-side sort (best/sustain/newest/price_asc/price_desc — ORDER BY runs across full match pool before limit slice) + Sustainability ↓ + Newest added to recs sort dropdown + brands page search box (live-filter by brand name, ephemeral state, sr-only label)
651582437a5f25b849ae0dd82cbb70e540df9491 · 2026-05-12 16:32:36 -0700 · SteveStudio2
Files touched
M data/waitlist.csvM data/whatsmystyle.db-shmM data/whatsmystyle.db-walA data/wms.sqliteM public/brands.htmlM public/index.htmlM public/js/app.jsM server.js
Diff
commit 651582437a5f25b849ae0dd82cbb70e540df9491
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Tue May 12 16:32:36 2026 -0700
yolo tick 30: recs grid server-side sort (best/sustain/newest/price_asc/price_desc — ORDER BY runs across full match pool before limit slice) + Sustainability ↓ + Newest added to recs sort dropdown + brands page search box (live-filter by brand name, ephemeral state, sr-only label)
---
data/waitlist.csv | 2 ++
data/whatsmystyle.db-shm | Bin 32768 -> 32768 bytes
data/whatsmystyle.db-wal | Bin 4610312 -> 4610312 bytes
data/wms.sqlite | 0
public/brands.html | 15 +++++++++++++--
public/index.html | 2 ++
public/js/app.js | 13 +++++++++----
server.js | 16 ++++++++++++++--
8 files changed, 40 insertions(+), 8 deletions(-)
diff --git a/data/waitlist.csv b/data/waitlist.csv
index 149d0be..900e57b 100644
--- a/data/waitlist.csv
+++ b/data/waitlist.csv
@@ -65,3 +65,5 @@ e2e-1778625279476@example.com,e2e,2026-05-12T22:34:39.477Z
e2e-1778625542846@example.com,e2e,2026-05-12T22:39:02.846Z
e2e-1778627519615@example.com,e2e,2026-05-12T23:11:59.620Z
e2e-1778627684022@example.com,e2e,2026-05-12T23:14:44.022Z
+e2e-1778628530526@example.com,e2e,2026-05-12T23:28:50.526Z
+e2e-1778628742953@example.com,e2e,2026-05-12T23:32:22.953Z
diff --git a/data/whatsmystyle.db-shm b/data/whatsmystyle.db-shm
index b8ac341..b387201 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 3a67447..d4a91ed 100644
Binary files a/data/whatsmystyle.db-wal and b/data/whatsmystyle.db-wal differ
diff --git a/data/wms.sqlite b/data/wms.sqlite
new file mode 100644
index 0000000..e69de29
diff --git a/public/brands.html b/public/brands.html
index 58fbb9d..b49afcd 100644
--- a/public/brands.html
+++ b/public/brands.html
@@ -56,6 +56,7 @@
.brand-card .tier-5 { background: #d4f4d4; color: #1d5e1d; }
.brand-card .tier-1 { background: #fbe4e4; color: #7a1717; }
.empty { color: #999; font-size: 14px; padding: 24px 0; text-align: center; }
+ .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
</style>
</head>
<body>
@@ -64,6 +65,11 @@
<p class="sub" id="sub">Every brand in our catalog.</p>
<div class="controls" id="controls" hidden>
+ <label style="flex: 1; min-width: 180px;">
+ <span class="sr-only">Filter brands</span>
+ <input id="search" type="search" placeholder="Filter brands…" autocomplete="off" spellcheck="false"
+ style="font-family: inherit; font-size: 13px; padding: 6px 12px; border: 1px solid #c8c1b3; border-radius: 8px; background: #faf7f2; color: #1d1d1f; width: 100%;" />
+ </label>
<label>Sort
<select id="sort">
<option value="pieces">Most pieces</option>
@@ -88,6 +94,7 @@
const SORT_KEY = 'wms.brands.sort';
const DENSITY_KEY= 'wms.brands.density';
let ALL = [];
+ let SEARCH = '';
function currentFilter() { return localStorage.getItem(FILTER_KEY) || 'all'; }
function currentSort() { return localStorage.getItem(SORT_KEY) || 'pieces'; }
@@ -142,9 +149,12 @@
`).join('');
$('#sort').value = s;
- const filtered = sortList(ALL.filter(b => matches(b, f)), s);
+ const q = SEARCH.trim().toLowerCase();
+ const searched = q ? ALL.filter(b => b.brand.toLowerCase().includes(q)) : ALL;
+ const filtered = sortList(searched.filter(b => matches(b, f)), s);
const sortLabel = { pieces: 'most pieces', az: 'A→Z', sustain: 'sustainability' }[s];
- $('#sub').textContent = `${filtered.length} of ${ALL.length} brand${ALL.length === 1 ? '' : 's'} · sorted by ${sortLabel}.`;
+ const note = q ? ` · matching “${q}”` : '';
+ $('#sub').textContent = `${filtered.length} of ${ALL.length} brand${ALL.length === 1 ? '' : 's'} · sorted by ${sortLabel}${note}.`;
if (!filtered.length) {
$('#grid').innerHTML = '<div class="empty">No brands match this filter.</div>';
@@ -180,6 +190,7 @@
$('#sort').value = currentSort();
$('#sort').addEventListener('change', () => setSort($('#sort').value));
$('#density').addEventListener('input', () => setDensity(Number($('#density').value)));
+ $('#search').addEventListener('input', () => { SEARCH = $('#search').value; render(); });
render();
})();
</script>
diff --git a/public/index.html b/public/index.html
index 8a1748b..e1e3071 100644
--- a/public/index.html
+++ b/public/index.html
@@ -126,6 +126,8 @@
<label>Sort
<select id="sort">
<option value="taste">Best Match</option>
+ <option value="sustain">Sustainability ↓</option>
+ <option value="newest">Newest</option>
<option value="price_asc">Price ↑</option>
<option value="price_desc">Price ↓</option>
<option value="brand">Brand A–Z</option>
diff --git a/public/js/app.js b/public/js/app.js
index b3cdf3d..58385c2 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -1080,7 +1080,12 @@ async function loadRecs() {
const activeMaterials = JSON.parse(localStorage.getItem('wms.facets.materials') || '[]');
const colorQS = activeColors.length ? `&colors=${encodeURIComponent(activeColors.join(','))}` : '';
const matQS = activeMaterials.length ? `&materials=${encodeURIComponent(activeMaterials.join(','))}` : '';
- const r = await fetch(`/api/recommend?limit=60${proQS}${colorQS}${matQS}`).then(r => r.json());
+ // Tick 30: server-side sort for price/sustain/newest. Client-side sort kept for
+ // brand/title (cheap string sort on the 60-item slice) and 'taste' (default).
+ const SERVER_SORTS = new Set(['price_asc', 'price_desc', 'sustain', 'newest']);
+ const serverSort = SERVER_SORTS.has(sort) ? sort : 'best';
+ const sortQS = `&sort=${encodeURIComponent(serverSort)}`;
+ const r = await fetch(`/api/recommend?limit=60${proQS}${colorQS}${matQS}${sortQS}`).then(r => r.json());
// Render the facet pill row.
const facetsRow = $('#facets-row');
if (facetsRow && r.facets) {
@@ -1096,9 +1101,9 @@ async function loadRecs() {
facetsRow.hidden = !html;
}
let items = r.items || [];
- if (sort === 'price_asc') items.sort((a, b) => (a.price_cents || 0) - (b.price_cents || 0));
- else if (sort === 'price_desc') items.sort((a, b) => (b.price_cents || 0) - (a.price_cents || 0));
- else if (sort === 'brand') items.sort((a, b) => (a.brand || '').localeCompare(b.brand || ''));
+ // Tick 30: price_asc/price_desc/sustain/newest are now server-sorted; respect that order.
+ // Keep brand/title as client-side string sort (server doesn't expose those).
+ if (sort === 'brand') items.sort((a, b) => (a.brand || '').localeCompare(b.brand || ''));
else if (sort === 'title') items.sort((a, b) => (a.title || '').localeCompare(b.title || ''));
// For set decorators on Best Match, apply Tailor re-rank on top of taste order.
if (isSetDec && sort === 'taste') {
diff --git a/server.js b/server.js
index 58f884a..0e74874 100644
--- a/server.js
+++ b/server.js
@@ -727,14 +727,26 @@ app.get('/api/recommend', (req, res) => {
db.prepare(`SELECT item_id, COUNT(DISTINCT production_id) c
FROM tryon_jobs WHERE user_id=? AND production_id IS NOT NULL AND item_id IS NOT NULL
GROUP BY item_id`).all(u.id).forEach(r => tryonCounts.set(r.item_id, r.c));
- const scored = rows.map(r => {
+ const allScored = rows.map(r => {
const e = JSON.parse(r.embedding);
let dot = 0;
for (let i = 0; i < 32; i++) dot += taste[i] * e[i];
const tier = tierFor(r.brand);
const sustainAdj = tier ? ((tier - 3) / 5) * sustainBoost : 0;
return { ...r, score: dot + sustainAdj, sustain: tier, production_credits: tryonCounts.get(r.id) || 0 };
- }).sort((a, b) => b.score - a.score).slice(0, limit);
+ });
+ // Tick 30: server-side sort. Default "best" = cosine score (existing behavior).
+ // Other modes operate on the full scored pool BEFORE the limit slice so the
+ // user sees true Newest / Price ↑↓ / Sustain rankings across the whole match
+ // set, not just the top-N-by-cosine.
+ const sort = String(req.query.sort || 'best').toLowerCase();
+ let ordered = allScored;
+ if (sort === 'sustain') ordered = allScored.slice().sort((a, b) => (b.sustain || 0) - (a.sustain || 0) || b.score - a.score);
+ else if (sort === 'price_asc') ordered = allScored.slice().sort((a, b) => (a.price_cents ?? Number.POSITIVE_INFINITY) - (b.price_cents ?? Number.POSITIVE_INFINITY));
+ else if (sort === 'price_desc') ordered = allScored.slice().sort((a, b) => (b.price_cents ?? -1) - (a.price_cents ?? -1));
+ else if (sort === 'newest') ordered = allScored.slice().sort((a, b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
+ else ordered = allScored.slice().sort((a, b) => b.score - a.score); // 'best' default
+ const scored = ordered.slice(0, limit);
// Tick 23: facet counts over the SCORED slice (what user sees), top 6 colors + top 4 materials.
function topFacets(field, n) {
const counts = new Map();
← 3c7e580 yolo tick 29: brands page sort selector (Most pieces / A→Z /
·
back to Whatsmystyle
·
crawl-catalog: log session-minutes after Browserbase session 69af117 →