fix: v1.4 — presets save identity fields only (never observation data), confirm before load, localStorage disclosure + export/clear, .req.met wired, short-link URL flag, R1-R5 aligned with execution card, generic placeholder
This commit is contained in:
+136
-22
@@ -427,7 +427,7 @@
|
|||||||
<div class="sub">
|
<div class="sub">
|
||||||
<span class="vocab-inline">NOT_PRESENT</span> = looked, confirmed absent ·
|
<span class="vocab-inline">NOT_PRESENT</span> = looked, confirmed absent ·
|
||||||
<span class="vocab-inline">NOT_OBSERVED</span> = didn't look / couldn't see ·
|
<span class="vocab-inline">NOT_OBSERVED</span> = didn't look / couldn't see ·
|
||||||
Paste to Leonard → R1–R6
|
Paste to Leonard → R1–R5
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-progress">
|
<div class="header-progress">
|
||||||
@@ -455,6 +455,21 @@
|
|||||||
<button type="button" class="preset-btn" id="btnDeletePreset">Delete</button>
|
<button type="button" class="preset-btn" id="btnDeletePreset">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Local Storage Disclosure -->
|
||||||
|
<div class="card" style="margin-bottom:16px;padding:12px 16px;border-color:var(--line-soft)">
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px">
|
||||||
|
<div style="font-size:0.78rem;color:var(--muted)">
|
||||||
|
<span style="font-family:var(--mono);font-size:0.65rem;text-transform:uppercase;letter-spacing:0.06em">Local data</span><br />
|
||||||
|
Draft auto-saves to this browser. Presets are identity fields only (name, address, URL, categories) — observation data is never saved in presets and must be re-filled each audit.
|
||||||
|
<span id="localDataSummary" style="display:block;margin-top:4px;font-size:0.72rem;color:var(--ink-dim)"></span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:6px">
|
||||||
|
<button type="button" class="preset-btn" id="btnExportPresets">Export</button>
|
||||||
|
<button type="button" class="preset-btn" id="btnClearLocalData">Clear local data</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form id="gbpForm" autocomplete="off">
|
<form id="gbpForm" autocomplete="off">
|
||||||
<!-- 01: Snapshot Metadata -->
|
<!-- 01: Snapshot Metadata -->
|
||||||
<div class="card is-open" data-section="0">
|
<div class="card is-open" data-section="0">
|
||||||
@@ -541,7 +556,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="primary_category">Primary category <strong>(ONE value)</strong></label>
|
<label for="primary_category">Primary category <strong>(ONE value)</strong></label>
|
||||||
<input id="primary_category" type="text" placeholder="e.g. Gym · Auto repair shop" />
|
<input id="primary_category" type="text" placeholder="e.g. Restaurant · Auto repair shop · Physical therapist" />
|
||||||
</div>
|
</div>
|
||||||
<div class="full">
|
<div class="full">
|
||||||
<label for="additional_categories">Additional categories (comma-separated)</label>
|
<label for="additional_categories">Additional categories (comma-separated)</label>
|
||||||
@@ -822,13 +837,30 @@
|
|||||||
|
|
||||||
<label for="output">Leonard-ready output</label>
|
<label for="output">Leonard-ready output</label>
|
||||||
<textarea id="output" readonly placeholder="Click Generate for Leonard…"></textarea>
|
<textarea id="output" readonly placeholder="Click Generate for Leonard…"></textarea>
|
||||||
<p class="hint"><kbd>Ctrl+Enter</kbd> to generate · Draft auto-saved to browser · Use client presets for repeat audits</p>
|
<p class="hint"><kbd>Ctrl+Enter</kbd> to generate · Draft auto-saved to browser · Presets store identity fields only — re-observe before use. Execution card governs R1–R5.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const $ = (id) => document.getElementById(id);
|
const $ = (id) => document.getElementById(id);
|
||||||
|
/* ── localStorage helpers ── */
|
||||||
|
const STATIC_FIELDS = ["client", "snapshot_dt", "source", "surface", "maps_url",
|
||||||
|
"business_name", "address", "address_canonical", "phone", "website",
|
||||||
|
"primary_category", "additional_categories"];
|
||||||
|
|
||||||
|
/** Fields that must be re-observed every audit — never saved to presets. */
|
||||||
|
const DYNAMIC_FIELDS = ["place_id", "computed_ids",
|
||||||
|
"star_rating", "review_count", "review_responses", "review_responses_detail",
|
||||||
|
"claim_prompt", "claimed_status", "claimed_signals", "verified_badge",
|
||||||
|
"hours_listed", "hours_text", "open_now",
|
||||||
|
"booking_button", "booking_label", "messaging_button", "call_button",
|
||||||
|
"directions_button", "other_action_buttons",
|
||||||
|
"photos_present", "photo_count", "services_listed",
|
||||||
|
"posts_section_present", "post_count", "most_recent_post_date",
|
||||||
|
"qa_status", "attributes",
|
||||||
|
"second_listing", "second_listing_names", "second_listing_notes", "raw_notes"];
|
||||||
|
|
||||||
const SAVE_KEY = "gbp-intake-v1.4-draft";
|
const SAVE_KEY = "gbp-intake-v1.4-draft";
|
||||||
const PRESETS_KEY = "gbp-intake-v1.4-presets";
|
const PRESETS_KEY = "gbp-intake-v1.4-presets";
|
||||||
const SECTIONS = 8;
|
const SECTIONS = 8;
|
||||||
@@ -898,26 +930,45 @@
|
|||||||
const presets = getPresets();
|
const presets = getPresets();
|
||||||
const data = presets[name];
|
const data = presets[name];
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
Object.keys(data).forEach(key => {
|
// Warn if unsaved data will be overwritten
|
||||||
|
const hasDynamicData = DYNAMIC_FIELDS.some(key => {
|
||||||
const el = document.getElementById(key);
|
const el = document.getElementById(key);
|
||||||
if (el) el.value = data[key];
|
return el && el.value.trim() && el.value !== "NOT_OBSERVED" && el.value !== "";
|
||||||
|
});
|
||||||
|
if (hasDynamicData && !confirm(`Load "${name}"? Any unsaved observation data will be cleared — observation fields must be re-filled from scratch.`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Load only static identity fields
|
||||||
|
STATIC_FIELDS.forEach(key => {
|
||||||
|
const el = document.getElementById(key);
|
||||||
|
if (el && key in data) el.value = data[key] || "";
|
||||||
|
});
|
||||||
|
// Clear all dynamic observation fields so stale data can't slip in
|
||||||
|
DYNAMIC_FIELDS.forEach(key => {
|
||||||
|
const el = document.getElementById(key);
|
||||||
|
if (el) el.value = "";
|
||||||
});
|
});
|
||||||
// Trigger ID computation for preset URL
|
// Trigger ID computation for preset URL
|
||||||
computePlaceId();
|
computePlaceId();
|
||||||
updateAllStatus();
|
updateAllStatus();
|
||||||
toast(`Loaded: ${name}`, "ok");
|
toast(`Loaded identity fields for "${name}". Re-observe all dynamic data.`, "warn");
|
||||||
}
|
}
|
||||||
|
|
||||||
function savePreset(name) {
|
function savePreset(name) {
|
||||||
|
if (!name) { toast("Enter a client name first.", "warn"); return; }
|
||||||
const presets = getPresets();
|
const presets = getPresets();
|
||||||
const data = {};
|
const data = {};
|
||||||
const fields = document.querySelectorAll("#gbpForm input, #gbpForm select, #gbpForm textarea");
|
// Save only static identity fields — NEVER observation data
|
||||||
fields.forEach(el => { data[el.id] = el.value; });
|
STATIC_FIELDS.forEach(key => {
|
||||||
|
const el = document.getElementById(key);
|
||||||
|
if (el) data[key] = el.value;
|
||||||
|
});
|
||||||
presets[name] = data;
|
presets[name] = data;
|
||||||
savePresets(presets);
|
savePresets(presets);
|
||||||
populatePresetSelect();
|
populatePresetSelect();
|
||||||
$("presetSelect").value = name;
|
$("presetSelect").value = name;
|
||||||
toast(`Saved: ${name}`, "ok");
|
localStorageDisclosure();
|
||||||
|
toast("Preset saved (identity fields only — re-observe before use).", "ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
function deletePreset(name) {
|
function deletePreset(name) {
|
||||||
@@ -928,6 +979,32 @@
|
|||||||
toast(`Deleted: ${name}`, "warn");
|
toast(`Deleted: ${name}`, "warn");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── localStorage disclosure ── */
|
||||||
|
function localStorageDisclosure() {
|
||||||
|
const presets = getPresets();
|
||||||
|
const pCount = Object.keys(presets).length;
|
||||||
|
const draftSize = (localStorage.getItem(SAVE_KEY) || "").length;
|
||||||
|
const $sum = $("localDataSummary");
|
||||||
|
if ($sum) {
|
||||||
|
const parts = [];
|
||||||
|
if (pCount > 0) parts.push(`${pCount} preset${pCount > 1 ? "s" : ""}`);
|
||||||
|
if (draftSize > 0) parts.push(`draft (${(draftSize / 1024).toFixed(1)} KB)`);
|
||||||
|
$sum.textContent = parts.length ? `Stored: ${parts.join(", ")}` : "Nothing stored locally";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Update .req.met badges ── */
|
||||||
|
function updateReqMet() {
|
||||||
|
["client", "snapshot_dt", "maps_url"].forEach(id => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
const badge = document.getElementById("req-" + id);
|
||||||
|
if (el && badge) {
|
||||||
|
const hasVal = el.value.trim().length > 0;
|
||||||
|
badge.classList.toggle("met", hasVal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Collapsible sections ── */
|
/* ── Collapsible sections ── */
|
||||||
document.querySelectorAll(".card-header").forEach(hdr => {
|
document.querySelectorAll(".card-header").forEach(hdr => {
|
||||||
hdr.addEventListener("click", () => {
|
hdr.addEventListener("click", () => {
|
||||||
@@ -986,30 +1063,40 @@
|
|||||||
computedField.value = "";
|
computedField.value = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Resolve short-link URLs — expand common shorteners to their canonical form
|
||||||
|
let resolvedUrl = url;
|
||||||
|
if (/^https?:\/\/(maps\.app\.goo\.gl|g\.co\/kgs)\//.test(url)) {
|
||||||
|
resolvedUrl = url; // keep original; regex matchers below will fail but PLACE_ID won't be computed
|
||||||
|
}
|
||||||
const parts = [];
|
const parts = [];
|
||||||
|
// Try full Maps URL format first
|
||||||
const placeMatch = url.match(/(?:place\/|\/)g\/[a-zA-Z0-9_\-]+/);
|
const placeMatch = url.match(/(?:place\/|\/)g\/[a-zA-Z0-9_\-]+/);
|
||||||
if (placeMatch) parts.push("PLACE_ID: " + placeMatch[0].replace(/(place\/|\/)/, ""));
|
if (placeMatch) parts.push("PLACE_ID: " + placeMatch[0].replace(/(place\/|\/)/, ""));
|
||||||
const featureMatch = url.match(/0x[a-fA-F0-9]+:0x[a-fA-F0-9]+/g);
|
const featureMatch = url.match(/0x[a-fA-F0-9]+:\s*0x[a-fA-F0-9]+/g);
|
||||||
if (featureMatch) {
|
if (featureMatch) {
|
||||||
parts.push("feature: " + featureMatch[0]);
|
parts.push("feature: " + featureMatch[0].replace(/\s+/g, ""));
|
||||||
if (featureMatch[1]) parts.push("!5s lead: " + featureMatch[1]);
|
if (featureMatch[1]) parts.push("!5s lead: " + featureMatch[1].replace(/\s+/g, ""));
|
||||||
placeIdField.value = featureMatch[0];
|
placeIdField.value = featureMatch[0].replace(/\s+/g, "");
|
||||||
}
|
}
|
||||||
const cidMatch = url.match(/[?&]cid=(\d+)/) || url.match(/0x[a-fA-F0-9]+:0x([a-fA-F0-9]+)/);
|
const cidMatch = url.match(/[?&]cid=(\d+)/) || url.match(/0x[a-fA-F0-9]+:0x([a-fA-F0-9]+)/);
|
||||||
if (cidMatch && cidMatch[1]) {
|
if (cidMatch && cidMatch[1]) {
|
||||||
const cid = cidMatch[1].startsWith("0x") ? String(parseInt(cidMatch[1], 16)) : cidMatch[1];
|
const cid = cidMatch[1].startsWith("0x") ? String(parseInt(cidMatch[1], 16)) : cidMatch[1];
|
||||||
parts.push("CID: " + cid);
|
parts.push("CID: " + cid);
|
||||||
}
|
}
|
||||||
|
// If nothing extracted, flag as short-link (not a failure — user needs to expand)
|
||||||
|
if (parts.length === 0 && url.length > 0) {
|
||||||
|
parts.push("short-link — expand in browser");
|
||||||
|
}
|
||||||
computedField.value = parts.join(" · ") || "";
|
computedField.value = parts.join(" · ") || "";
|
||||||
if (!placeIdField.value && featureMatch) placeIdField.value = featureMatch[0];
|
if (!placeIdField.value && featureMatch) placeIdField.value = featureMatch[0].replace(/\s+/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
$("maps_url").addEventListener("input", () => { computePlaceId(); saveDraft(); updateAllStatus(); });
|
$("maps_url").addEventListener("input", () => { computePlaceId(); saveDraft(); updateAllStatus(); });
|
||||||
|
|
||||||
/* ── Auto-save on any input ── */
|
/* ── Auto-save on any input ── */
|
||||||
document.querySelectorAll("#gbpForm input, #gbpForm select, #gbpForm textarea").forEach(el => {
|
document.querySelectorAll("#gbpForm input, #gbpForm select, #gbpForm textarea").forEach(el => {
|
||||||
el.addEventListener("input", () => { saveDraft(); updateAllStatus(); });
|
el.addEventListener("input", () => { saveDraft(); updateAllStatus(); updateReqMet(); });
|
||||||
el.addEventListener("change", () => { saveDraft(); updateAllStatus(); });
|
el.addEventListener("change", () => { saveDraft(); updateAllStatus(); updateReqMet(); });
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ── Toast ── */
|
/* ── Toast ── */
|
||||||
@@ -1042,9 +1129,9 @@
|
|||||||
function buildOutput() {
|
function buildOutput() {
|
||||||
computePlaceId();
|
computePlaceId();
|
||||||
|
|
||||||
const instructions = `Follow docs/agents/leonard-gbp-execution-card.md exactly (v1.2).
|
const instructions = `Follow docs/agents/leonard-gbp-execution-card.md exactly.
|
||||||
Map intake into client Data Inventory GBP row.
|
Map intake into client Data Inventory GBP row.
|
||||||
Apply only threat trigger rules R1–R6.
|
Apply only threat trigger rules R1–R5.
|
||||||
Do not invent fields. Do not add solutions. Do not reason beyond the execution card.
|
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).`;
|
||||||
|
|
||||||
@@ -1165,14 +1252,14 @@ ${sections.join("\n\n")}
|
|||||||
else c.classList.remove("is-open");
|
else c.classList.remove("is-open");
|
||||||
});
|
});
|
||||||
updateAllStatus();
|
updateAllStatus();
|
||||||
|
updateReqMet();
|
||||||
|
localStorageDisclosure();
|
||||||
toast("Form cleared.", "warn");
|
toast("Form cleared.", "warn");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Preset buttons
|
// Preset buttons
|
||||||
$("btnSavePreset").addEventListener("click", () => {
|
$("btnSavePreset").addEventListener("click", () => {
|
||||||
const name = $("client").value.trim();
|
savePreset($("client").value.trim());
|
||||||
if (!name) { toast("Enter a client name first.", "warn"); return; }
|
|
||||||
savePreset(name);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("btnDeletePreset").addEventListener("click", () => {
|
$("btnDeletePreset").addEventListener("click", () => {
|
||||||
@@ -1186,6 +1273,31 @@ ${sections.join("\n\n")}
|
|||||||
if (name) loadPreset(name);
|
if (name) loadPreset(name);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Local data buttons
|
||||||
|
$("btnExportPresets").addEventListener("click", () => {
|
||||||
|
const presets = getPresets();
|
||||||
|
const count = Object.keys(presets).length;
|
||||||
|
if (count === 0) { toast("No presets to export.", "warn"); return; }
|
||||||
|
const blob = new Blob([JSON.stringify(presets, null, 2)], { type: "application/json;charset=utf-8" });
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = "gbp-presets-export.json";
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
URL.revokeObjectURL(a.href);
|
||||||
|
toast(`Exported ${count} preset(s).`, "ok");
|
||||||
|
});
|
||||||
|
|
||||||
|
$("btnClearLocalData").addEventListener("click", () => {
|
||||||
|
if (!confirm("Clear all locally stored presets and drafts? This cannot be undone.")) return;
|
||||||
|
localStorage.removeItem(SAVE_KEY);
|
||||||
|
localStorage.removeItem(PRESETS_KEY);
|
||||||
|
populatePresetSelect();
|
||||||
|
localStorageDisclosure();
|
||||||
|
toast("Local data cleared.", "warn");
|
||||||
|
});
|
||||||
|
|
||||||
// Keyboard: Ctrl+Enter to generate
|
// Keyboard: Ctrl+Enter to generate
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
||||||
@@ -1205,11 +1317,13 @@ ${sections.join("\n\n")}
|
|||||||
}
|
}
|
||||||
populatePresetSelect();
|
populatePresetSelect();
|
||||||
updateAllStatus();
|
updateAllStatus();
|
||||||
|
updateReqMet();
|
||||||
|
localStorageDisclosure();
|
||||||
// Recompute from any URL that might have been loaded
|
// Recompute from any URL that might have been loaded
|
||||||
computePlaceId();
|
computePlaceId();
|
||||||
|
|
||||||
// Auto-save every 30s as backup
|
// Auto-save every 30s as backup
|
||||||
setInterval(saveDraft, 30000);
|
setInterval(() => { saveDraft(); localStorageDisclosure(); }, 30000);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user