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">
|
||||
<span class="vocab-inline">NOT_PRESENT</span> = looked, confirmed absent ·
|
||||
<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 class="header-progress">
|
||||
@@ -455,6 +455,21 @@
|
||||
<button type="button" class="preset-btn" id="btnDeletePreset">Delete</button>
|
||||
</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">
|
||||
<!-- 01: Snapshot Metadata -->
|
||||
<div class="card is-open" data-section="0">
|
||||
@@ -541,7 +556,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<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 class="full">
|
||||
<label for="additional_categories">Additional categories (comma-separated)</label>
|
||||
@@ -822,13 +837,30 @@
|
||||
|
||||
<label for="output">Leonard-ready output</label>
|
||||
<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 id="toast"></div>
|
||||
|
||||
<script>
|
||||
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 PRESETS_KEY = "gbp-intake-v1.4-presets";
|
||||
const SECTIONS = 8;
|
||||
@@ -898,26 +930,45 @@
|
||||
const presets = getPresets();
|
||||
const data = presets[name];
|
||||
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);
|
||||
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
|
||||
computePlaceId();
|
||||
updateAllStatus();
|
||||
toast(`Loaded: ${name}`, "ok");
|
||||
toast(`Loaded identity fields for "${name}". Re-observe all dynamic data.`, "warn");
|
||||
}
|
||||
|
||||
function savePreset(name) {
|
||||
if (!name) { toast("Enter a client name first.", "warn"); return; }
|
||||
const presets = getPresets();
|
||||
const data = {};
|
||||
const fields = document.querySelectorAll("#gbpForm input, #gbpForm select, #gbpForm textarea");
|
||||
fields.forEach(el => { data[el.id] = el.value; });
|
||||
// Save only static identity fields — NEVER observation data
|
||||
STATIC_FIELDS.forEach(key => {
|
||||
const el = document.getElementById(key);
|
||||
if (el) data[key] = el.value;
|
||||
});
|
||||
presets[name] = data;
|
||||
savePresets(presets);
|
||||
populatePresetSelect();
|
||||
$("presetSelect").value = name;
|
||||
toast(`Saved: ${name}`, "ok");
|
||||
localStorageDisclosure();
|
||||
toast("Preset saved (identity fields only — re-observe before use).", "ok");
|
||||
}
|
||||
|
||||
function deletePreset(name) {
|
||||
@@ -928,6 +979,32 @@
|
||||
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 ── */
|
||||
document.querySelectorAll(".card-header").forEach(hdr => {
|
||||
hdr.addEventListener("click", () => {
|
||||
@@ -986,30 +1063,40 @@
|
||||
computedField.value = "";
|
||||
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 = [];
|
||||
// Try full Maps URL format first
|
||||
const placeMatch = url.match(/(?:place\/|\/)g\/[a-zA-Z0-9_\-]+/);
|
||||
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) {
|
||||
parts.push("feature: " + featureMatch[0]);
|
||||
if (featureMatch[1]) parts.push("!5s lead: " + featureMatch[1]);
|
||||
placeIdField.value = featureMatch[0];
|
||||
parts.push("feature: " + featureMatch[0].replace(/\s+/g, ""));
|
||||
if (featureMatch[1]) parts.push("!5s lead: " + featureMatch[1].replace(/\s+/g, ""));
|
||||
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]+)/);
|
||||
if (cidMatch && cidMatch[1]) {
|
||||
const cid = cidMatch[1].startsWith("0x") ? String(parseInt(cidMatch[1], 16)) : cidMatch[1];
|
||||
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(" · ") || "";
|
||||
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(); });
|
||||
|
||||
/* ── Auto-save on any input ── */
|
||||
document.querySelectorAll("#gbpForm input, #gbpForm select, #gbpForm textarea").forEach(el => {
|
||||
el.addEventListener("input", () => { saveDraft(); updateAllStatus(); });
|
||||
el.addEventListener("change", () => { saveDraft(); updateAllStatus(); });
|
||||
el.addEventListener("input", () => { saveDraft(); updateAllStatus(); updateReqMet(); });
|
||||
el.addEventListener("change", () => { saveDraft(); updateAllStatus(); updateReqMet(); });
|
||||
});
|
||||
|
||||
/* ── Toast ── */
|
||||
@@ -1042,9 +1129,9 @@
|
||||
function buildOutput() {
|
||||
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.
|
||||
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.
|
||||
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");
|
||||
});
|
||||
updateAllStatus();
|
||||
updateReqMet();
|
||||
localStorageDisclosure();
|
||||
toast("Form cleared.", "warn");
|
||||
});
|
||||
|
||||
// Preset buttons
|
||||
$("btnSavePreset").addEventListener("click", () => {
|
||||
const name = $("client").value.trim();
|
||||
if (!name) { toast("Enter a client name first.", "warn"); return; }
|
||||
savePreset(name);
|
||||
savePreset($("client").value.trim());
|
||||
});
|
||||
|
||||
$("btnDeletePreset").addEventListener("click", () => {
|
||||
@@ -1186,6 +1273,31 @@ ${sections.join("\n\n")}
|
||||
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
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
||||
@@ -1205,11 +1317,13 @@ ${sections.join("\n\n")}
|
||||
}
|
||||
populatePresetSelect();
|
||||
updateAllStatus();
|
||||
updateReqMet();
|
||||
localStorageDisclosure();
|
||||
// Recompute from any URL that might have been loaded
|
||||
computePlaceId();
|
||||
|
||||
// Auto-save every 30s as backup
|
||||
setInterval(saveDraft, 30000);
|
||||
setInterval(() => { saveDraft(); localStorageDisclosure(); }, 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user