[object Object]

← 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

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 →