[object Object]

โ† back to Ventura Corridor

iter 54: cross-surface deep-linking โ€” /pitches.html expanded detail panel gets 5 cross-action buttons (๐Ÿ“ฎ Postcard preview, ๐Ÿข Building roster, ๐Ÿ“ Log response, ๐Ÿšถ Walk route, ๐Ÿ“œ Timeline JSON); /postcards.html accepts ?id= URL param + falls back to /api/pitches?id= when pitch isn't in postcard cohort; /buildings.html accepts ?bldg= param + auto-expands matching card with smooth scroll; /responses.html accepts ?id= param + auto-opens edit modal after 250ms; /walk-route.html accepts ?focus=<pitchId> + scrolls and flashes matching tenant row, widens cohort if needed

8620d2683b8fbd934f7de087524aff32e3754934 ยท 2026-05-06 12:52:55 -0700 ยท SteveStudio2

Files touched

Diff

commit 8620d2683b8fbd934f7de087524aff32e3754934
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Wed May 6 12:52:55 2026 -0700

    iter 54: cross-surface deep-linking โ€” /pitches.html expanded detail panel gets 5 cross-action buttons (๐Ÿ“ฎ Postcard preview, ๐Ÿข Building roster, ๐Ÿ“ Log response, ๐Ÿšถ Walk route, ๐Ÿ“œ Timeline JSON); /postcards.html accepts ?id= URL param + falls back to /api/pitches?id= when pitch isn't in postcard cohort; /buildings.html accepts ?bldg= param + auto-expands matching card with smooth scroll; /responses.html accepts ?id= param + auto-opens edit modal after 250ms; /walk-route.html accepts ?focus=<pitchId> + scrolls and flashes matching tenant row, widens cohort if needed
---
 public/buildings.html  | 18 +++++++++++++++++-
 public/pitches.html    |  7 +++++++
 public/postcards.html  | 26 ++++++++++++++++++++++----
 public/responses.html  |  9 +++++++++
 public/walk-route.html | 19 ++++++++++++++++++-
 5 files changed, 73 insertions(+), 6 deletions(-)

diff --git a/public/buildings.html b/public/buildings.html
index 6ca02da..f28c508 100644
--- a/public/buildings.html
+++ b/public/buildings.html
@@ -284,7 +284,23 @@ document.getElementById('min-tenants').addEventListener('change', load);
 document.getElementById('sort').addEventListener('change', load);
 document.getElementById('filter').addEventListener('input', applyFilter);
 
-load();
+// Deep-link via ?bldg= URL param: pre-filter + auto-expand the matching card
+(async () => {
+  const urlBldg = new URLSearchParams(location.search).get('bldg');
+  if (urlBldg) {
+    document.getElementById('filter').value = urlBldg;
+    document.getElementById('min-tenants').value = '2';
+    await load();
+    // Expand the first matching card
+    const target = document.querySelector(`.bldg[data-addr="${encodeURIComponent(urlBldg)}"] .addr`);
+    if (target) {
+      target.scrollIntoView({ behavior: 'smooth', block: 'center' });
+      toggleBldg(target);
+    }
+  } else {
+    load();
+  }
+})();
 </script>
 </body>
 </html>
diff --git a/public/pitches.html b/public/pitches.html
index d7e100c..2fe951c 100644
--- a/public/pitches.html
+++ b/public/pitches.html
@@ -312,6 +312,13 @@ function render() {
           <h4>Notes</h4>
           <textarea data-field="notes" data-id="${r.id}" placeholder="Internal notes, follow-ups, contextโ€ฆ">${escapeHtml(r.notes || '')}</textarea>
           <div class="actions">${statusActions(r)}</div>
+          <div class="cross-actions" style="margin-top:14px;padding-top:12px;border-top:1px dashed var(--rule);display:flex;gap:8px;flex-wrap:wrap;font-size:10px;letter-spacing:.18em;text-transform:uppercase">
+            <a href="/postcards.html?id=${r.id}" target="_blank" style="color:var(--metal);text-decoration:none;padding:5px 10px;border:1px solid var(--rule)">๐Ÿ“ฎ Postcard preview</a>
+            <a href="/buildings.html?bldg=${encodeURIComponent((r.address||'').replace(/\s*(SUITE|STE|UNIT|#).*$/i,'').trim())}" target="_blank" style="color:var(--metal);text-decoration:none;padding:5px 10px;border:1px solid var(--rule)">๐Ÿข Building roster</a>
+            <a href="/responses.html?id=${r.id}" target="_blank" style="color:var(--metal);text-decoration:none;padding:5px 10px;border:1px solid var(--rule)">๐Ÿ“ Log response</a>
+            <a href="/walk-route.html?focus=${r.id}" target="_blank" style="color:var(--metal);text-decoration:none;padding:5px 10px;border:1px solid var(--rule)">๐Ÿšถ Walk route</a>
+            <a href="/api/pitches/${r.id}/timeline" target="_blank" style="color:var(--ink-mute);text-decoration:none;padding:5px 10px;border:1px solid var(--rule);font-family:var(--mono);font-size:9px">๐Ÿ“œ Timeline JSON</a>
+          </div>
           <div style="font-family:var(--mono);font-size:9px;color:var(--ink-mute);margin-top:14px">
             id ${r.id} ยท biz #${r.business_id} ยท created ${new Date(r.created_at).toLocaleDateString()} ยท last update ${new Date(r.updated_at).toLocaleString()}
             ${r.sent_at ? `ยท sent ${new Date(r.sent_at).toLocaleString()}` : ''}
diff --git a/public/postcards.html b/public/postcards.html
index 0a55e9c..04ccdde 100644
--- a/public/postcards.html
+++ b/public/postcards.html
@@ -196,7 +196,19 @@ async function loadPitchList() {
       `<option value="${p.id}">${(p.name || '').slice(0, 50)} ยท ${p.dw_proximity || ''} ยท pri ${p.priority}</option>`
     ).join('');
     sel.addEventListener('change', () => loadPreview(parseInt(sel.value, 10)));
-    if (allPitches.length) loadPreview(parseInt(sel.value, 10));
+    // Deep-link via ?id= param (from /pitches.html cross-action)
+    const urlId = parseInt(new URLSearchParams(location.search).get('id') || '', 10);
+    if (Number.isFinite(urlId)) {
+      sel.value = String(urlId);
+      loadPreview(urlId);
+      // If the pitch wasn't in the dropdown (only top 250 sorted by proximity), still fetch by ID
+      if (sel.value !== String(urlId)) {
+        document.getElementById('pitch-id-input').value = String(urlId);
+        loadPreview(urlId);
+      }
+    } else if (allPitches.length) {
+      loadPreview(parseInt(sel.value, 10));
+    }
   } catch (e) {
     document.getElementById('pitch-select').innerHTML = '<option>error loading: ' + e.message + '</option>';
   }
