โ 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
M public/buildings.htmlM public/pitches.htmlM public/postcards.htmlM public/responses.htmlM public/walk-route.html
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 โ