feat: GBP Snapshot Form v1.3 — NOT_PRESENT/NOT_OBSERVED vocabulary, observation surface, address_canonical, Likely Claimed — inferred, R1–R6, auto-computed PLACE_ID from URL, complete action-button row, Download .txt, auto-datetime+TZ

This commit is contained in:
2026-08-04 15:27:49 +00:00
parent 3c402a6672
commit a191c17cf4
+596 -176
View File
@@ -3,170 +3,447 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>DOP — GBP Snapshot Intake v1.2</title>
<title>DOP — GBP Snapshot Intake v1.3</title>
<style>
:root { --bg:#0f1419; --panel:#1a2332; --border:#2d3a4f; --text:#e7ecf3; --muted:#8b9bb4; --accent:#3b82f6; --input:#0d1219; --ok:#22c55e; }
:root {
--bg: #0f1419;
--panel: #1a2332;
--border: #2d3a4f;
--text: #e7ecf3;
--muted: #8b9bb4;
--accent: #3b82f6;
--accent-hover: #2563eb;
--success: #22c55e;
--input-bg: #0d1219;
}
* { box-sizing: border-box; }
body { margin:0; font-family: system-ui, sans-serif; background:var(--bg); color:var(--text); line-height:1.45; }
.wrap { max-width:900px; margin:0 auto; padding:24px 16px 48px; }
h1 { font-size:1.3rem; margin:0 0 4px; }
.sub { color:var(--muted); font-size:0.9rem; margin-bottom:18px; }
.card { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:16px 16px 8px; margin-bottom:14px; }
.card h2 { font-size:0.8rem; text-transform:uppercase; letter-spacing:0.04em; color:var(--muted); margin:0 0 12px; }
.grid { display:grid; grid-template-columns:1fr 1fr; gap:10px 14px; }
@media (max-width:640px){ .grid { grid-template-columns:1fr; } }
.full { grid-column:1 / -1; }
label { display:block; font-size:0.8rem; color:var(--muted); margin-bottom:4px; }
input, select, textarea { width:100%; background:var(--input); border:1px solid var(--border); border-radius:8px; color:var(--text); padding:9px 11px; font-size:0.95rem; outline:none; }
input:focus, select:focus, textarea:focus { border-color:var(--accent); }
textarea { min-height:64px; resize:vertical; }
.actions { display:flex; flex-wrap:wrap; gap:10px; margin:18px 0 10px; }
button { border:none; border-radius:8px; padding:11px 16px; font-size:0.95rem; font-weight:600; cursor:pointer; }
.btn-primary { background:var(--accent); color:#fff; }
.btn-secondary { background:transparent; color:var(--text); border:1px solid var(--border); }
.status { font-size:0.9rem; color:var(--ok); min-height:1.2rem; }
#output { width:100%; min-height:280px; font-family:ui-monospace,monospace; font-size:0.82rem; background:var(--input); border:1px solid var(--border); border-radius:10px; color:var(--text); padding:12px; white-space:pre; }
.hint { font-size:0.82rem; color:var(--muted); margin-top:8px; }
body {
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.45;
}
.wrap {
max-width: 900px;
margin: 0 auto;
padding: 24px 16px 48px;
}
h1 {
font-size: 1.35rem;
margin: 0 0 4px;
font-weight: 650;
}
.sub {
color: var(--muted);
font-size: 0.92rem;
margin-bottom: 20px;
}
.card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px 16px 8px;
margin-bottom: 14px;
}
.card h2 {
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--muted);
margin: 0 0 12px;
font-weight: 600;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px 14px;
}
@media (max-width: 640px) {
.grid { grid-template-columns: 1fr; }
}
.full { grid-column: 1 / -1; }
label {
display: block;
font-size: 0.8rem;
color: var(--muted);
margin-bottom: 4px;
}
input[type="text"],
input[type="url"],
select,
textarea {
width: 100%;
background: var(--input-bg);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
padding: 9px 11px;
font-size: 0.95rem;
outline: none;
}
input:focus, select:focus, textarea:focus {
border-color: var(--accent);
}
textarea { min-height: 72px; resize: vertical; }
.actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 18px 0 10px;
}
button {
border: none;
border-radius: 8px;
padding: 11px 16px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
}
.btn-primary {
background: var(--accent);
color: white;
}
.btn-primary:hover { background: var(--accent-hover); }
.btn-secondary {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
}
.btn-secondary:hover { border-color: var(--muted); }
.status {
font-size: 0.9rem;
color: var(--success);
min-height: 1.2em;
margin-bottom: 8px;
}
#output {
width: 100%;
min-height: 360px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.82rem;
line-height: 1.4;
background: var(--input-bg);
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text);
padding: 12px;
white-space: pre;
}
.hint {
font-size: 0.82rem;
color: var(--muted);
margin-top: 8px;
}
.vocab-hint {
font-size: 0.78rem;
color: var(--muted);
font-style: italic;
margin-bottom: 6px;
}
</style>
</head>
<body>
<div class="wrap">
<h1>DOP — GBP Snapshot Intake v1.2</h1>
<p class="sub">NOT_PRESENT = looked, confirmed absent · NOT_OBSERVED = did not look / could not see. One primary category only. Paste output to Leonard (execution card v1.2).</p>
<div class="wrap">
<h1>DOP — GBP Snapshot Intake v1.3</h1>
<p class="sub">Complete cold-audit form. <strong>NOT_PRESENT</strong> = looked, confirmed absent. <strong>NOT_OBSERVED</strong> = did not look / could not see. One primary category only. Paste output to Leonard — follows execution card v1.2 (R1R6).</p>
<form id="gbpForm" autocomplete="off">
<!-- Snapshot Metadata -->
<div class="card">
<h2>Snapshot Metadata</h2>
<div class="grid">
<div><label for="client">Client name</label><input id="client" type="text" /></div>
<div><label for="snapshot_dt">Snapshot datetime + TZ (ISO)</label><input id="snapshot_dt" type="text" placeholder="2026-08-03T14:30:00-07:00" /></div>
<div><label for="source">Source</label>
<div>
<label for="client">Client name</label>
<input id="client" type="text" placeholder="Client name" />
</div>
<div>
<label for="snapshot_dt">Snapshot datetime + TZ (ISO 8601)</label>
<input id="snapshot_dt" type="text" placeholder="2026-08-04T14:30:00-07:00" />
</div>
<div>
<label for="source">Source</label>
<select id="source">
<option>Human eye on Maps</option>
<option>Places API</option>
<option>Owner dashboard</option>
<option value="Human eye on Maps">Human eye on Maps</option>
<option value="Places API">Places API</option>
<option value="Owner dashboard">Owner dashboard</option>
</select>
</div>
<div><label for="surface">Observation surface</label>
<div>
<label for="surface">Observation surface</label>
<select id="surface">
<option value="">— select —</option>
<option value="desktop_maps">desktop_maps</option>
<option value="mobile_maps">mobile_maps</option>
<option value="knowledge_panel">knowledge_panel</option>
<option value="other">other</option>
</select>
</div>
<div class="full"><label for="maps_url">Maps or Place URL</label><input id="maps_url" type="url" placeholder="https://www.google.com/maps/place/..." /></div>
<div><label for="place_id">Place ID (blank = agent computes from URL)</label><input id="place_id" type="text" /></div>
<div class="full">
<label for="maps_url">Maps or Place URL</label>
<input id="maps_url" type="url" placeholder="https://www.google.com/maps/place/..." />
</div>
<div>
<label for="place_id">Place ID</label>
<input id="place_id" type="text" placeholder="Leave blank — auto-computed from URL" />
</div>
<div>
<label for="computed_ids">Computed IDs (auto-filled)</label>
<input id="computed_ids" type="text" readonly placeholder="PLACE_ID · feature · CID" style="color:var(--muted)" />
</div>
</div>
</div>
<!-- Core Fields -->
<div class="card">
<h2>Core</h2>
<h2>Core Fields</h2>
<div class="vocab-hint">Copy exactly as shown. Use NOT_OBSERVED if you didn't see it.</div>
<div class="grid">
<div><label for="business_name">Business name shown</label><input id="business_name" type="text" /></div>
<div><label for="address">Address shown (exact)</label><input id="address" type="text" /></div>
<div><label for="address_canonical">Address canonical (optional)</label><input id="address_canonical" type="text" placeholder="Suite not #" /></div>
<div><label for="phone">Phone (full digits, no mask)</label><input id="phone" type="text" placeholder="+1 530-929-7120" /></div>
<div><label for="website">Website shown</label><input id="website" type="text" /></div>
<div><label for="primary_category">Primary category (ONE only)</label><input id="primary_category" type="text" placeholder="e.g. Gym" /></div>
<div class="full"><label for="additional_categories">Additional categories (list)</label><input id="additional_categories" type="text" /></div>
<div>
<label for="business_name">Business name shown</label>
<input id="business_name" type="text" />
</div>
<div>
<label for="address">Address shown (exact string)</label>
<input id="address" type="text" />
</div>
<div>
<label for="address_canonical">Address canonical (optional normalize)</label>
<input id="address_canonical" type="text" placeholder="Suite not #, Drive not Dr" />
</div>
<div>
<label for="phone">Phone shown (full digits, no masking)</label>
<input id="phone" type="text" placeholder="+1 530-929-7120" />
</div>
<div>
<label for="website">Website shown</label>
<input id="website" type="text" />
</div>
<div>
<label for="primary_category">Primary category <strong>(ONE canonical value)</strong></label>
<input id="primary_category" type="text" placeholder="e.g. Gym · Physical fitness program · Personal trainer" />
</div>
<div class="full">
<label for="additional_categories">Additional categories (comma-separated)</label>
<input id="additional_categories" type="text" />
</div>
</div>
</div>
<!-- Reputation -->
<div class="card">
<h2>Reputation</h2>
<div class="grid">
<div><label for="star_rating">Star rating</label><input id="star_rating" type="text" /></div>
<div><label for="review_count">Review count</label><input id="review_count" type="text" /></div>
<div><label for="review_responses">Review responses observed</label>
<select id="review_responses"><option>NOT_OBSERVED</option><option>Yes</option><option>No</option></select>
<div>
<label for="star_rating">Star rating</label>
<input id="star_rating" type="text" placeholder="e.g. 4.8" />
</div>
<div><label for="review_responses_detail">If Yes: N of total with replies + most recent age</label><input id="review_responses_detail" type="text" /></div>
<div>
<label for="review_count">Review count</label>
<input id="review_count" type="text" placeholder="e.g. 42" />
</div>
</div>
<div class="card">
<h2>Ownership / Claim</h2>
<div class="grid">
<div><label for="claim_prompt">Own this business? prompt</label>
<select id="claim_prompt"><option>NOT_OBSERVED</option><option>Yes</option><option>No</option></select>
</div>
<div><label for="claimed">Claimed status</label>
<select id="claimed">
<option>Unknown</option>
<option>Claimed</option>
<option>Unclaimed</option>
<option>Likely Claimed — inferred</option>
<div>
<label for="review_responses">Review responses observed?</label>
<select id="review_responses">
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
</div>
<div class="full"><label for="claimed_signals">If inferred — signals used</label><input id="claimed_signals" type="text" /></div>
<div><label for="verified">Verified badge</label>
<select id="verified"><option>NOT_OBSERVED</option><option>Yes</option><option>No</option></select>
<div>
<label for="review_responses_detail">If Yes: N of total with replies + most recent age</label>
<input id="review_responses_detail" type="text" placeholder="e.g. 3 of 5, most recent 12d ago" />
</div>
</div>
</div>
<!-- Ownership / Claim Signals -->
<div class="card">
<h2>Ownership / Claim Signals</h2>
<div class="grid">
<div>
<label for="claim_prompt">"Own this business?" prompt visible</label>
<select id="claim_prompt">
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
<option value="No">No</option>
<option value="Yes">Yes</option>
</select>
</div>
<div>
<label for="claimed_status">Claimed status</label>
<select id="claimed_status">
<option value="Unknown">Unknown</option>
<option value="Claimed">Claimed</option>
<option value="Unclaimed">Unclaimed</option>
<option value="Likely Claimed — inferred">Likely Claimed — inferred</option>
</select>
</div>
<div class="full">
<label for="claimed_signals">If inferred — signals used</label>
<input id="claimed_signals" type="text" placeholder="e.g. No claim prompt; review responses observed" />
</div>
<div>
<label for="verified_badge">Verified badge observed?</label>
<select id="verified_badge">
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
</div>
</div>
</div>
<!-- Operations -->
<div class="card">
<h2>Operations</h2>
<div class="grid">
<div><label for="hours_listed">Hours listed</label>
<select id="hours_listed"><option>No</option><option>Yes</option></select>
</div>
<div><label for="open_now">Open now (requires datetime+TZ)</label>
<select id="open_now"><option>NOT_OBSERVED</option><option>Yes</option><option>No</option></select>
</div>
<div class="full"><label for="hours_text">Hours text</label><input id="hours_text" type="text" /></div>
</div>
</div>
<div class="card">
<h2>Conversion (complete whole row if any button observed)</h2>
<div class="grid">
<div><label for="booking_button">Booking button on GBP</label>
<select id="booking_button"><option>NOT_OBSERVED</option><option>Yes</option><option>No</option></select>
</div>
<div><label for="booking_label">Booking label</label><input id="booking_label" type="text" /></div>
<div><label for="messaging_button">Messaging button</label>
<select id="messaging_button"><option>NOT_OBSERVED</option><option>Yes</option><option>No</option></select>
</div>
<div class="full"><label for="other_buttons">Other action buttons</label><input id="other_buttons" type="text" /></div>
</div>
</div>
<div class="card">
<h2>Content</h2>
<div class="grid">
<div><label for="photos_present">Photos present</label>
<select id="photos_present"><option>No</option><option>Yes</option></select>
</div>
<div><label for="photo_count">Approx photo count</label><input id="photo_count" type="text" /></div>
<div class="full"><label for="services">Services listed (exact)</label><input id="services" type="text" /></div>
<div><label for="posts_section">Posts section present</label>
<select id="posts_section"><option>NOT_OBSERVED</option><option>Yes</option><option>No</option></select>
</div>
<div><label for="post_count">Post count (0 if empty)</label><input id="post_count" type="text" placeholder="0" /></div>
<div><label for="recent_post">Most recent post date</label><input id="recent_post" type="text" /></div>
<div><label for="qa_status">Q&A status</label>
<select id="qa_status">
<option>NOT_OBSERVED</option>
<option>NOT_PRESENT</option>
<option>empty</option>
<option>answers present</option>
<div>
<label for="hours_listed">Hours listed</label>
<select id="hours_listed">
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
</div>
<div class="full"><label for="attributes">Attributes / highlights</label><input id="attributes" type="text" /></div>
<div>
<label for="open_now">Open now indicator (valid only with datetime+TZ)</label>
<select id="open_now">
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
</div>
<div class="full">
<label for="hours_text">Hours text</label>
<input id="hours_text" type="text" placeholder="e.g. MonFri 5:30 AM6:30 PM, Sat 810 AM, Sun Closed" />
</div>
</div>
</div>
<!-- Conversion (Action Button Row) -->
<div class="card">
<h2>Related listings</h2>
<h2>Conversion — Complete the whole row if any button observed</h2>
<div class="vocab-hint">Map the full action-button row on the GBP panel. If you see one, capture all.</div>
<div class="grid">
<div><label for="second_listing">Second related listing nearby</label>
<select id="second_listing"><option>Unknown</option><option>Yes</option><option>No</option></select>
<div>
<label for="booking_button">Booking button <strong>on the GBP</strong></label>
<select id="booking_button">
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
<option value="Yes">Yes</option>
<option value="No NOT_PRESENT">No — NOT_PRESENT</option>
</select>
</div>
<div>
<label for="booking_label">Booking button label</label>
<input id="booking_label" type="text" placeholder="e.g. Book appointment" />
</div>
<div>
<label for="messaging_button">Messaging button present</label>
<select id="messaging_button">
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
<option value="Yes">Yes</option>
<option value="No NOT_PRESENT">No — NOT_PRESENT</option>
</select>
</div>
<div>
<label for="call_button">Call button present</label>
<select id="call_button">
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
<option value="Yes">Yes</option>
<option value="No NOT_PRESENT">No — NOT_PRESENT</option>
</select>
</div>
<div>
<label for="directions_button">Directions button present</label>
<select id="directions_button">
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
<option value="Yes">Yes</option>
<option value="No NOT_PRESENT">No — NOT_PRESENT</option>
</select>
</div>
<div class="full">
<label for="other_action_buttons">Other action buttons observed</label>
<input id="other_action_buttons" type="text" placeholder="e.g. Order, Sign up, Get offer" />
</div>
</div>
</div>
<!-- Content Surface -->
<div class="card">
<h2>Content Surface</h2>
<div class="grid">
<div>
<label for="photos_present">Photos present</label>
<select id="photos_present">
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
</div>
<div>
<label for="photo_count">Approx photo count</label>
<input id="photo_count" type="text" placeholder="e.g. 6" />
</div>
<div class="full">
<label for="services_listed">Services listed (exact labels)</label>
<input id="services_listed" type="text" placeholder="e.g. strength training only" />
</div>
<div>
<label for="posts_section_present">Posts / Updates section present</label>
<select id="posts_section_present">
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
<option value="Yes">Yes</option>
<option value="No NOT_PRESENT">No — NOT_PRESENT</option>
</select>
</div>
<div>
<label for="post_count">Post count (0 if empty)</label>
<input id="post_count" type="text" placeholder="0" />
</div>
<div>
<label for="most_recent_post_date">Most recent post date (optional)</label>
<input id="most_recent_post_date" type="text" />
</div>
<div>
<label for="qa_status">Q&amp;A status</label>
<select id="qa_status">
<option value="NOT_OBSERVED">NOT_OBSERVED</option>
<option value="NOT_PRESENT">NOT_PRESENT</option>
<option value="Visible but empty">Visible but empty</option>
<option value="Answers present">Answers present</option>
</select>
</div>
<div class="full">
<label for="attributes">Attributes / highlights visible (optional)</label>
<input id="attributes" type="text" placeholder="e.g. Wheelchair accessible, Women-owned, LGBTQ+ friendly" />
</div>
</div>
</div>
<!-- Related Listings -->
<div class="card">
<h2>Related Listings</h2>
<div class="grid">
<div>
<label for="second_listing">Second related listing nearby</label>
<select id="second_listing">
<option value="Unknown">Unknown</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
</div>
<div>
<label for="second_listing_names">Related listing name(s)</label>
<input id="second_listing_names" type="text" />
</div>
<div class="full">
<label for="second_listing_notes">Related listing notes (factual only)</label>
<textarea id="second_listing_notes" placeholder="What relationship? Different suite? Same building?"></textarea>
</div>
<div class="full">
<label for="raw_notes">Raw notes (optional, factual only)</label>
<textarea id="raw_notes" placeholder="Anything else worth capturing — no analysis, just observation"></textarea>
</div>
<div><label for="second_names">Related listing name(s)</label><input id="second_names" type="text" /></div>
<div class="full"><label for="second_notes">Related notes (factual)</label><textarea id="second_notes"></textarea></div>
<div class="full"><label for="raw_notes">Raw notes</label><textarea id="raw_notes"></textarea></div>
</div>
</div>
</form>
@@ -174,83 +451,226 @@
<div class="actions">
<button type="button" class="btn-primary" id="btnGenerate">Generate for Leonard</button>
<button type="button" class="btn-secondary" id="btnCopy">Copy</button>
<button type="button" class="btn-secondary" id="btnDownload">Download .txt</button>
<button type="button" class="btn-secondary" id="btnClear">Clear</button>
</div>
<div class="status" id="status"></div>
<label for="output">Leonard-ready output</label>
<textarea id="output" readonly></textarea>
<p class="hint">Follows execution card v1.2 (R1R6). PLACE_ID computed from URL by agent if blank.</p>
</div>
<script>
const $ = (id) => document.getElementById(id);
function val(id) {
const v = ($(id).value || '').trim();
return v === '' ? 'NOT_OBSERVED' : v;
}
function buildOutput() {
return `Follow docs/agents/leonard-gbp-execution-card.md exactly (v1.2).
<textarea id="output" readonly placeholder="Click Generate for Leonard…"></textarea>
<p class="hint">Follows execution card v1.2 (R1R6). PLACE_ID and IDs auto-computed from URL. Paste output to Leonard with the instruction to follow the execution card exactly.</p>
</div>
<script>
const $ = (id) => document.getElementById(id);
function val(id) {
const el = $(id);
const v = (el.value || "").trim();
if (v === "") return "NOT_OBSERVED";
// Normalize "No NOT_PRESENT" values
if (v === "No NOT_PRESENT") return "NOT_PRESENT";
return v;
}
function nowISO() {
const d = new Date();
const tz = -d.getTimezoneOffset();
const sign = tz >= 0 ? "+" : "-";
const pad = (n) => String(Math.abs(n)).padStart(2, "0");
const hh = pad(Math.floor(Math.abs(tz) / 60));
const mm = pad(Math.abs(tz) % 60);
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}T${String(d.getHours()).padStart(2,"0")}:${String(d.getMinutes()).padStart(2,"0")}:00${sign}${hh}:${mm}`;
}
// Pre-fill datetime
$("snapshot_dt").value = nowISO();
// Auto-compute PLACE_ID from MAPS_URL on input
function computePlaceId() {
const url = $("maps_url").value.trim();
const placeIdField = $("place_id");
const computedField = $("computed_ids");
if (!url) {
placeIdField.placeholder = "Leave blank — auto-computed from URL";
computedField.value = "";
return;
}
const parts = [];
// Extract place ID (/g/...)
const placeMatch = url.match(/(?:place\/|\/)g\/[a-zA-Z0-9_\-]+/);
if (placeMatch) parts.push("PLACE_ID: " + placeMatch[0].replace(/(place\/|\/)/, ""));
// Extract feature IDs (0x...:0x...)
const featureMatch = url.match(/0x[a-fA-F0-9]+:0x[a-fA-F0-9]+/g);
if (featureMatch) {
parts.push("feature: " + featureMatch[0]);
if (featureMatch[1]) parts.push("!5s lead: " + featureMatch[1]);
// Fill place_id field with the first pair
placeIdField.value = featureMatch[0];
}
// Extract CID
const cidMatch = url.match(/[?&]cid=(\d+)/) || url.match(/0x[a-fA-F0-9]+:0x([a-fA-F0-9]+)/);
if (cidMatch && cidMatch[1]) {
const cid = cidMatch[1].startsWith("0x") ? String(parseInt(cidMatch[1], 16)) : cidMatch[1];
parts.push("CID: " + cid);
}
computedField.value = parts.join(" · ") || "";
if (!placeIdField.value && parts.length > 0) placeIdField.value = featureMatch ? featureMatch[0] : "";
}
$("maps_url").addEventListener("input", computePlaceId);
function buildOutput() {
// Auto-compute PLACE_ID before generating
computePlaceId();
const instructions = `Follow docs/agents/leonard-gbp-execution-card.md exactly (v1.2).
Map intake into client Data Inventory GBP row.
Apply only threat trigger rules R1R6.
Do not invent fields. Do not add solutions. Do not reason beyond the execution card.
Return: updated GBP section, any threat changes, Changes Made list (with object IDs), Rules Fired list (rule ID, version, inputs, values, output).
Return: updated GBP section, any threat changes, Changes Made list (with object IDs), Rules Fired list (rule ID, version, inputs, values, output).`;
--- GBP SNAPSHOT INTAKE ---
CLIENT: ${val('client')}
SNAPSHOT_DATETIME: ${val('snapshot_dt')}
SOURCE: ${val('source')}
OBSERVATION_SURFACE: ${val('surface')}
MAPS_URL: ${val('maps_url')}
PLACE_ID: ${($('place_id').value || '').trim() || 'COMPUTE_FROM_URL'}
const metadata = [
`CLIENT: ${val("client")}`,
`SNAPSHOT_DATETIME: ${val("snapshot_dt")}`,
`SOURCE: ${val("source")}`,
`OBSERVATION_SURFACE: ${val("surface")}`,
`MAPS_URL: ${val("maps_url")}`,
`PLACE_ID: ${val("place_id")}`
].join("\n");
BUSINESS_NAME: ${val('business_name')}
ADDRESS: ${val('address')}
ADDRESS_CANONICAL: ${val('address_canonical')}
PHONE: ${val('phone')}
WEBSITE: ${val('website')}
PRIMARY_CATEGORY: ${val('primary_category')}
ADDITIONAL_CATEGORIES: ${val('additional_categories')}
const core = [
`BUSINESS_NAME: ${val("business_name")}`,
`ADDRESS: ${val("address")}`,
`ADDRESS_CANONICAL: ${val("address_canonical")}`,
`PHONE: ${val("phone")}`,
`WEBSITE: ${val("website")}`,
`PRIMARY_CATEGORY: ${val("primary_category")}`,
`ADDITIONAL_CATEGORIES: ${val("additional_categories")}`
].join("\n");
STAR_RATING: ${val('star_rating')}
REVIEW_COUNT: ${val('review_count')}
REVIEW_RESPONSES_OBSERVED: ${val('review_responses')}
REVIEW_RESPONSES_DETAIL: ${val('review_responses_detail')}
const reputation = [
`STAR_RATING: ${val("star_rating")}`,
`REVIEW_COUNT: ${val("review_count")}`,
`REVIEW_RESPONSES_OBSERVED: ${val("review_responses")}`,
`REVIEW_RESPONSES_DETAIL: ${val("review_responses_detail")}`
].join("\n");
CLAIM_PROMPT_VISIBLE: ${val('claim_prompt')}
CLAIMED_STATUS: ${val('claimed')}
CLAIMED_SIGNALS: ${val('claimed_signals')}
VERIFIED_BADGE_VISIBLE: ${val('verified')}
const claim = [
`CLAIM_PROMPT_VISIBLE: ${val("claim_prompt")}`,
`CLAIMED_STATUS: ${val("claimed_status")}`,
`CLAIMED_SIGNALS: ${val("claimed_signals")}`,
`VERIFIED_BADGE_VISIBLE: ${val("verified_badge")}`
].join("\n");
HOURS_LISTED: ${val('hours_listed')}
HOURS_TEXT: ${val('hours_text')}
OPEN_NOW_OBSERVED: ${val('open_now')}
const operations = [
`HOURS_LISTED: ${val("hours_listed")}`,
`HOURS_TEXT: ${val("hours_text")}`,
`OPEN_NOW_OBSERVED: ${val("open_now")}`
].join("\n");
BOOKING_BUTTON_PRESENT_ON_GBP: ${val('booking_button')}
BOOKING_BUTTON_LABEL: ${val('booking_label')}
MESSAGING_BUTTON_PRESENT: ${val('messaging_button')}
OTHER_ACTION_BUTTONS: ${val('other_buttons')}
const conversion = [
`BOOKING_BUTTON_PRESENT_ON_GBP: ${val("booking_button")}`,
`BOOKING_BUTTON_LABEL: ${val("booking_label")}`,
`MESSAGING_BUTTON_PRESENT: ${val("messaging_button")}`,
`CALL_BUTTON_PRESENT: ${val("call_button")}`,
`DIRECTIONS_BUTTON_PRESENT: ${val("directions_button")}`,
`OTHER_ACTION_BUTTONS: ${val("other_action_buttons")}`
].join("\n");
PHOTOS_PRESENT: ${val('photos_present')}
PHOTO_COUNT_APPROX: ${val('photo_count')}
SERVICES_LISTED: ${val('services')}
POSTS_SECTION_PRESENT: ${val('posts_section')}
POST_COUNT: ${val('post_count')}
MOST_RECENT_POST_DATE: ${val('recent_post')}
QA_STATUS: ${val('qa_status')}
ATTRIBUTES_HIGHLIGHTS: ${val('attributes')}
const content = [
`PHOTOS_PRESENT: ${val("photos_present")}`,
`PHOTO_COUNT_APPROX: ${val("photo_count")}`,
`SERVICES_LISTED: ${val("services_listed")}`,
`POSTS_SECTION_PRESENT: ${val("posts_section_present")}`,
`POST_COUNT: ${val("post_count")}`,
`MOST_RECENT_POST_DATE: ${val("most_recent_post_date")}`,
`QA_STATUS: ${val("qa_status")}`,
`ATTRIBUTES_HIGHLIGHTS: ${val("attributes")}`
].join("\n");
SECOND_LISTING_NEARBY: ${val('second_listing')}
SECOND_LISTING_NAMES: ${val('second_names')}
SECOND_LISTING_NOTES: ${val('second_notes')}
RAW_NOTES: ${val('raw_notes')}
const related = [
`SECOND_LISTING_NEARBY: ${val("second_listing")}`,
`SECOND_LISTING_NAMES: ${val("second_listing_names")}`,
`SECOND_LISTING_NOTES: ${val("second_listing_notes")}`,
`RAW_NOTES: ${val("raw_notes")}`
].join("\n");
return `${instructions}
--- GBP SNAPSHOT INTAKE v1.3 ---
${metadata}
${core}
${reputation}
${claim}
${operations}
${conversion}
${content}
${related}
`;
}
$('btnGenerate').onclick = () => { $('output').value = buildOutput(); $('status').textContent = 'Generated.'; };
$('btnCopy').onclick = async () => {
const t = $('output').value || buildOutput();
$('output').value = t;
try { await navigator.clipboard.writeText(t); $('status').textContent = 'Copied.'; } catch { $('status').textContent = 'Select-all + copy.'; }
};
$('btnClear').onclick = () => { $('gbpForm').reset(); $('output').value = ''; $('status').textContent = 'Cleared.'; };
</script>
}
function setStatus(msg) {
$("status").textContent = msg;
if (msg) setTimeout(() => {
if ($("status").textContent === msg) $("status").textContent = "";
}, 3000);
}
$("btnGenerate").addEventListener("click", () => {
$("output").value = buildOutput();
setStatus("Generated.");
});
$("btnCopy").addEventListener("click", async () => {
const text = $("output").value || buildOutput();
$("output").value = text;
try {
await navigator.clipboard.writeText(text);
setStatus("Copied.");
} catch (e) {
$("output").focus();
$("output").select();
setStatus("Select-all ready — Ctrl/Cmd+C.");
}
});
$("btnDownload").addEventListener("click", () => {
const text = $("output").value || buildOutput();
$("output").value = text;
const client = ($("client").value || "client").trim().replace(/[^\w\-]+/g, "_") || "client";
const date = ($("snapshot_dt").value || "").split("T")[0] || "nodate";
const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = `gbp-intake-${client}-${date}.txt`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(a.href);
setStatus("Downloaded.");
});
$("btnClear").addEventListener("click", () => {
$("gbpForm").reset();
$("snapshot_dt").value = nowISO();
$("computed_ids").value = "";
$("output").value = "";
setStatus("Cleared.");
});
</script>
</body>
</html>