[object Object]

← 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

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 →