fix(film): capture resilience + scene fixes; finalize 20-clip durations

Orchestrator: bind ctx.spaNavigate to the page (fixes breadth-flash/role-perspective doing nothing) and wrap each clip in try/catch so one bad scene can't abort the run or lose the manifest. Cursor: guard page.evaluate against navigation-mid-move (fixes storm-intel 'execution context destroyed'). territory-map now leads with an existing Hot-Lead parcel (full populated detail); crane-close shows the full crane (mast+cable) then pans to the swinging card. Manifest holds real ffprobe durations for all 20 clips (film = 14m56s).
This commit is contained in:
Satyam Rastogi
2026-05-31 00:07:44 +05:30
parent f76a9da7b4
commit ba52200d08
5 changed files with 380 additions and 210 deletions
@@ -1,58 +1,51 @@
// crane-close.mjs — PUBLIC, route "/" (~22s). The swinging-crane CTA closer.
// Scroll to the end "Ready to Build?" CTA card (it hangs from a blueprint crane and
// swings like a pendulum, class .crane-swing), let the swing settle, then ease the
// cursor onto the "Schedule Demo" CTA and HOLD while it swings. Do NOT navigate away.
// Show the FULL tower crane first (apex / jib / mast — the blueprint SVG), THEN pan DOWN
// to the "Ready to Build?" card that hangs from it and swings (.crane-swing). Hover the
// CTA and hold; never click (must not navigate to /login).
export default async (page, cursor, ctx) => {
const dwell = (ms) => page.waitForTimeout(ms);
await page.waitForLoadState("networkidle").catch(() => {});
await dwell(1000);
await dwell(800);
// 1) Scroll to the very end (the crane CTA sits just before the footer).
await page
.evaluate(() => window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" }))
.catch(() => {});
// Wait for the smooth scroll + swing animation to begin/settle.
await dwell(2500);
// 2) Wait for the swinging CTA card to be visible; scroll it into view to be safe.
// The card carries the unique "Ready to Build?" headline.
// 1) Bring the crane SECTION's TOP into frame so the apex + jib + mast are visible
// (scrolling to document bottom previously cut the crane off, showing only the card).
const craneSvg = page.locator('svg[viewBox="0 0 800 460"]').first();
const craneCard = page.locator(".crane-swing").first();
const heading = page.getByRole("heading", { name: /ready to build/i }).first();
const anchor = (await craneCard.count()) ? craneCard : heading;
const anchor = (await craneSvg.count()) ? craneSvg : craneCard;
if (await anchor.count()) {
await anchor.scrollIntoViewIfNeeded().catch(() => {});
await anchor.waitFor({ state: "visible", timeout: 12000 }).catch(() => {});
await anchor
.evaluate((el) => el.scrollIntoView({ behavior: "smooth", block: "start" }))
.catch(() => {});
} else {
await page
.evaluate(() => window.scrollTo({ top: document.body.scrollHeight * 0.8, behavior: "smooth" }))
.catch(() => {});
}
await dwell(2500); // hold while the pendulum swings
await dwell(3200); // hold on the full crane (apex / counter-jib / jib / lattice mast)
// 3) Ease the cursor onto the CTA button ("Schedule Demo") inside the swinging card
// and hold — hover only, never click (we must not navigate to /login).
const cta = page
.getByRole("link", { name: /schedule demo/i })
.last(); // the crane card's CTA is the last on the page (hero has the first)
// 2) Pan DOWN to reveal the card swinging on its cable beneath the jib.
await page.evaluate(() => window.scrollBy({ top: 340, behavior: "smooth" })).catch(() => {});
await dwell(4000); // hold while the card swings like a pendulum
// 3) Ease the cursor onto the card's CTA and hold — HOVER ONLY, never click.
const cta = page.getByRole("link", { name: /schedule demo/i }).last();
if (await cta.count()) {
const box = await cta.boundingBox().catch(() => null);
if (box) {
await cursor.moveTo(box.x + box.width / 2, box.y + box.height / 2, 1000);
const b = await cta.boundingBox().catch(() => null);
if (b) {
await cursor.moveTo(b.x + b.width / 2, b.y + b.height / 2, 1000);
await dwell(2500);
}
} else if (await heading.count()) {
// Fallback: drift onto the headline region of the card.
const hb = await heading.boundingBox().catch(() => null);
if (hb) await cursor.moveTo(hb.x + hb.width / 2, hb.y + hb.height / 2, 1000);
}
await dwell(3500);
// 4) Small secondary drift to the "Start Free Trial" CTA to show both, still no click.
const cta2 = page.getByRole("link", { name: /start free trial/i }).first();
const cta2 = page.getByRole("link", { name: /start free trial/i }).last();
if (await cta2.count()) {
const b2 = await cta2.boundingBox().catch(() => null);
if (b2) {
await cursor.moveTo(b2.x + b2.width / 2, b2.y + b2.height / 2, 900);
await dwell(2500);
await dwell(2000);
}
}
// 5) Final hold on the swinging card — the closing brand beat.
// 4) Final hold on the full crane + swinging card — the closing brand beat.
await dwell(3000);
};
@@ -1,15 +1,11 @@
// territory-map.mjs — OWNER, route "/owner/maps" (~55s, HERO).
// Full territory walkthrough on the Leaflet map:
// load map+tiles → click an EMPTY spot to drop a new property pin (reverse-geocode →
// detail drawer opens in create/edit mode) → step through the drawer (set Canvassing
// Status — the agent/canvasser-style assignment control) → then click an EXISTING
// parcel polygon to reveal its populated detail panel. End on a populated drawer.
// Everything is guarded so a missing element never crashes the scene.
// territory-map.mjs — OWNER, route "/owner/maps" (~60s, HERO).
// LEAD with an EXISTING HOT LEAD: click its red parcel to reveal the fully-populated
// property detail (view mode, all info) — the data-rich payoff. THEN show creating a NEW
// entry by dropping a pin on empty map (reverse-geocode → set Hot Lead). All guarded.
export default async (page, cursor, ctx) => {
const dwell = (ms) => page.waitForTimeout(ms);
// 1) Wait for the map. The page shows a full-screen "Loading Territory Map..." loader
// (~1s) before the Leaflet container mounts.
// 1) Wait for the map (full-screen "Loading Territory Map..." loader, then Leaflet mounts).
await page
.getByText(/loading territory map/i)
.first()
@@ -17,90 +13,72 @@ export default async (page, cursor, ctx) => {
.catch(() => {});
const map = page.locator(".leaflet-container").first();
await map.waitFor({ state: "visible", timeout: 20000 });
// Let the OSM tiles settle.
await page.waitForLoadState("networkidle").catch(() => {});
await dwell(2000); // open on the loaded territory map
const box = await map.boundingBox();
if (!box) {
await dwell(3000);
return;
}
// 2) Click an EXISTING HOT LEAD parcel. Leaflet renders each property Polygon as an SVG
// path; Hot Leads are stroked red (#ef4444). Clicking fires onSelect → the
// PropertyDetailDrawer opens in VIEW mode populated with that lead's full data.
let hot = page.locator('.leaflet-overlay-pane path[stroke="#ef4444"]').first();
if (!(await hot.count())) hot = page.locator(".leaflet-interactive").first(); // fallback: any parcel
if (await hot.count()) {
await cursor.clickLocator(hot, 900);
const drawer = page.getByRole("dialog", { name: /property details/i }).first();
await drawer.waitFor({ state: "visible", timeout: 12000 }).catch(() => {});
await dwell(3500); // hold on the fully-populated hot-lead detail (owner, value, status…)
// 2) Drop a new property pin on an EMPTY patch of map. Clicking empty map triggers a
// reverse-geocode (Nominatim) then opens the PropertyDetailDrawer in create mode.
// Pick a spot offset from existing parcels (upper-left quadrant).
const drop = { x: box.x + box.width * 0.32, y: box.y + box.height * 0.30 };
const waitGeocode = page
.waitForResponse((r) => /nominatim\.openstreetmap\.org\/reverse/.test(r.url()), { timeout: 20000 })
.catch(() => null);
await cursor.click(drop.x, drop.y, 900);
await waitGeocode;
// Reveal the "Edit Details" affordance on the populated panel.
const edit = drawer.getByRole("button", { name: /edit details/i }).first();
if (await edit.count()) {
const eb = await edit.boundingBox().catch(() => null);
if (eb) {
await cursor.moveTo(eb.x + eb.width / 2, eb.y + eb.height / 2, 900);
await dwell(2500);
}
}
// Drawer opens (role=dialog "Property Details"). Wait for the address to resolve
// ("Fetching Address..."/"Resolving Location..." → real content).
const drawer = page.getByRole("dialog", { name: /property details/i }).first();
await drawer.waitFor({ state: "visible", timeout: 12000 }).catch(() => {});
await page
.getByText(/resolving location|fetching address/i)
.first()
.waitFor({ state: "hidden", timeout: 15000 })
.catch(() => {});
await dwell(2800); // hold on the new-entry drawer
// 3) Set a Canvassing Status — this is the assignment-style control for a new lead.
// In create mode the drawer opens already editing, so the status pills are present.
const hotLead = drawer.getByRole("button", { name: /^hot lead$/i }).first();
if (await hotLead.count()) {
await cursor.clickLocator(hotLead, 800);
await dwell(2500);
}
// 4) Show the Create Entry action (the populated drawer footer). Just reveal/hover it —
// we don't need to actually persist, the payoff is the rich detail panel.
const createBtn = drawer.getByRole("button", { name: /create entry|save changes/i }).first();
if (await createBtn.count()) {
const cb = await createBtn.boundingBox().catch(() => null);
if (cb) {
await cursor.moveTo(cb.x + cb.width / 2, cb.y + cb.height / 2, 800);
await dwell(2000);
// Close it before showing the create flow.
const close = drawer.getByRole("button", { name: /close property details/i }).first();
if (await close.count()) {
await cursor.clickLocator(close, 700);
await dwell(1500);
}
}
// 5) Close the new-entry drawer, then click an EXISTING parcel polygon to reveal its
// populated detail panel (the data-rich payoff).
const closeBtn = drawer.getByRole("button", { name: /close property details/i }).first();
if (await closeBtn.count()) {
await cursor.clickLocator(closeBtn, 700);
await dwell(1500);
}
// Click over a dense central area where colored parcels live. Leaflet polygon clicks
// open the drawer in view mode (with an "Edit Details" footer button).
const parcel = { x: box.x + box.width * 0.55, y: box.y + box.height * 0.55 };
await cursor.click(parcel.x, parcel.y, 900);
await dwell(1500);
// If that opened a NEW-entry geocode drawer instead of selecting a parcel, that's still
// a valid populated panel; if it selected a parcel we get "Edit Details".
const drawer2 = page.getByRole("dialog", { name: /property details/i }).first();
if (await drawer2.count()) {
// 3) Now show CREATING a NEW entry: click an empty patch → reverse-geocode → new drawer.
const box = await map.boundingBox();
if (box) {
const drop = { x: box.x + box.width * 0.3, y: box.y + box.height * 0.28 };
const waitGeocode = page
.waitForResponse((r) => /nominatim\.openstreetmap\.org\/reverse/.test(r.url()), { timeout: 20000 })
.catch(() => null);
await cursor.click(drop.x, drop.y, 900);
await waitGeocode;
const drawer2 = page.getByRole("dialog", { name: /property details/i }).first();
await drawer2.waitFor({ state: "visible", timeout: 12000 }).catch(() => {});
await page
.getByText(/resolving location|fetching address/i)
.first()
.waitFor({ state: "hidden", timeout: 12000 })
.waitFor({ state: "hidden", timeout: 15000 })
.catch(() => {});
await dwell(2500);
await dwell(2500); // hold on the new-entry drawer with the resolved address
// Reveal the edit affordance on the selected parcel, if present.
const editBtn = drawer2.getByRole("button", { name: /edit details/i }).first();
if (await editBtn.count()) {
await cursor.clickLocator(editBtn, 800);
await dwell(2500);
// Set the new entry's Canvassing Status to Hot Lead.
const hotBtn = drawer2.getByRole("button", { name: /^hot lead$/i }).first();
if (await hotBtn.count()) {
await cursor.clickLocator(hotBtn, 800);
await dwell(2000);
}
// Reveal the Create Entry action (hover; we don't need to persist).
const createBtn = drawer2.getByRole("button", { name: /create entry|save changes/i }).first();
if (await createBtn.count()) {
const cb = await createBtn.boundingBox().catch(() => null);
if (cb) {
await cursor.moveTo(cb.x + cb.width / 2, cb.y + cb.height / 2, 800);
await dwell(2000);
}
}
}
// 6) Final hold on the populated property detail panel.
await dwell(3000);
await dwell(2500); // final hold
};