@@ -207,14 +219,20 @@ async function loadPreview(id) {
   currentId = id;
   document.getElementById('iframe-front').src = `/api/pitches/${id}/postcard-preview/front.html`;
   document.getElementById('iframe-back').src  = `/api/pitches/${id}/postcard-preview/back.html`;
-  // Pull pitch metadata for the info strip
-  const p = allPitches.find(x => String(x.id) === String(id));
+  // Pull pitch metadata for the info strip โ€” fallback to fetch if not in current cohort
+  let p = allPitches.find(x => String(x.id) === String(id));
+  if (!p) {
+    try {
+      const d = await fetch('/api/pitches?id=' + id).then(r => r.json());
+      p = (d.rows || [])[0];
+    } catch {}
+  }
   if (p) {
     const info = document.getElementById('pitch-info');
     info.style.display = 'block';
     info.innerHTML = `
       <b>${escapeHtml(p.name)}</b>
-      <div class="meta">${escapeHtml(p.address || '')} ยท ${escapeHtml(p.dw_proximity || '')} ยท ${escapeHtml(p.pitch_type || '')} ยท pri ${p.priority} ยท status ${p.status}</div>
+      <div class="meta">${escapeHtml(p.address || '')} ยท ${escapeHtml(p.dw_proximity || '')} ยท ${escapeHtml(p.pitch_type || '')} ยท pri ${p.priority} ยท status ${p.status} ยท channel ${escapeHtml(p.outreach_channel || 'โ€”')}</div>
     `;
   }
 }
diff --git a/public/responses.html b/public/responses.html
index b23eedd..7118ba1 100644
--- a/public/responses.html
+++ b/public/responses.html
@@ -473,6 +473,15 @@ document.querySelectorAll('.filter-bar .pill').forEach(p => {
 loadFunnel();
 loadFollowups();
 loadResponses();
+
+// Deep-link via ?id= URL param: open edit modal for that pitch
+(async () => {
+  const urlId = parseInt(new URLSearchParams(location.search).get('id') || '', 10);
+  if (Number.isFinite(urlId)) {
+    // Wait briefly so the response list loads first (so the row is visible behind the modal)
+    setTimeout(() => openModalById(urlId), 250);
+  }
+})();
 </script>
 </body>
 </html>
diff --git a/public/walk-route.html b/public/walk-route.html
index f634ab6..147955b 100644
--- a/public/walk-route.html
+++ b/public/walk-route.html
@@ -389,7 +389,24 @@ document.getElementById('direction').addEventListener('change', load);
 document.getElementById('limit').addEventListener('change', load);
 document.getElementById('filter-status').addEventListener('change', load);
 
-load();
+(async () => {
+  await load();
+  const urlFocus = new URLSearchParams(location.search).get('focus');
+  if (!urlFocus) return;
+  const flash = (el) => {
+    el.scrollIntoView({ behavior: 'smooth', block: 'center' });
+    el.style.transition = 'box-shadow 0.4s';
+    el.style.boxShadow = '0 0 0 2px var(--metal-glow)';
+    setTimeout(() => { el.style.boxShadow = ''; }, 2500);
+  };
+  let el = document.querySelector(`.tenant[data-pitch-id="${urlFocus}"]`);
+  if (el) return flash(el);
+  document.getElementById('limit').value = '80';
+  document.getElementById('filter-status').value = 'all';
+  await load();
+  el = document.querySelector(`.tenant[data-pitch-id="${urlFocus}"]`);
+  if (el) flash(el);
+})();
 </script>
 </body>
 </html>

โ† 410b896 iter 53: building 360ยฐ tenant roster โ€” migration 013 adds v_  ยท  back to Ventura Corridor  ยท  iter 55: same-tenant detection โ€” migration 014 adds pg_trgm, 65148eb โ†’