[object Object]

← back to AbramsEgo

feat(sell): landing demo video + fulfillment runbook

fb3f1117e8bed3f9200265104e2ee9d8e7ceb22e · 2026-07-01 17:37:34 -0700 · Steve

50s silent Playwright tour of the :9773 dashboard (norma per-panel pattern,
red-highlight dwell on 11 panels), 1.6MB 720p h264, self-hosted at
/assets/demo/. Embedded in landing features section — muted/loop/poster,
lazy preload with IntersectionObserver autoplay. memos/fulfillment.md is
the manual-first Hosted/$19 + Pro/$49 provisioning + refund runbook.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Files touched

Diff

commit fb3f1117e8bed3f9200265104e2ee9d8e7ceb22e
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jul 1 17:37:34 2026 -0700

    feat(sell): landing demo video + fulfillment runbook
    
    50s silent Playwright tour of the :9773 dashboard (norma per-panel pattern,
    red-highlight dwell on 11 panels), 1.6MB 720p h264, self-hosted at
    /assets/demo/. Embedded in landing features section — muted/loop/poster,
    lazy preload with IntersectionObserver autoplay. memos/fulfillment.md is
    the manual-first Hosted/$19 + Pro/$49 provisioning + refund runbook.
    
    Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
 memos/fulfillment.md                         |  58 ++++++++++++++++++++
 public/assets/demo/abramsego-tour-poster.jpg | Bin 0 -> 96530 bytes
 public/assets/demo/abramsego-tour.mp4        | Bin 0 -> 1611368 bytes
 public/landing.html                          |  24 +++++++++
 scripts/demo-video.js                        |  78 +++++++++++++++++++++++++++
 5 files changed, 160 insertions(+)

diff --git a/memos/fulfillment.md b/memos/fulfillment.md
new file mode 100644
index 0000000..0ab4786
--- /dev/null
+++ b/memos/fulfillment.md
@@ -0,0 +1,58 @@
+# Fulfillment runbook — what happens after a checkout completes
+
+**Status:** TEST mode only (Stripe test keys, no real charges). This is the
+manual-first playbook for when a Hosted/$19 or Pro/$49 checkout completes —
+today in TEST, later live once Steve flips the switch. Provisioning is done
+by Steve by hand; nothing auto-provisions.
+
+## The moment of purchase
+
+1. Customer clicks a pricing CTA on `/landing.html` → `/api/checkout` creates a
+   Stripe Checkout Session (TEST) for the chosen tier → customer pays on
+   Stripe's hosted page.
+2. Stripe calls our webhook (`checkout.session.completed`). The server records
+   the sale to `data/revenue-ledger.jsonl` (engine: `sell`) and it shows up in
+   the dashboard P&L within one refresh.
+3. **Nothing else is automatic.** The customer's email + tier sit in the ledger
+   entry; Steve fulfills manually from there.
+
+## Manual provisioning — Steve's steps (target: within 24h of purchase)
+
+### Hosted — $19/mo
+1. Read the ledger entry (email, tier, Stripe customer id).
+2. Create the customer's instance on Kamatera: copy of the AbramsEgo app on its
+   own port, fresh Basic Auth credentials, empty data dir (their fleet, not ours).
+3. Point a subdomain at it (e.g. `<name>.abramsego.com`) with SSL.
+4. Email the customer from steve@designerwallcoverings.com: their URL, their
+   login, a 3-line quickstart (how to point collectors at their own pm2/ledger).
+
+### Pro — $49/mo
+Everything in Hosted, plus:
+5. Enable the revenue engines on their instance (`/api/revenue/record` open to
+   their key) and the grounded-chat panel wired to their Ollama or ours.
+6. Offer a 30-minute setup call (calendar link in the welcome email).
+
+## What the customer receives
+
+- **Hosted:** a private, Basic-auth-gated dashboard URL watching *their*
+  processes, with the cost ledger, canary board, and P&L panels live; a welcome
+  email with credentials and quickstart.
+- **Pro:** the above + revenue-engine recording, grounded AI chat, and the
+  setup call.
+- **Self-host (free):** no fulfillment — the repo/docs link is on the landing
+  page; they run it themselves.
+
+## Refund path
+
+- Simple rule: **full refund, no questions, within 30 days** — issued by Steve
+  in the Stripe dashboard (Payments → refund).
+- After refund: tear down the instance (stop pm2 process, remove subdomain),
+  append a negative entry to `revenue-ledger.jsonl` so the P&L stays honest.
+- Subscription cancels: customer emails us or uses the Stripe customer portal
+  (once enabled live); instance stays up until the end of the paid period.
+
+## Rails
+
+- Live keys, real charges, public exposure of `/landing.html`, DNS for customer
+  subdomains — all Steve-gated. Until go-live, every "sale" is a TEST-mode
+  rehearsal of this exact runbook.
diff --git a/public/assets/demo/abramsego-tour-poster.jpg b/public/assets/demo/abramsego-tour-poster.jpg
new file mode 100644
index 0000000..ad6db38
Binary files /dev/null and b/public/assets/demo/abramsego-tour-poster.jpg differ
diff --git a/public/assets/demo/abramsego-tour.mp4 b/public/assets/demo/abramsego-tour.mp4
new file mode 100644
index 0000000..bea2e6b
Binary files /dev/null and b/public/assets/demo/abramsego-tour.mp4 differ
diff --git a/public/landing.html b/public/landing.html
index a0bc33f..1844f84 100644
--- a/public/landing.html
+++ b/public/landing.html
@@ -54,6 +54,11 @@
   .card .ico{font-size:22px}
   .card h3{margin:10px 0 6px;font-size:15px}
   .card p{margin:0;color:var(--dim);font-size:13px}
