← back to Marketing Command Center
Refine Owned·DW Fleet: surface dormant + repetition health signals + sort
f91859accf3a535e24282a99e47ec8a9778c76ed · 2026-08-25 10:30:13 -0700 · Steve
Turn the owned-account posts view from a flat alpha list into an at-a-glance health
read, operationalizing the two real problems the data showed:
- 💤 dormant flag: 0 posts or none in >14 days (the 6 no-creds accounts, plus any
that go quiet); the row is tinted so it stands out
- ↻ N× same flag: ≥4 of the last 6 posts share one product title (catches
@asseeninhotels posting the same SKU 6×, @suedewallpaper 4×)
- summary line: 'X/35 posting · Y dormant · Z repetitive'
- per-account recency ('N posts · Xd ago')
- sort control (Needs attention [default] / Recent activity / Most posts / A→Z),
localStorage-persisted per the standing sort rule
Verified: summary 29/35 · 6 dormant · 2 repetitive; flags render; attention-sort puts
flagged accounts first; re-sort works; no page errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M public/panels/vendors.htmlM public/panels/vendors.js
Diff
commit f91859accf3a535e24282a99e47ec8a9778c76ed
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Aug 25 10:30:13 2026 -0700
Refine Owned·DW Fleet: surface dormant + repetition health signals + sort
Turn the owned-account posts view from a flat alpha list into an at-a-glance health
read, operationalizing the two real problems the data showed:
- 💤 dormant flag: 0 posts or none in >14 days (the 6 no-creds accounts, plus any
that go quiet); the row is tinted so it stands out
- ↻ N× same flag: ≥4 of the last 6 posts share one product title (catches
@asseeninhotels posting the same SKU 6×, @suedewallpaper 4×)
- summary line: 'X/35 posting · Y dormant · Z repetitive'
- per-account recency ('N posts · Xd ago')
- sort control (Needs attention [default] / Recent activity / Most posts / A→Z),
localStorage-persisted per the standing sort rule
Verified: summary 29/35 · 6 dormant · 2 repetitive; flags render; attention-sort puts
flagged accounts first; re-sort works; no page errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
public/panels/vendors.html | 11 +++++-
public/panels/vendors.js | 87 ++++++++++++++++++++++++++++++++++++++--------
2 files changed, 82 insertions(+), 16 deletions(-)
diff --git a/public/panels/vendors.html b/public/panels/vendors.html
index 0fe80e2..9d9668e 100644
--- a/public/panels/vendors.html
+++ b/public/panels/vendors.html
@@ -9,7 +9,16 @@
</div>
<div class="card" id="vend-owned-card">
<h2>Owned · DW Fleet</h2>
- <div class="muted" style="margin-bottom:10px">Our own Instagram accounts across the DW fleet. Click any handle to open the account.</div>
+ <div class="row" style="justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:10px">
+ <div class="muted" id="vend-owned-summary">Our own Instagram accounts across the DW fleet — last 6 posts each.</div>
+ <div style="display:flex;align-items:center;gap:6px"><label class="fld" style="font-size:11px">Sort</label>
+ <select id="vend-owned-sort">
+ <option value="attention">Needs attention</option>
+ <option value="recent">Recent activity</option>
+ <option value="posts">Most posts</option>
+ <option value="alpha">A→Z</option>
+ </select></div>
+ </div>
<div id="vend-owned-rows"><div class="muted">Loading owned accounts…</div></div>
</div>
<div class="card">
diff --git a/public/panels/vendors.js b/public/panels/vendors.js
index f641d0f..9b966ea 100644
--- a/public/panels/vendors.js
+++ b/public/panels/vendors.js
@@ -40,8 +40,7 @@ window.MCC_PANELS['vendors'] = {
for (const k of Object.keys(byHandle)) byHandle[k].sort((a, b) => new Date(b.ts) - new Date(a.ts));
} catch { /* ledger unreachable — rows render without posts */ }
- const ownedPosts = (handle) => {
- const list = (byHandle[handle] || []).slice(0, 6);
+ const ownedPosts = (list) => {
if (!list.length) return '<div class="muted" style="font-size:11px;padding:2px 2px 4px">No posts yet.</div>';
const items = list.map(p => {
const cap = esc((p.product_title || '').replace(/\s+/g, ' ').slice(0, 120)) || '(no title)';
@@ -60,19 +59,73 @@ window.MCC_PANELS['vendors'] = {
return `<div class="ig-list">${items}</div>`;
};
- const rows = [...owned].sort((a, b) => String(a.name || a.handle).localeCompare(String(b.name || b.handle)));
- host.innerHTML = rows.map(a => {
- const h = esc(a.handle);
- const n = (byHandle[a.handle] || []).length;
- return `<div class="vend-block" style="border-bottom:1px solid var(--line);padding:10px 2px;background:#faf6ee">
- <div class="row" style="justify-content:space-between;align-items:center">
- <div style="min-width:200px"><b>${esc(a.name || a.handle)}</b> <span class="pill">OURS</span></div>
- <div style="flex:1"><a class="lnk" href="https://www.instagram.com/${h}/" target="_blank" rel="noopener noreferrer">@${h} ↗</a></div>
- <div style="min-width:70px;text-align:right;font-variant-numeric:tabular-nums" title="posts in the fleet ledger">${n ? n + ' posts' : ''}</div>
- </div>
- ${ownedPosts(a.handle)}
- </div>`;
- }).join('');
+ // Per-account health signals computed from the ledger — surfaced so the two
+ // real problems in the data (dormant accounts + one-product repetition) are
+ // visible at a glance instead of hidden in a flat alpha list.
+ const DORMANT_DAYS = 14, REP_MIN = 4; // repetitive = ≥4 of the last 6 posts share one title
+ const meta = owned.map(a => {
+ 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 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 dormant = posts.length === 0 || daysOld > DORMANT_DAYS;
+ const repetitive = last6.length >= REP_MIN && repCount >= REP_MIN;
+ return { a, posts, last6, count: posts.length, newestTs, daysOld, dormant, repetitive, repCount };
+ });
+
+ const SORT_KEY = 'mcc_vendors_owned_sort';
+ const sortSel = root.querySelector('#vend-owned-sort');
+ if (sortSel) sortSel.value = localStorage.getItem(SORT_KEY) || 'attention';
+ const sortMeta = (mode) => {
+ const alpha = (x, y) => String(x.a.name || x.a.handle).localeCompare(String(y.a.name || y.a.handle));
+ 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 + repetitive on top, then stalest.
+ const score = m => (m.dormant ? 2 : 0) + (m.repetitive ? 1 : 0);
+ return arr.sort((x, y) => score(y) - score(x) || (y.daysOld - x.daysOld) || alpha(x, y));
+ };
+
+ const flag = (bg, fg, txt, title) => `<span class="pill" style="background:${bg};color:${fg}" title="${esc(title)}">${txt}</span>`;
+ const renderOwned = (mode) => {
+ host.innerHTML = sortMeta(mode).map(m => {
+ const a = m.a, h = esc(a.handle);
+ const recency = m.count ? ago(m.newestTs) : '—';
+ const flags = [
+ m.dormant ? flag('#f7e2e2', '#a53a3a', '💤 dormant', m.count ? `No posts in ${Math.round(m.daysOld)} days` : 'No posts yet (account has no creds / hasn’t published)') : '',
+ m.repetitive ? flag('#fdf3dc', '#8a6a1e', `↻ ${m.repCount}× same`, `${m.repCount} of the last ${m.last6.length} posts are the same product — vary the content`) : '',
+ ].filter(Boolean).join(' ');
+ return `<div class="vend-block" style="border-bottom:1px solid var(--line);padding:10px 2px;background:${m.dormant ? '#fbf3f2' : '#faf6ee'}">
+ <div class="row" style="justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap">
+ <div style="min-width:200px"><b>${esc(a.name || a.handle)}</b> <span class="pill">OURS</span> ${flags}</div>
+ <div style="flex:1"><a class="lnk" href="https://www.instagram.com/${h}/" target="_blank" rel="noopener noreferrer">@${h} ↗</a></div>
+ <div style="min-width:120px;text-align:right;font-variant-numeric:tabular-nums;font-size:11.5px;color:var(--muted,#8a8372)" title="posts in the fleet ledger">${m.count ? `${m.count} posts · ${recency}` : ''}</div>
+ </div>
+ ${ownedPosts(m.last6)}
+ </div>`;
+ }).join('');
+ };
+
+ // Summary line — the at-a-glance fleet health read.
+ const summaryEl = root.querySelector('#vend-owned-summary');
+ if (summaryEl) {
+ const posting = meta.filter(m => m.count).length;
+ const dormant = meta.filter(m => m.dormant).length;
+ const repetitive = meta.filter(m => m.repetitive).length;
+ summaryEl.innerHTML = `<b>${posting}</b>/${meta.length} posting · ` +
+ `<b style="color:${dormant ? '#a53a3a' : 'inherit'}">${dormant}</b> dormant · ` +
+ `<b style="color:${repetitive ? '#8a6a1e' : 'inherit'}">${repetitive}</b> repetitive — last 6 posts each.`;
+ }
+
+ renderOwned(sortSel ? sortSel.value : 'attention');
+ if (sortSel && !sortSel.dataset.wired) {
+ sortSel.dataset.wired = '1';
+ sortSel.onchange = () => { localStorage.setItem(SORT_KEY, sortSel.value); renderOwned(sortSel.value); };
+ }
})();
// Amplify kit registry — raw (UNESCAPED) clipboard text + X-composer URL per
@@ -197,6 +250,10 @@ window.MCC_PANELS['vendors'] = {
image: p.image || '',
permalink,
srcCaption: fullCap,
+ // Carried for refresh-on-click matching (TK-10843): the IG media id is the
+ // strongest key, then permalink, then timestamp+caption.
+ mediaId: p.id || '',
+ timestamp: p.timestamp || '',
};
return { id, xUrl };
};
← 6b6da55 Add "Make it ours" DW-original creative system to #vendors a
·
back to Marketing Command Center
·
Owned·DW Fleet: add 7-day cadence + 'slowing' signal 9690867 →