Add reusable GBP Snapshot HTML form for cold audits (Leonard paste/download)
This commit is contained in:
@@ -0,0 +1,487 @@
|
|||||||
|
<!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</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: 860px;
|
||||||
|
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="date"],
|
||||||
|
input[type="url"],
|
||||||
|
input[type="number"],
|
||||||
|
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: 320px;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>DOP — GBP Snapshot Intake</h1>
|
||||||
|
<p class="sub">Fill once per cold audit. Generate text → copy/paste to Leonard. Uses execution card rules only.</p>
|
||||||
|
|
||||||
|
<form id="gbpForm" autocomplete="off">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Snapshot Metadata</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<div>
|
||||||
|
<label for="client">Client name</label>
|
||||||
|
<input id="client" type="text" placeholder="Overcome Fitness" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="snapshot_date">Snapshot date</label>
|
||||||
|
<input id="snapshot_date" type="date" />
|
||||||
|
</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="place_id">Place ID (if known)</label>
|
||||||
|
<input id="place_id" type="text" placeholder="Optional" />
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Core Fields</h2>
|
||||||
|
<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</label>
|
||||||
|
<input id="address" type="text" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="phone">Phone shown</label>
|
||||||
|
<input id="phone" type="text" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="website">Website shown</label>
|
||||||
|
<input id="website" type="text" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="primary_category">Primary category</label>
|
||||||
|
<input id="primary_category" type="text" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="additional_categories">Additional categories</label>
|
||||||
|
<input id="additional_categories" type="text" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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 response behavior observed</label>
|
||||||
|
<select id="review_responses">
|
||||||
|
<option value="Unknown">Unknown</option>
|
||||||
|
<option value="Yes">Yes</option>
|
||||||
|
<option value="No">No</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Ownership / Claim Signals</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<div>
|
||||||
|
<label for="claimed">Claimed or “Own this business” visible</label>
|
||||||
|
<select id="claimed">
|
||||||
|
<option value="Unknown">Unknown</option>
|
||||||
|
<option value="Yes">Yes</option>
|
||||||
|
<option value="No">No</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="verified">Verified badge or equivalent observed</label>
|
||||||
|
<select id="verified">
|
||||||
|
<option value="Unknown">Unknown</option>
|
||||||
|
<option value="Yes">Yes</option>
|
||||||
|
<option value="No">No</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Operations</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<div>
|
||||||
|
<label for="hours_listed">Hours listed</label>
|
||||||
|
<select id="hours_listed">
|
||||||
|
<option value="No">No</option>
|
||||||
|
<option value="Yes">Yes</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="open_now">Open now indicator observed</label>
|
||||||
|
<select id="open_now">
|
||||||
|
<option value="Unknown">Unknown</option>
|
||||||
|
<option value="Yes">Yes</option>
|
||||||
|
<option value="No">No</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="full">
|
||||||
|
<label for="hours_text">Hours text (paste if short)</label>
|
||||||
|
<input id="hours_text" type="text" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Conversion</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<div>
|
||||||
|
<label for="booking_button">Booking / appointment button present</label>
|
||||||
|
<select id="booking_button">
|
||||||
|
<option value="Unknown">Unknown</option>
|
||||||
|
<option value="Yes">Yes</option>
|
||||||
|
<option value="No">No</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="booking_label">Booking button label (if any)</label>
|
||||||
|
<input id="booking_label" type="text" />
|
||||||
|
</div>
|
||||||
|
<div class="full">
|
||||||
|
<label for="other_buttons">Other action buttons observed</label>
|
||||||
|
<input id="other_buttons" type="text" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Content Surface</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<div>
|
||||||
|
<label for="photos_present">Photos present</label>
|
||||||
|
<select id="photos_present">
|
||||||
|
<option value="No">No</option>
|
||||||
|
<option value="Yes">Yes</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="photo_count">Approx photo count</label>
|
||||||
|
<input id="photo_count" type="text" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="posts_visible">Posts visible</label>
|
||||||
|
<select id="posts_visible">
|
||||||
|
<option value="Unknown">Unknown</option>
|
||||||
|
<option value="Yes">Yes</option>
|
||||||
|
<option value="No">No</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="qa_visible">Q&A visible</label>
|
||||||
|
<select id="qa_visible">
|
||||||
|
<option value="Unknown">Unknown</option>
|
||||||
|
<option value="Yes">Yes</option>
|
||||||
|
<option value="No">No</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="full">
|
||||||
|
<label for="raw_notes">Raw notes (optional, factual only)</label>
|
||||||
|
<textarea id="raw_notes"></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 to clipboard</button>
|
||||||
|
<button type="button" class="btn-secondary" id="btnDownload">Download .txt</button>
|
||||||
|
<button type="button" class="btn-secondary" id="btnClear">Clear form</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">Paste the output block directly to Leonard. He should follow the execution card and only fire threat rules R1–R3.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
|
||||||
|
function val(id) {
|
||||||
|
const el = $(id);
|
||||||
|
const v = (el.value || "").trim();
|
||||||
|
return v === "" ? "Not observed" : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
function todayISO() {
|
||||||
|
const d = new Date();
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(d.getDate()).padStart(2, "0");
|
||||||
|
return `${d.getFullYear()}-${m}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
$("snapshot_date").value = todayISO();
|
||||||
|
|
||||||
|
function buildOutput() {
|
||||||
|
return `Follow docs/agents/leonard-gbp-execution-card.md exactly.
|
||||||
|
Map this intake into the client Data Inventory GMB row.
|
||||||
|
Apply only threat trigger rules R1–R3.
|
||||||
|
Do not invent fields. Do not add solutions. Do not reason beyond the execution card.
|
||||||
|
Return: updated GMB section, any threat changes, Changes Made list, Rules Fired list.
|
||||||
|
|
||||||
|
--- GBP SNAPSHOT INTAKE ---
|
||||||
|
|
||||||
|
CLIENT: ${val("client")}
|
||||||
|
SNAPSHOT_DATE: ${val("snapshot_date")}
|
||||||
|
SOURCE: ${val("source")}
|
||||||
|
MAPS_URL: ${val("maps_url")}
|
||||||
|
PLACE_ID: ${val("place_id")}
|
||||||
|
|
||||||
|
BUSINESS_NAME: ${val("business_name")}
|
||||||
|
ADDRESS: ${val("address")}
|
||||||
|
PHONE: ${val("phone")}
|
||||||
|
WEBSITE: ${val("website")}
|
||||||
|
PRIMARY_CATEGORY: ${val("primary_category")}
|
||||||
|
ADDITIONAL_CATEGORIES: ${val("additional_categories")}
|
||||||
|
|
||||||
|
STAR_RATING: ${val("star_rating")}
|
||||||
|
REVIEW_COUNT: ${val("review_count")}
|
||||||
|
REVIEW_RESPONSES_OBSERVED: ${val("review_responses")}
|
||||||
|
|
||||||
|
CLAIMED_VISIBLE: ${val("claimed")}
|
||||||
|
VERIFIED_BADGE_VISIBLE: ${val("verified")}
|
||||||
|
|
||||||
|
HOURS_LISTED: ${val("hours_listed")}
|
||||||
|
HOURS_TEXT: ${val("hours_text")}
|
||||||
|
OPEN_NOW_OBSERVED: ${val("open_now")}
|
||||||
|
|
||||||
|
BOOKING_BUTTON_PRESENT: ${val("booking_button")}
|
||||||
|
BOOKING_BUTTON_LABEL: ${val("booking_label")}
|
||||||
|
OTHER_ACTION_BUTTONS: ${val("other_buttons")}
|
||||||
|
|
||||||
|
PHOTOS_PRESENT: ${val("photos_present")}
|
||||||
|
PHOTO_COUNT_APPROX: ${val("photo_count")}
|
||||||
|
POSTS_VISIBLE: ${val("posts_visible")}
|
||||||
|
QA_VISIBLE: ${val("qa_visible")}
|
||||||
|
|
||||||
|
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")}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(msg) {
|
||||||
|
$("status").textContent = msg;
|
||||||
|
if (msg) setTimeout(() => { if ($("status").textContent === msg) $("status").textContent = ""; }, 2500);
|
||||||
|
}
|
||||||
|
|
||||||
|
$("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 to clipboard.");
|
||||||
|
} catch (e) {
|
||||||
|
$("output").focus();
|
||||||
|
$("output").select();
|
||||||
|
setStatus("Select-all ready — press 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_date").value || todayISO();
|
||||||
|
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("Download started.");
|
||||||
|
});
|
||||||
|
|
||||||
|
$("btnClear").addEventListener("click", () => {
|
||||||
|
$("gbpForm").reset();
|
||||||
|
$("snapshot_date").value = todayISO();
|
||||||
|
$("output").value = "";
|
||||||
|
setStatus("Form cleared.");
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user