Files
veripath/tools/gbp-snapshot-form.html
T

676 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>DOP — GBP Snapshot Intake v1.3</title>
<style>
: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: 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.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" 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 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>
<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</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 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 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" placeholder="e.g. 4.8" />
</div>
<div>
<label for="review_count">Review count</label>
<input id="review_count" type="text" placeholder="e.g. 42" />
</div>
<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>
<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 value="NOT_OBSERVED">NOT_OBSERVED</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
</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>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="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>
</div>
</form>
<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 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).`;
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");
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");
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");
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");
const operations = [
`HOURS_LISTED: ${val("hours_listed")}`,
`HOURS_TEXT: ${val("hours_text")}`,
`OPEN_NOW_OBSERVED: ${val("open_now")}`
].join("\n");
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");
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");
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}
`;
}
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>