v0.1.0-alpha: interactive demo platform core

- Fastify API: demos/steps CRUD, content-addressed assets, scoped API keys
  (read/author/publish/admin, draft-only default per D4)
- Player: image + sandboxed DOM steps, hotspots, tooltips, keyboard nav,
  privacy-first beacons (no IP storage, viewer tokens only — R7)
- Editor: React SPA — demo list, drag-to-draw hotspots, annotations, publish
- MCP server: Streamable HTTP, scope-filtered tools, untrusted-data wrapping
  on viewer-supplied content (R8); platform never calls a model
- Capture extension (MV3): screenshot-flow recording, hotspots pre-placed
  from real clicks
- Docker: single app image + postgres, Traefik-ready (expose only)

E2E verified: capture->author->publish->play->beacon->lead->analytics + full
MCP tool surface with scope filtering.
This commit is contained in:
Scot Thom 2026-07-18 05:17:45 +00:00
commit 9b09f14916
33 changed files with 5555 additions and 0 deletions

25
extension/background.js Normal file
View file

@ -0,0 +1,25 @@
// Screenshot-flow capture: on each click in the recorded tab, grab a screenshot
// plus the click position, building image Steps with pre-placed hotspots.
let recording = { active: false, tabId: null, steps: [] };
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === "start") {
recording = { active: true, tabId: msg.tabId, steps: [] };
chrome.scripting.executeScript({ target: { tabId: msg.tabId }, files: ["content.js"] });
sendResponse({ ok: true });
} else if (msg.type === "click" && recording.active && sender.tab?.id === recording.tabId) {
chrome.tabs.captureVisibleTab({ format: "png" }, (dataUrl) => {
recording.steps.push({ dataUrl, xPct: msg.xPct, yPct: msg.yPct });
sendResponse({ ok: true });
});
return true; // async
} else if (msg.type === "status") {
sendResponse({ active: recording.active, count: recording.steps.length });
} else if (msg.type === "stop") {
recording.active = false;
sendResponse({ steps: recording.steps });
} else if (msg.type === "discard") {
recording = { active: false, tabId: null, steps: [] };
sendResponse({ ok: true });
}
});

16
extension/content.js Normal file
View file

@ -0,0 +1,16 @@
// Reports click positions as viewport percentages. Injected only while recording.
(function () {
if (window.__dpCaptureInstalled) return;
window.__dpCaptureInstalled = true;
document.addEventListener(
"click",
(e) => {
chrome.runtime.sendMessage({
type: "click",
xPct: (e.clientX / window.innerWidth) * 100,
yPct: (e.clientY / window.innerHeight) * 100,
});
},
true,
);
})();

11
extension/manifest.json Normal file
View file

@ -0,0 +1,11 @@
{
"manifest_version": 3,
"name": "Demo Platform Capture",
"version": "0.1.0",
"description": "Capture click-through product demos. Screenshot mode (v0); DOM mode coming in v1.",
"permissions": ["activeTab", "tabs", "storage", "scripting"],
"host_permissions": ["<all_urls>"],
"action": { "default_popup": "popup.html", "default_title": "Demo Capture" },
"background": { "service_worker": "background.js" },
"icons": {}
}

21
extension/popup.html Normal file
View file

@ -0,0 +1,21 @@
<!doctype html>
<html>
<head><meta charset="utf-8">
<style>
body { font: 13px system-ui, sans-serif; width: 280px; padding: 14px; color: #0f172a; }
input { width: 100%; margin: 4px 0 8px; padding: 6px 8px; border: 1px solid #e2e8f0; border-radius: 6px; box-sizing: border-box; }
button { width: 100%; margin-top: 6px; padding: 8px; border: 1px solid #e2e8f0; border-radius: 6px; background: #fff; cursor: pointer; }
button.primary { background: #2563eb; color: #fff; border-color: #2563eb; }
#status { color: #64748b; margin: 8px 0; }
</style>
</head>
<body>
<label>Platform URL <input id="base" placeholder="https://demo.example.com"></label>
<label>API key <input id="key" type="password" placeholder="dp_..."></label>
<div id="status">Not recording.</div>
<button id="start" class="primary">Start recording this tab</button>
<button id="finish" hidden>Finish &amp; upload</button>
<button id="discard" hidden>Discard</button>
<script src="popup.js"></script>
</body>
</html>

67
extension/popup.js Normal file
View file

@ -0,0 +1,67 @@
const $ = (id) => document.getElementById(id);
chrome.storage.local.get(["base", "key"], (v) => {
if (v.base) $("base").value = v.base;
if (v.key) $("key").value = v.key;
});
const save = () => chrome.storage.local.set({ base: $("base").value.trim(), key: $("key").value.trim() });
$("base").addEventListener("change", save);
$("key").addEventListener("change", save);
function refresh() {
chrome.runtime.sendMessage({ type: "status" }, (s) => {
$("status").textContent = s.active ? `Recording — ${s.count} steps captured. Click through your flow, then Finish.` : "Not recording.";
$("start").hidden = s.active;
$("finish").hidden = !s.active;
$("discard").hidden = !s.active;
});
}
refresh();
$("start").addEventListener("click", async () => {
save();
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
chrome.runtime.sendMessage({ type: "start", tabId: tab.id }, refresh);
});
$("discard").addEventListener("click", () => chrome.runtime.sendMessage({ type: "discard" }, refresh));
$("finish").addEventListener("click", () => {
chrome.runtime.sendMessage({ type: "stop" }, async ({ steps }) => {
const base = $("base").value.trim().replace(/\/$/, "");
const key = $("key").value.trim();
if (!base || !key) { $("status").textContent = "Set platform URL and API key first."; return; }
if (!steps.length) { $("status").textContent = "No steps captured."; return; }
try {
$("status").textContent = "Uploading…";
const auth = { Authorization: `Bearer ${key}` };
const demo = await (await fetch(`${base}/api/demos`, {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({ name: `Capture ${new Date().toLocaleString()}` }),
})).json();
for (const s of steps) {
const blob = await (await fetch(s.dataUrl)).blob();
const fd = new FormData();
fd.append("file", blob, "step.png");
const asset = await (await fetch(`${base}/api/assets`, { method: "POST", headers: auth, body: fd })).json();
const step = await (await fetch(`${base}/api/demos/${demo.id}/steps`, {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({ snapshot_type: "image", asset_id: asset.id }),
})).json();
// pre-place a hotspot centred on the recorded click
await fetch(`${base}/api/steps/${step.id}`, {
method: "PATCH",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({ hotspot: { x: Math.max(0, s.xPct - 4), y: Math.max(0, s.yPct - 4), w: 8, h: 8 } }),
});
}
chrome.runtime.sendMessage({ type: "discard" }, () => {});
$("status").textContent = `Done — demo "${demo.name}" created with ${steps.length} steps. Open the editor to refine.`;
refresh();
} catch (e) {
$("status").textContent = `Upload failed: ${e}`;
}
});
});