← back to Rentv
feat(closings-admin): sibling-drift fix — mirror the created date+time chip to closings.html (/api/closings joins registry first_seen; corpus rows use article date, date-only, no fabricated time). 100% card coverage
6033166b983a9280268b93d5da43180895716aab · 2026-08-06 08:29:40 -0700 · Steve
Files touched
M public/closings.htmlM server.js
Diff
commit 6033166b983a9280268b93d5da43180895716aab
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Aug 6 08:29:40 2026 -0700
feat(closings-admin): sibling-drift fix — mirror the created date+time chip to closings.html (/api/closings joins registry first_seen; corpus rows use article date, date-only, no fabricated time). 100% card coverage
---
public/closings.html | 9 +++++++++
server.js | 9 +++++++++
2 files changed, 18 insertions(+)
diff --git a/public/closings.html b/public/closings.html
index 1388eb06..fc6a9f97 100644
--- a/public/closings.html
+++ b/public/closings.html
@@ -61,6 +61,7 @@
.card h3{font-size:13px;color:var(--sub);font-weight:600;line-height:1.35}
.card .meta{font-size:12px;color:var(--sub);font-weight:600;display:flex;flex-wrap:wrap;gap:4px 10px}
.card .meta b{color:var(--ink)}
+ .card .when{font-size:11px;color:var(--sub);font-weight:600;letter-spacing:.02em}
.card .meta.parties{font-size:11px;gap:3px 8px}
.card .meta.parties span{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block}
.card .meta.parties .party{cursor:pointer;border-radius:4px}.card .meta.parties .party:hover{text-decoration:underline;color:var(--red)}
@@ -196,6 +197,13 @@ function dateOf(d){
const m=String(d.image||'').match(/([A-Z][a-z]+?)(\d{1,2})(\d{4})\.(?:jpg|jpeg|png|webp)/);if(!m)return null;const mo=MONTHS[m[1].slice(0,3)];if(mo==null)return null;return new Date(Date.UTC(+m[3],mo,+m[2]));
}
const fmtDate=dt=>dt?dt.toLocaleDateString(undefined,{month:'short',day:'numeric',year:'numeric'}):'';
+// Admin-card rule: show the RECORD's created date+time. A full ISO ts (feed deals, registry first_seen)
+// renders date+time; a DATE-ONLY value (corpus article date) renders date-only, LOCAL-parsed so a
+// UTC-midnight string doesn't slip to the prior evening, and with no fabricated clock.
+const fmtDateTime=v=>{if(!v)return'';const s=String(v);
+ const md=s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
+ if(md)return new Date(+md[1],+md[2]-1,+md[3]).toLocaleDateString(undefined,{month:'short',day:'numeric',year:'numeric'});
+ const dt=new Date(s);return isNaN(dt)?'':dt.toLocaleString(undefined,{month:'short',day:'numeric',year:'numeric',hour:'numeric',minute:'2-digit'});};
const isManual=d=>d._manual||String(d.id||'').startsWith('mc');
function pills(){
@@ -244,6 +252,7 @@ function render(){
${loc?`<div class="meta"><span class="drill" data-filt="q" data-val="${esc(loc)}" title="Filter by ${esc(loc)}">📍 <b>${esc(loc)}</b></span>${d.size_label?`<span>📐 ${esc(d.size_label)}</span>`:''}${d.year_built?`<span>🏗 ${esc(d.year_built)}</span>`:''}</div>`:''}
${(d.buyer||d.seller||d.broker)?`<div class="meta parties">${d.buyer?`<span class="party" data-party="${esc(d.buyer)}" title="Buyer — click to see this party's deals">🟢 <b>${esc(d.buyer)}</b></span>`:''}${d.seller?`<span class="party" data-party="${esc(d.seller)}" title="Seller — click to filter">🔴 ${esc(d.seller)}</span>`:''}${d.broker?`<span class="party" data-party="${esc(d.broker)}" title="Broker — click to filter">🤝 ${esc(d.broker)}</span>`:''}</div>`:''}
${(d.address&&d.title)?`<h3>${esc(d.title)}</h3>`:''}
+ ${d.created_at?`<div class="when" title="${esc(d.created_at)}">🕓 Added ${esc(fmtDateTime(d.created_at))}</div>`:''}
<div class="cardfoot">
${story}
<div class="acts">
diff --git a/server.js b/server.js
index 963254ac..51185606 100644
--- a/server.js
+++ b/server.js
@@ -416,6 +416,9 @@ function corpusClosings() {
amount: a.amount || null, amount_label: (amt > 0 && amt < 1_000_000) ? '' : (a.amount_label || ''),
size_label: a.size_label || '', units: a.units || null, sqft: a.sqft || null,
address: null, close_date: a.date || '', year_built: null,
+ // article date as the record's captured-on proxy (date-only; the chip renders it without a
+ // fabricated clock) — mirrors corpusDeals so closings admin cards satisfy the created-date rule.
+ created_at: a.date || null,
buyer: a.buyer || '', seller: a.seller || '', broker: a.broker || '', summary: a.summary || '',
};
});
@@ -436,6 +439,10 @@ app.get('/api/closings', adminOnly, (req, r) => {
const corpusById = new Map(allCorpus.map((a) => [String(a.id), a]));
const corpusExtra = allCorpus.filter((a) => !feedIds.has(String(a.id)));
const base = [...feedSales, ...corpusExtra];
+ // Registry first_seen = when RENTV first captured a feed deal → created_at, so the closings admin
+ // grid shows the record's created date+time (Steve's admin-card rule); corpus rows fall back to the
+ // article date set in corpusClosings above.
+ const regDeals = readJSON('deals-registry.json', { deals: {} }).deals || {};
// The deals-pull feed carries images/price but NOT the LLM-extracted party fields — those live
// only in the corpus. GAP-FILL each feed row from its matching corpus article (never overwrite
// a field the feed already has) so buyer/seller/broker actually surface on feed closings.
@@ -448,6 +455,8 @@ app.get('/api/closings', adminOnly, (req, r) => {
const row = e ? { ...d2, ...e, _edited: true } : { ...d2 };
row._hidden = hides.has(String(d.id));
row._corpus = feedIds.has(String(d.id)) ? undefined : true;
+ const reg = regDeals[String(d.id)];
+ row.created_at = row.created_at || (reg && reg.first_seen) || null; // when RENTV first captured it
return row;
}).filter((d) => includeHidden || !d._hidden);
const adds = (ov.adds || []).map((a) => ({ ...a, _manual: true }));
← 5de160cc harden(deals-admin): Cody gate — every card now shows a crea
·
back to Rentv
·
Refresh RENTV wordmark → 2026 liquid-glass logo from rentv.c 65458b63 →