[object Object]

← back to Re Flyer Aggregator

sources dashboard: (1) fix article sort — detail rows + modal now sort by parsed date newest-first (was string-comparing RFC dates -> day-of-week order); (2) resizable columns — drag a header's right edge to widen, width persists, table expands right w/ horizontal scroll (Steve) (TK-10708)

8439848df58afa8962ebde944fc67df130f89e01 · 2026-08-19 23:18:11 -0700 · Steve Abrams

Files touched

Diff

commit 8439848df58afa8962ebde944fc67df130f89e01
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 19 23:18:11 2026 -0700

    sources dashboard: (1) fix article sort — detail rows + modal now sort by parsed date newest-first (was string-comparing RFC dates -> day-of-week order); (2) resizable columns — drag a header's right edge to widen, width persists, table expands right w/ horizontal scroll (Steve) (TK-10708)
---
 public/sources/index.html | 21 +++++++++++++++------
 1 file changed, 15 insertions(+), 6 deletions(-)

diff --git a/public/sources/index.html b/public/sources/index.html
index 1403a3a..539bbbd 100644
--- a/public/sources/index.html
+++ b/public/sources/index.html
@@ -55,8 +55,10 @@
  .mrow .md{color:#8A5A00;font-size:12px;margin-right:6px} .mrow .mm{color:#9aa;font-size:12px} .mrow b{color:#0E7C4A}
  .mtag{display:inline-block;background:#eef0f6;color:#445;border-radius:4px;padding:1px 7px;font-size:11px;margin:2px 4px 0 0}
  /* ---- full nav-agent table: draggable + toggleable columns, expandable rows, clickable numbers ---- */
- thead th[draggable]{cursor:grab} thead th[draggable]:active{cursor:grabbing} thead th.act{color:var(--blue)} thead th .ar{font-size:9px}
+ thead th{position:relative} thead th[draggable]{cursor:grab} thead th[draggable]:active{cursor:grabbing} thead th.act{color:var(--blue)} thead th .ar{font-size:9px}
  thead th.dragover{background:#dbe4fb}
+ .rz{position:absolute;right:-3px;top:0;width:8px;height:100%;cursor:col-resize;user-select:none;z-index:3} .rz:hover{background:#16357A22}
+ #srctable{width:auto;min-width:100%} .tblwrap{overflow-x:auto}
  td.xc,th.xc{width:24px;color:#9aa} .car{font-size:10px;cursor:pointer}
  tbody tr.srow{cursor:pointer} tbody tr.srow:hover>td{background:#fbfcff} tbody tr.srow.exp>td{background:#eef4ff}
  td.num{cursor:pointer} td.num:hover{text-decoration:underline}
@@ -102,7 +104,7 @@
   <span class="vsw"><button data-v="table" class="on">▦ Table</button><button data-v="grid">▤ Grid</button><button data-v="list">☰ List</button><button data-v="kv">⋮ KV</button></span>
   <label class="dens">Density <input type="range" id="dens" min="1" max="5" step="1" value="3"></label>
   <div class="stat" id="sstat"></div></div>
- <table id="srctable"><thead><tr id="thead"></tr></thead><tbody id="tb"></tbody></table>
+ <div class="tblwrap"><table id="srctable"><thead><tr id="thead"></tr></thead><tbody id="tb"></tbody></table></div>
  <div id="srccards"></div>
  <h2>DATABASE CRAWL — brokers (ads + flyers on their sites)</h2>
  <div class="card" id="crawl">Loading crawl status…</div>
@@ -125,7 +127,7 @@ function openModal(title,count,items,kind){
    const tags=Object.keys(PLAT).filter(k=>r[k]).map(k=>`<span class="mtag">${PLAT[k]}</span>`).join('');
    return `<div class="mrow"><a href="${href}" target="_blank" rel="noopener">${dom}</a> ${r.flyer_count?`<span class="mtag">📄 ${r.flyer_count} flyer${r.flyer_count>1?'s':''}</span>`:''} ${r.google_analytics?'<span class="mtag">GA</span>':''} ${tags}</div>`;}).join('');
  } else {
-  const s=items.slice().sort((a,b)=>(''+(b.date||'')).localeCompare(''+(a.date||'')));
+  const s=items.slice().sort((a,b)=>(Date.parse(b.date)||0)-(Date.parse(a.date)||0));
   b.innerHTML=s.map(x=>`<div class="mrow"><span class="md">${dt(x.date)}</span>${x.price?'<b>'+esc(x.price)+'</b> ':''}${x.link?`<a href="${x.link}" target="_blank" rel="noopener">${esc(x.title)}</a>`:esc(x.title)} <span class="mm">${esc(x.market||'')}</span></div>`).join('');
  }
  document.getElementById('modal').classList.add('show');
@@ -140,15 +142,16 @@ const NAVFIELDS=[['method','METHOD'],['region','MARKET']];
 const ALLCOLS=[['name','Source'],['method','Method'],['region','Market'],['pulled','Items pulled'],['deals','Deals kept'],['latest','Latest'],['recent','Recent from this source']];
 let COLORDER=JSON.parse(localStorage.srcColOrder||'null')||ALLCOLS.map(c=>c[0]);
 let CVIS=JSON.parse(localStorage.srcColVis||'null')||ALLCOLS.reduce((o,c)=>(o[c[0]]=true,o),{});
+let COLW=JSON.parse(localStorage.srcColW||'null')||{};   // per-column resized widths (px)
 ALLCOLS.forEach(c=>{if(!COLORDER.includes(c[0]))COLORDER.push(c[0]);if(!(c[0] in CVIS))CVIS[c[0]]=true;}); // heal schema drift
 const clabel=k=>(ALLCOLS.find(c=>c[0]===k)||[])[1]||k;
 const cols=()=>COLORDER.filter(k=>CVIS[k]);
 const latestOf=x=>Math.max(0,...(x.samples||[]).map(s=>Date.parse(s.date)||0));
 const EXP=new Set();
 function renderHead(){const tr=document.getElementById('thead');
- tr.innerHTML='<th class="xc"></th>'+cols().map(k=>`<th draggable="true" data-k="${k}" class="${sortK===k?'act':''}">${clabel(k)}<span class="ar">${sortK===k?(dir<0?' ▼':' ▲'):''}</span></th>`).join('');
+ tr.innerHTML='<th class="xc"></th>'+cols().map(k=>`<th draggable="true" data-k="${k}" class="${sortK===k?'act':''}" style="${COLW[k]?'width:'+COLW[k]+'px':''}">${clabel(k)}<span class="ar">${sortK===k?(dir<0?' ▼':' ▲'):''}</span><span class="rz" title="drag to resize"></span></th>`).join('');
  tr.querySelectorAll('th[data-k]').forEach(th=>{
-  th.onclick=()=>{const k=th.dataset.k;if(sortK===k)dir*=-1;else{sortK=k;dir=(k==='name'||k==='method'||k==='region')?1:-1;}renderHead();renderSrc();};
+  th.onclick=e=>{if(e.target.classList.contains('rz'))return;const k=th.dataset.k;if(sortK===k)dir*=-1;else{sortK=k;dir=(k==='name'||k==='method'||k==='region')?1:-1;}renderHead();renderSrc();};
   th.ondragstart=e=>{e.dataTransfer.setData('text/plain',th.dataset.k);th.classList.add('act');};
   th.ondragover=e=>{e.preventDefault();th.classList.add('dragover');};
   th.ondragleave=()=>th.classList.remove('dragover');
@@ -156,6 +159,12 @@ function renderHead(){const tr=document.getElementById('thead');
    if(from&&to&&from!==to){const a=COLORDER.filter(c=>c!==from);a.splice(a.indexOf(to),0,from);COLORDER=a;localStorage.srcColOrder=JSON.stringify(COLORDER);}
    renderHead();renderSrc();};
  });}
+// column RESIZE — drag a header's right edge; width persists (#thead is stable across renderHead)
+let _rz=null,_rzX=0,_rzW=0;
+document.getElementById('thead').addEventListener('mousedown',e=>{ if(!e.target.classList.contains('rz'))return;
+ e.preventDefault();e.stopPropagation(); const th=e.target.closest('th'); _rz=th;_rzX=e.clientX;_rzW=th.offsetWidth; th.draggable=false; document.body.style.userSelect='none'; });
+document.addEventListener('mousemove',e=>{ if(!_rz)return; _rz.style.width=Math.max(56,_rzW+(e.clientX-_rzX))+'px'; });
+document.addEventListener('mouseup',()=>{ if(!_rz)return; COLW[_rz.dataset.k]=_rz.offsetWidth; localStorage.srcColW=JSON.stringify(COLW); _rz.draggable=true; document.body.style.userSelect=''; _rz=null; });
 function cellHTML(x,k,max){
  if(k==='name')return `<td><b>${esc(x.name)}</b></td>`;
  if(k==='method')return `<td><span class="badge ${MB[x.method]}">${x.method}</span></td>`;
@@ -168,7 +177,7 @@ function cellHTML(x,k,max){
 function detailRow(x,span){
  return `<tr class="detail"><td colspan="${span}"><div class="dwrap">
   <div class="dm"><b>${esc(x.name)}</b> · ${x.method} · ${esc(x.region)} · pulled ${x.pulled} · kept ${x.deals||0}${x.url?` · <a href="${esc(x.url)}" target="_blank" rel="noopener">${esc(x.url.slice(0,64))}</a>`:''}</div>
-  ${(x.samples||[]).map(s=>`<div class="ds"><span style="color:#8A5A00">${dt(s.date)}</span> ${s.price?'<b>'+esc(s.price)+'</b> ':''}<a href="${s.link}" target="_blank" rel="noopener">${esc(s.title)}</a> <span style="color:#9aa">· ${esc(s.market||'')}</span></div>`).join('')||'<div class="ds" style="color:#9aa">no deal samples</div>'}</div></td></tr>`;}
+  ${(x.samples||[]).slice().sort((a,b)=>(Date.parse(b.date)||0)-(Date.parse(a.date)||0)).map(s=>`<div class="ds"><span style="color:#8A5A00">${dt(s.date)}</span> ${s.price?'<b>'+esc(s.price)+'</b> ':''}<a href="${s.link}" target="_blank" rel="noopener">${esc(s.title)}</a> <span style="color:#9aa">· ${esc(s.market||'')}</span></div>`).join('')||'<div class="ds" style="color:#9aa">no deal samples</div>'}</div></td></tr>`;}
 function buildNav(){ if(!SRC)return; let h='';
  for(const [key,lbl] of NAVFIELDS){ const counts={}; SRC.feeds.forEach(x=>{const v=x[key]||'—';counts[v]=(counts[v]||0)+1;});
   h+=`<div class="field">${lbl}</div>`;

← 471e0d7 capture: loosen 'deals kept' to RENTV-breadth (any recent CR  ·  back to Re Flyer Aggregator  ·  sources dashboard (table mode): each article on ONE LINE (wh 07ed1ce →