+  /* — demo video — */
+  .demo{margin-top:26px;text-align:center}
+  .demo video{width:100%;max-width:920px;border:1px solid var(--line);border-radius:14px;
+    box-shadow:0 20px 60px -30px #000;display:block;margin:0 auto;background:#0c1420}
+  .demo .cap{color:var(--dim);font-size:11px;margin-top:10px}
   /* — pricing — */
   .tiers{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:8px}
   @media(max-width:860px){.tiers{grid-template-columns:1fr}}
@@ -144,6 +149,25 @@
       <div class="card"><div class="ico">🤖</div><h3>Grounded AI chat</h3>
         <p>Ask "is it paying for itself?" and get an answer grounded in the live snapshot — via your local Ollama, so the chat itself costs $0.</p></div>
     </div>
+
+    <!-- 50s silent tour of the live dashboard (autoplays muted when scrolled into view) -->
+    <div class="demo">
+      <video id="tour" muted loop playsinline preload="none"
+             poster="/assets/demo/abramsego-tour-poster.jpg"
+             src="/assets/demo/abramsego-tour.mp4"></video>
+      <div class="cap">50-second tour of the live dashboard — P&amp;L, cost trend, fleet, canaries, officers, sessions and grounded chat.</div>
+    </div>
+    <script>
+      (function(){
+        var v = document.getElementById('tour');
+        if (!('IntersectionObserver' in window)) { v.preload = 'metadata'; v.controls = true; return; }
+        new IntersectionObserver(function(es, obs){
+          es.forEach(function(e){
+            if (e.isIntersecting) { v.play().catch(function(){ v.controls = true; }); obs.disconnect(); }
+          });
+        }, { threshold: 0.35 }).observe(v);
+      })();
+    </script>
   </section>
 
   <!-- PRICING -->
diff --git a/scripts/demo-video.js b/scripts/demo-video.js
new file mode 100644
index 0000000..a193782
--- /dev/null
+++ b/scripts/demo-video.js
@@ -0,0 +1,78 @@
+#!/usr/bin/env node
+// Capture a ~50s click-through tour of the :9773 dashboard (norma-videos pattern:
+// per-panel dwell with a red highlight box; muted/no-TTS cut for the landing page).
+// Run: NODE_PATH=~/.claude/skills/app-demo-video/node_modules node scripts/demo-video.js
+// Output: tmp/demo-raw/<hash>.webm — converted to mp4 by scripts/demo-video.sh
+const { chromium } = require('playwright');
+const path = require('path');
+
+const BASE = process.env.EGO_URL || 'http://127.0.0.1:9773/';
+const OUT_DIR = path.join(__dirname, '..', 'tmp', 'demo-raw');
+const W = 1280, H = 720;
+
+// h2 text prefixes, in tour order (matched with startsWith on the emoji+title)
+const TOUR = [
+  '💰 Self-Funding P&L',
+  '🔌 Cost to run',
+  '📈 Cost trend',
+  '🛰 Fleet · Kamatera',
+  '🖥 Fleet · Local',
+  '🐤 Canary board',
+  '🏛 Officers',
+  '🏆 Wins',
+  '💬 Live Claude sessions',
+  '🧠 Model usage',
+  '🤖 Ask AbramsEgo',
+];
+const DWELL_MS = 3600;   // per-panel hold
+const INTRO_MS = 4500;   // top-of-page hold
+
+(async () => {
+  // skill's playwright (1.59.1) expects browser build 1217, which isn't cached;
+  // point at the cached headless-shell build instead of downloading a new one
+  const shell = process.env.HOME + '/Library/Caches/ms-playwright/chromium_headless_shell-1228/chrome-headless-shell-mac-arm64/chrome-headless-shell';
+  const browser = await chromium.launch({ executablePath: shell });
+  const ctx = await browser.newContext({
+    viewport: { width: W, height: H },
+    recordVideo: { dir: OUT_DIR, size: { width: W, height: H } },
+    httpCredentials: { username: 'admin', password: 'DW2024!' },
+  });
+  const page = await ctx.newPage();
+  await page.goto(BASE, { waitUntil: 'networkidle' }).catch(() => {});
+  // let the snapshot panels populate
+  await page.waitForTimeout(3000);
+
+  await page.addStyleTag({ content: `
+    .demo-hl { outline: 3px solid #ff4d4d !important; outline-offset: 4px;
+               border-radius: 8px; transition: outline-color .3s; }
+    html { scroll-behavior: smooth; }
+  `});
+
+  await page.waitForTimeout(INTRO_MS);
+
+  for (const title of TOUR) {
+    const hit = await page.evaluate((t) => {
+      const h = [...document.querySelectorAll('h2')]
+        .find(el => el.textContent.trim().startsWith(t.slice(0, 6)) && el.textContent.includes(t.slice(2).trim().split(' ')[0]));
+      if (!h) return false;
+      const panel = h.closest('section,div.panel,article') || h.parentElement;
+      document.querySelectorAll('.demo-hl').forEach(e => e.classList.remove('demo-hl'));
+      panel.classList.add('demo-hl');
+      panel.scrollIntoView({ behavior: 'smooth', block: 'center' });
+      return true;
+    }, title);
+    if (!hit) console.error('panel not found:', title);
+    await page.waitForTimeout(DWELL_MS);
+  }
+
+  // close on the top metrics bar
+  await page.evaluate(() => {
+    document.querySelectorAll('.demo-hl').forEach(e => e.classList.remove('demo-hl'));
+    window.scrollTo({ top: 0, behavior: 'smooth' });
+  });
+  await page.waitForTimeout(3000);
+
+  await ctx.close();
+  await browser.close();
+  console.log('webm written to', OUT_DIR);
+})();

← 2261b68 queue: task 34 done — landing CTAs + PUBLIC_LANDING flag lan  ·  back to AbramsEgo  ·  queue: task 35 done — landing demo video + fulfillment runbo 3c094d7 →