← back to Marketing Command Center
Owned·DW Fleet: apply contrarian fixes (sort actionability + NaN/URL guards)
88aa02b2e37ea0aa605a5c85caf9491622d8fbb8 · 2026-08-25 10:52:51 -0700 · Steve
Cody the Contrarian red-team caught 3 real issues:
- Attention sort was backwards: a DEAD (dormant) account that was also repetitive
outranked a LIVE account spamming the same product right now. Re-scored by
ACTIONABILITY — active-repetitive(4) > went-dark-with-history(3) > slowing(2) >
never-posted/no-creds(1) > healthy(0). Now the fixable-right-now accounts top
the list, not the structurally-dead ones.
- repetitive now requires !dormant (a dead account repeating old posts is not
actionable) — also removes the noisy '↻ same' badge from dormant rows.
- NaN guard: an unparseable ledger ts no longer yields daysOld=NaN (which read as
'healthy' and broke the sort comparator) — treated as no-date → dormant.
- permalink href now rejects non-http(s) URIs (javascript:/data:) defensively.
- recent sort reuses the guarded daysOld instead of raw Date math.
Verified: computed vs rendered attention order match (scores 4,2,1,0 monotonic);
active-repetitive tops the list; 0 dormant rows show repetitive; 0 unsafe hrefs;
no page errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M public/panels/vendors.js
Diff
commit 88aa02b2e37ea0aa605a5c85caf9491622d8fbb8
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Aug 25 10:52:51 2026 -0700
Owned·DW Fleet: apply contrarian fixes (sort actionability + NaN/URL guards)
Cody the Contrarian red-team caught 3 real issues:
- Attention sort was backwards: a DEAD (dormant) account that was also repetitive
outranked a LIVE account spamming the same product right now. Re-scored by
ACTIONABILITY — active-repetitive(4) > went-dark-with-history(3) > slowing(2) >
never-posted/no-creds(1) > healthy(0). Now the fixable-right-now accounts top
the list, not the structurally-dead ones.
- repetitive now requires !dormant (a dead account repeating old posts is not
actionable) — also removes the noisy '↻ same' badge from dormant rows.
- NaN guard: an unparseable ledger ts no longer yields daysOld=NaN (which read as
'healthy' and broke the sort comparator) — treated as no-date → dormant.
- permalink href now rejects non-http(s) URIs (javascript:/data:) defensively.
- recent sort reuses the guarded daysOld instead of raw Date math.
Verified: computed vs rendered attention order match (scores 4,2,1,0 monotonic);
active-repetitive tops the list; 0 dormant rows show repetitive; 0 unsafe hrefs;
no page errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
public/panels/vendors.js | 29 ++++++++++++++++++++++-------
1 file changed, 22 insertions(+), 7 deletions(-)
diff --git a/public/panels/vendors.js b/public/panels/vendors.js
index 376d7fa..de6a067 100644
--- a/public/panels/vendors.js
+++ b/public/panels/vendors.js
@@ -49,7 +49,8 @@ window.MCC_PANELS['vendors'] = {
const thumb = p.image_url
? `<img class="ig-li-thumb" loading="lazy" src="${esc(p.image_url)}" alt="" onerror="this.style.visibility='hidden'">`
: `<span class="ig-li-thumb"></span>`;
- return `<a class="ig-li" href="${esc(p.permalink || '#')}" target="_blank" rel="noopener noreferrer">
+ const link = /^https?:\/\//i.test(p.permalink || '') ? p.permalink : '#'; // reject javascript:/data: URIs from ledger data
+ return `<a class="ig-li" href="${esc(link)}" target="_blank" rel="noopener noreferrer">
${thumb}
<span class="ig-li-body">
<span class="ig-li-cap">${cap} <span class="ig-li-ext">↗</span></span>
@@ -67,14 +68,20 @@ window.MCC_PANELS['vendors'] = {
const posts = (byHandle[a.handle] || []);
const last6 = posts.slice(0, 6);
const newestTs = posts.length ? posts[0].ts : null;
- const daysOld = newestTs ? (Date.now() - new Date(newestTs)) / 864e5 : Infinity;
+ const newestMs = newestTs ? new Date(newestTs).getTime() : NaN;
+ // Guard against an unparseable ts (external ledger data): treat as no-date →
+ // Infinity, so the account reads as dormant rather than silently "healthy"
+ // and the sort comparator never gets a NaN (which breaks ordering).
+ const daysOld = isFinite(newestMs) ? (Date.now() - newestMs) / 864e5 : Infinity;
const freq = {};
let repCount = 0;
for (const p of last6) { const t = (p.product_title || '').trim().toLowerCase(); if (!t) continue; freq[t] = (freq[t] || 0) + 1; if (freq[t] > repCount) repCount = freq[t]; }
const weekAgo = Date.now() - 7 * 864e5;
- const week7 = posts.filter(p => p.ts && new Date(p.ts) >= weekAgo).length; // cadence: posts in the last 7 days
+ const week7 = posts.filter(p => { const ms = p.ts ? new Date(p.ts).getTime() : NaN; return isFinite(ms) && ms >= weekAgo; }).length; // cadence: posts in the last 7 days
const dormant = posts.length === 0 || daysOld > DORMANT_DAYS;
- const repetitive = last6.length >= REP_MIN && repCount >= REP_MIN;
+ // repetition is only actionable on a LIVE account — a dormant/dead account
+ // repeating its last (old) posts needs no editorial fix, so don't flag/score it.
+ const repetitive = !dormant && last6.length >= REP_MIN && repCount >= REP_MIN;
const slowing = !dormant && week7 <= 1; // trailing off — within the dormant window but posting ≤1/wk
return { a, posts, last6, count: posts.length, week7, newestTs, daysOld, dormant, repetitive, slowing, repCount };
});
@@ -87,9 +94,17 @@ window.MCC_PANELS['vendors'] = {
const arr = meta.slice();
if (mode === 'alpha') return arr.sort(alpha);
if (mode === 'posts') return arr.sort((x, y) => y.count - x.count || alpha(x, y));
- if (mode === 'recent') return arr.sort((x, y) => (y.newestTs ? new Date(y.newestTs) : 0) - (x.newestTs ? new Date(x.newestTs) : 0) || alpha(x, y));
- // 'attention' (default): worst first — dormant, then repetitive, then slowing, then stalest.
- const score = m => (m.dormant ? 3 : 0) + (m.repetitive ? 2 : 0) + (m.slowing ? 1 : 0);
+ if (mode === 'recent') return arr.sort((x, y) => x.daysOld - y.daysOld || alpha(x, y)); // newest first; 0-post (Infinity) last
+ // 'attention' (default): most ACTIONABLE first, not just "worst". An active account
+ // spamming one product is fixable RIGHT NOW, so it tops the list; a once-active
+ // account gone dark is next (investigate); then slowing; then never-posted (no
+ // creds — structural/known, needs no daily action); then healthy.
+ const score = m =>
+ m.repetitive ? 4 // active + repeating one product → editorial fix now
+ : (m.dormant && m.count > 0) ? 3 // was posting, went silent >14d → investigate
+ : m.slowing ? 2 // trailing off → re-engage
+ : m.dormant ? 1 // never posted (no creds) → known/structural
+ : 0;
return arr.sort((x, y) => score(y) - score(x) || (y.daysOld - x.daysOld) || alpha(x, y));
};
← 7064e7c Re-route Make-it-ours off Gemini: settlement vision → Claude
·
back to Marketing Command Center
·
Route settlement vision gate to local $0 ollama qwen2.5vl (b 4c5fac6 →