← back to Lifestyle Asset Intel
yolo tick #23: source citation links on comp rows
0e64230aef905c92fa9f9883c1ece2b3ec19dfd9 · 2026-05-13 19:16:40 -0700 · Steve Abrams
LEFT JOIN raw_observations in valueAsset() to expose payload.source_url
per comp. Asset detail view renders an external ↗ cite link (target=_blank,
rel=noopener noreferrer nofollow) next to each comp's source slug when a
URL is available. Adds .cite styling to app.css. Sotheby's editorial scrape
already populates raw_observations.payload.source_url, so 2 comps on the
birkin-20 detail page now resolve to live article URLs.
Files touched
M lib/valuation.jsM public/css/app.cssM views/asset.ejs
Diff
commit 0e64230aef905c92fa9f9883c1ece2b3ec19dfd9
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed May 13 19:16:40 2026 -0700
yolo tick #23: source citation links on comp rows
LEFT JOIN raw_observations in valueAsset() to expose payload.source_url
per comp. Asset detail view renders an external ↗ cite link (target=_blank,
rel=noopener noreferrer nofollow) next to each comp's source slug when a
URL is available. Adds .cite styling to app.css. Sotheby's editorial scrape
already populates raw_observations.payload.source_url, so 2 comps on the
birkin-20 detail page now resolve to live article URLs.
---
lib/valuation.js | 5 ++++-
public/css/app.css | 10 ++++++++++
views/asset.ejs | 6 +++++-
3 files changed, 19 insertions(+), 2 deletions(-)
diff --git a/lib/valuation.js b/lib/valuation.js
index 6fe547d..b2974b1 100644
--- a/lib/valuation.js
+++ b/lib/valuation.js
@@ -35,9 +35,11 @@ async function valueAsset(slug) {
const comps = await many(
`SELECT t.*, s.slug AS source_slug, s.name AS source_name,
- s.tier AS source_tier, s.weight AS source_weight
+ s.tier AS source_tier, s.weight AS source_weight,
+ ro.payload->>'source_url' AS source_url
FROM transactions t
JOIN sources s ON s.id = t.source_id
+ LEFT JOIN raw_observations ro ON ro.id = t.raw_observation_id
WHERE t.canonical_asset_id = $1
ORDER BY t.transacted_at DESC
LIMIT 50`,
@@ -168,6 +170,7 @@ async function valueAsset(slug) {
comps: comps.map((c) => ({
source: c.source_slug,
source_tier: c.source_tier,
+ source_url: c.source_url || null,
transacted_at: c.transacted_at,
gross: numberOrNull(c.gross_transaction_price),
net: numberOrNull(c.expected_net_seller_proceeds),
diff --git a/public/css/app.css b/public/css/app.css
index ccc91e9..4d7ad4d 100644
--- a/public/css/app.css
+++ b/public/css/app.css
@@ -215,6 +215,16 @@ tfoot tr.totals th, tfoot tr.totals td {
.facet-chip.facet-false { background: color-mix(in srgb, var(--bad) 10%, var(--card)); border-color: color-mix(in srgb, var(--bad) 30%, var(--line)); }
.facet-chip.facet-false .facet-v { color: var(--bad); }
+/* source citation link on comp rows (v0.2) */
+a.cite {
+ display: inline-block; margin-left: 0.4rem;
+ font-size: 0.72em; font-family: var(--mono);
+ color: var(--muted); text-decoration: none;
+ padding: 0.05rem 0.35rem; border-radius: 4px;
+ border: 1px solid var(--line);
+}
+a.cite:hover { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 50%, var(--line)); }
+
/* MSRP / premium-to-retail block (v0.2) */
.msrp-block .premium-cell { background: color-mix(in srgb, var(--accent) 14%, var(--card)); border-color: color-mix(in srgb, var(--accent) 35%, var(--line)); }
.msrp-block .premium-val { color: var(--accent); font-size: 1.05em; }
diff --git a/views/asset.ejs b/views/asset.ejs
index ccd2832..4e53821 100644
--- a/views/asset.ejs
+++ b/views/asset.ejs
@@ -101,7 +101,11 @@
<% data.comps.forEach(c => { %>
<tr>
<td><%= fmtDate(c.transacted_at) %></td>
- <td><%= c.source %></td>
+ <td>
+ <%= c.source %><% if (c.source_url) { %>
+ <a class="cite" href="<%= c.source_url %>" target="_blank" rel="noopener noreferrer nofollow" title="Open source page (new tab)">↗ cite</a>
+ <% } %>
+ </td>
<td>T<%= c.source_tier %></td>
<td>
<%= grade(c.condition_grade) %>
← 69ece51 yolo tick #22: source-weight-aware comparable_similarity_wei
·
back to Lifestyle Asset Intel
·
yolo tick #24: /api/family ?sort= (assets/comps/value/name/s e863c10 →