v0.2.0-alpha: DOM capture, all three output slices, lead gates
- Extension DOM mode: auth-context serializer — inlines CSS + images as data URLs, freezes form state, masks [data-dp-mask], strips scripts (R3 client) - API: extractDataUrls rewrites large inline resources into content-addressed assets (R3 server; verified 939KB inline -> 82-byte row + served asset) - Lead-gate steps: config on step, player overlay form blocks advance, submits to leads endpoint (editor toggle + MCP set_lead_gate) - Step-guide export (v2 slice): authed markdown + public HTML at /p/:token/guide - Video export (v3 slice, v0): ffmpeg slideshow of image steps + optional narration mux; DOM rasterization deferred to Chromium service (R4) - Editor: reorder arrows, embed-code copy, guide link, video export - REST reorder endpoint; MCP surface now 13 tools (export_guide, set_lead_gate, render_video) - ffmpeg added to runtime image E2E verified incl. R3 extraction round-trip, lead_gate in public JSON, guide renders, MCP agent exported guide + rendered video.
This commit is contained in:
parent
9b09f14916
commit
a535e15253
18 changed files with 541 additions and 22 deletions
|
|
@ -3,6 +3,7 @@ import { randomBytes } from "node:crypto";
|
|||
import { z } from "zod";
|
||||
import { pool } from "../db.js";
|
||||
import { requireScope } from "../auth.js";
|
||||
import { extractDataUrls } from "../assetExtract.js";
|
||||
|
||||
const stepPatch = z.object({
|
||||
hotspot: z
|
||||
|
|
@ -14,6 +15,15 @@ const stepPatch = z.object({
|
|||
.nullable()
|
||||
.optional(),
|
||||
position: z.number().int().min(0).optional(),
|
||||
lead_gate: z
|
||||
.object({
|
||||
enabled: z.boolean(),
|
||||
fields: z
|
||||
.array(z.object({ name: z.string().max(50), label: z.string().max(100), required: z.boolean() }))
|
||||
.max(10),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export async function demoRoutes(app: FastifyInstance): Promise<void> {
|
||||
|
|
@ -29,7 +39,7 @@ export async function demoRoutes(app: FastifyInstance): Promise<void> {
|
|||
const d = await pool.query("SELECT * FROM demos WHERE id=$1", [id]);
|
||||
if (!d.rowCount) return reply.code(404).send({ error: "not found" });
|
||||
const s = await pool.query(
|
||||
"SELECT id, position, snapshot_type, asset_id, hotspot, annotation FROM steps WHERE demo_id=$1 ORDER BY position",
|
||||
"SELECT id, position, snapshot_type, asset_id, hotspot, annotation, lead_gate FROM steps WHERE demo_id=$1 ORDER BY position",
|
||||
[id],
|
||||
);
|
||||
return { ...d.rows[0], steps: s.rows };
|
||||
|
|
@ -60,13 +70,15 @@ export async function demoRoutes(app: FastifyInstance): Promise<void> {
|
|||
return reply.code(400).send({ error: "image step requires asset_id" });
|
||||
if (body.snapshot_type === "dom" && !body.dom_html)
|
||||
return reply.code(400).send({ error: "dom step requires dom_html" });
|
||||
let domHtml = body.dom_html ?? null;
|
||||
if (domHtml) domHtml = await extractDataUrls(domHtml); // R3 server half
|
||||
const pos =
|
||||
body.position ??
|
||||
(await pool.query("SELECT COALESCE(MAX(position)+1,0) AS p FROM steps WHERE demo_id=$1", [id]))
|
||||
.rows[0].p;
|
||||
const r = await pool.query(
|
||||
"INSERT INTO steps (demo_id, position, snapshot_type, asset_id, dom_html) VALUES ($1,$2,$3,$4,$5) RETURNING id, position, snapshot_type, asset_id",
|
||||
[id, pos, body.snapshot_type, body.asset_id ?? null, body.dom_html ?? null],
|
||||
[id, pos, body.snapshot_type, body.asset_id ?? null, domHtml],
|
||||
);
|
||||
await pool.query("UPDATE demos SET updated_at=now() WHERE id=$1", [id]);
|
||||
return r.rows[0];
|
||||
|
|
@ -80,6 +92,7 @@ export async function demoRoutes(app: FastifyInstance): Promise<void> {
|
|||
let i = 1;
|
||||
if (body.hotspot !== undefined) { sets.push(`hotspot=$${i++}`); vals.push(body.hotspot ? JSON.stringify(body.hotspot) : null); }
|
||||
if (body.annotation !== undefined) { sets.push(`annotation=$${i++}`); vals.push(body.annotation ? JSON.stringify(body.annotation) : null); }
|
||||
if (body.lead_gate !== undefined) { sets.push(`lead_gate=$${i++}`); vals.push(body.lead_gate ? JSON.stringify(body.lead_gate) : null); }
|
||||
if (body.position !== undefined) { sets.push(`position=$${i++}`); vals.push(body.position); }
|
||||
if (!sets.length) return reply.code(400).send({ error: "empty patch" });
|
||||
vals.push(stepId);
|
||||
|
|
@ -97,6 +110,19 @@ export async function demoRoutes(app: FastifyInstance): Promise<void> {
|
|||
return { ok: true };
|
||||
});
|
||||
|
||||
app.post("/api/demos/:id/reorder", { preHandler: requireScope("author") }, async (req, reply) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const body = z.object({ ordered_step_ids: z.array(z.string().uuid()).min(1) }).parse(req.body);
|
||||
await pool.query("BEGIN");
|
||||
try {
|
||||
for (let i = 0; i < body.ordered_step_ids.length; i++)
|
||||
await pool.query("UPDATE steps SET position=$1 WHERE id=$2 AND demo_id=$3", [i, body.ordered_step_ids[i], id]);
|
||||
await pool.query("UPDATE demos SET updated_at=now() WHERE id=$1", [id]);
|
||||
await pool.query("COMMIT");
|
||||
} catch (e) { await pool.query("ROLLBACK"); throw e; }
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
app.post("/api/demos/:id/publish", { preHandler: requireScope("publish") }, async (req, reply) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const token = randomBytes(12).toString("hex");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue