@elitedcs/ghl-mcp 3.86.0 → 3.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -29061,7 +29061,7 @@ var require_package = __commonJS({
29061
29061
  "package.json"(exports2, module2) {
29062
29062
  module2.exports = {
29063
29063
  name: "@elitedcs/ghl-mcp",
29064
- version: "3.86.0",
29064
+ version: "3.87.0",
29065
29065
  mcpName: "io.github.drjerryrelth/ghl-command",
29066
29066
  description: "GoHighLevel MCP Server for Claude. 250 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
29067
29067
  main: "dist/index.js",
@@ -29092,7 +29092,7 @@ var require_package = __commonJS({
29092
29092
  "!skills/blueprint/presets/clinic-launch-a2p.md"
29093
29093
  ],
29094
29094
  scripts: {
29095
- build: "node src/command-os/assessment/build-app.mjs && esbuild src/index.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/index.js --packages=external && esbuild src/capture-helper.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/capture-helper.js --packages=external",
29095
+ build: "node src/command-os/assessment/build-app.mjs && node scripts/gen-report-css.mjs && esbuild src/index.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/index.js --packages=external && esbuild src/capture-helper.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/capture-helper.js --packages=external",
29096
29096
  typecheck: "tsc --noEmit",
29097
29097
  setup: "node setup-wizard.mjs",
29098
29098
  catalogue: "node scripts/export-command-os-catalogue.mjs",
@@ -29422,7 +29422,144 @@ function parseCockpitDate(value){
29422
29422
  }
29423
29423
  });
29424
29424
 
29425
+ // src/command-os/shopper/score.ts
29426
+ function gradeOf(score2) {
29427
+ return score2 >= 90 ? "A" : score2 >= 80 ? "B" : score2 >= 70 ? "C" : score2 >= 60 ? "D" : "F";
29428
+ }
29429
+ function scoreRun(run) {
29430
+ if (run.status === "failed") return { graded: false, reason: "The check did not run, so there is nothing to grade." };
29431
+ if (run.status !== "done") return { graded: false, reason: "The grade appears when the check finishes." };
29432
+ const f = run.findings;
29433
+ if (!f) return { graded: false, reason: "No messages were read, so there is nothing to grade." };
29434
+ const st = run.steps;
29435
+ const stepRows = st?.read ? (st.rows ?? []).filter((r) => r.outcome !== "entered" && r.outcome !== "finished") : [];
29436
+ const lastByStep = /* @__PURE__ */ new Map();
29437
+ for (const r of stepRows) lastByStep.set(`${r.workflowId}|${r.stepId ?? r.stepName}`, r.outcome);
29438
+ const totalSteps = lastByStep.size;
29439
+ const unclear = [...lastByStep.values()].filter((o) => o === "unclear").length;
29440
+ const judgedSteps = [...lastByStep.values()].filter((o) => o !== "unclear" && o !== "waiting").length;
29441
+ const drafts = run.draftWorkflows ?? [];
29442
+ const stepProblems = st?.read ? (st.problems ?? []).length : 0;
29443
+ if (f.sent === 0 && judgedSteps === 0 && drafts.length === 0 && stepProblems === 0) return { graded: false, reason: "Nothing the test lead received and no workflow step could be judged, so there is nothing to grade. Check that the workflows are published, then run it again." };
29444
+ const held = (f.notYetDue ?? []).filter((m) => m.heldByWait).length;
29445
+ if (held) return { graded: false, reason: `GoHighLevel was still holding the test lead, so ${held} planned message${held === 1 ? " was" : "s were"} not judged. A grade now would look better than the truth. Run the check again.` };
29446
+ const deductions2 = [];
29447
+ const add2 = (kind, area, text2) => {
29448
+ const p = POINTS[kind];
29449
+ deductions2.push({ kind, points: p.points, text: text2, area, ..."cap" in p ? { cap: p.cap } : {} });
29450
+ };
29451
+ for (const p of st?.read ? st.problems ?? [] : []) add2(p.kind, "Workflow steps", `${POINTS[p.kind].what}: "${p.stepName}" in ${p.workflowName}`);
29452
+ for (const d of drafts) add2("did_nothing", "Workflow steps", `A chosen workflow is still a draft, so it did nothing: ${d}`);
29453
+ for (const p of f.problems ?? []) add2(p.kind, p.kind === "broken_link" ? "Links" : "Messages", p.sentBy ? `${POINTS[p.kind].what}: ${p.sentBy}` : POINTS[p.kind].what);
29454
+ const draftIds = run.draftWorkflowIds ?? [];
29455
+ const rootCauses = [
29456
+ ...drafts.map((name, i) => ({ id: draftIds.length === drafts.length ? draftIds[i] : void 0, name, why: `the workflow is still a draft` })),
29457
+ ...(st?.read ? st.problems ?? [] : []).filter((p) => p.kind === "failed_holding" || p.kind === "failed_gave_up").map((p) => ({ id: p.workflowId, name: p.workflowName, why: `"${p.stepName}" failed` }))
29458
+ ];
29459
+ const sameWorkflow = (c, m) => c.id && m.workflowId ? c.id === m.workflowId : m.label.startsWith(`${c.name}: `);
29460
+ const ownStepLogged = (m) => (st?.read ? st.rows ?? [] : []).some((r) => (r.outcome === "ran" || r.outcome === "branch") && (m.workflowId ? r.workflowId === m.workflowId : true) && (m.stepId ? r.stepId === m.stepId : m.label.endsWith(`: ${r.stepName}`)));
29461
+ const ownStepCharged = (m) => {
29462
+ const problems = st?.read ? st.problems ?? [] : [];
29463
+ if (m.stepId) {
29464
+ let last;
29465
+ for (const r of st?.read ? st.rows ?? [] : []) if (r.stepId === m.stepId && (!m.workflowId || r.workflowId === m.workflowId)) last = r;
29466
+ return last && last.outcome === "did_nothing" ? { stepName: last.stepName, kind: "did_nothing" } : void 0;
29467
+ }
29468
+ const p = problems.find((q3) => (q3.kind === "did_nothing" || q3.kind === "skipped") && m.label === `${q3.workflowName}: ${q3.stepName}`);
29469
+ return p ? { stepName: p.stepName, kind: p.kind } : void 0;
29470
+ };
29471
+ for (const m of f.missing ?? []) {
29472
+ const own = ownStepLogged(m) ? void 0 : ownStepCharged(m);
29473
+ const cause = own ? { why: `its own step "${own.stepName}" ${own.kind === "skipped" ? "was skipped" : "did nothing"}` } : ownStepLogged(m) ? void 0 : rootCauses.find((c) => sameWorkflow(c, m));
29474
+ if (cause) deductions2.push({ kind: "missing", points: 0, text: `${POINTS.missing.what}: ${m.label}`, area: "Messages", causedBy: `Not charged again: ${cause.why}, already counted above` });
29475
+ else add2("missing", "Messages", `${POINTS.missing.what}: ${m.label}`);
29476
+ }
29477
+ const lost = deductions2.reduce((a, d) => a + d.points, 0);
29478
+ const cap = deductions2.filter((d) => d.cap !== void 0).sort((a, b) => a.cap - b.cap)[0];
29479
+ let score2 = Math.max(0, 100 - lost);
29480
+ let capped;
29481
+ if (cap && score2 > cap.cap) {
29482
+ score2 = cap.cap;
29483
+ capped = { at: cap.cap, reason: cap.text };
29484
+ }
29485
+ const plural2 = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
29486
+ const parts = [];
29487
+ if (st?.read && totalSteps) parts.push(`${judgedSteps} of ${plural2(totalSteps, "step")}`);
29488
+ parts.push(plural2(f.sent, "message"));
29489
+ const gaps = [];
29490
+ if (!st) gaps.push("the workflow steps were not read");
29491
+ if (st && !st.read) gaps.push("the workflow steps could not be read");
29492
+ if (st?.read && totalSteps > judgedSteps) gaps.push(`${plural2(totalSteps - judgedSteps, "step")} could not be judged (unclear or still waiting)`);
29493
+ if (run.planNote?.noPhone) gaps.push(`${plural2(run.planNote.noPhone, "text message")} not checked: no test phone was given`);
29494
+ if (st?.read && totalSteps === 0) gaps.push("no workflow steps were logged for the test lead");
29495
+ if (st?.unreadWorkflows?.length) gaps.push(`${plural2(st.unreadWorkflows.length, "workflow")} not read`);
29496
+ if (st?.partialWorkflows?.length) gaps.push(`${plural2(st.partialWorkflows.length, "workflow")} only partly read`);
29497
+ if (drafts.length) gaps.push(`${plural2(drafts.length, "draft workflow")} ran nothing`);
29498
+ const notDue = (f.notYetDue ?? []).filter((m) => !m.heldByWait).length;
29499
+ if (notDue) gaps.push(`${plural2(notDue, "planned message")} not due yet`);
29500
+ if (run.planNote?.source === "none") gaps.push("no plan known, so a missing message could not be detected");
29501
+ if (run.planNote?.conditional) gaps.push(`${plural2(run.planNote.conditional, "planned message")} that depend on what the lead does (an if/else or a wait for a reply)`);
29502
+ if (run.planNote?.noWording) gaps.push(`${plural2(run.planNote.noWording, "planned message")} whose wording is all merge fields, so they could not be matched`);
29503
+ if (run.planNote?.unread?.length) gaps.push(`the plan of ${plural2(run.planNote.unread.length, "workflow")} could not be read`);
29504
+ const notOpened = f.links?.skipped ?? 0;
29505
+ if (notOpened) gaps.push(`${plural2(notOpened, "link")} not opened`);
29506
+ const coverage = `Checked ${parts.join(" and ")}${gaps.length ? `. Not checked: ${gaps.join("; ")}` : ""}`;
29507
+ const complete = gaps.length === (notOpened ? 1 : 0);
29508
+ const areaNames = [
29509
+ ...judgedSteps || drafts.length ? ["Workflow steps"] : [],
29510
+ // Messages is an area only when a message arrived or a miss was charged: an excused miss alone proves nothing.
29511
+ ...f.sent || deductions2.some((d) => d.area === "Messages" && d.points > 0) ? ["Messages"] : [],
29512
+ ...f.links && f.links.total - f.links.skipped > 0 ? ["Links"] : []
29513
+ ];
29514
+ const areas2 = areaNames.map((label) => ({ label, score: Math.max(0, 100 - deductions2.filter((d) => d.area === label).reduce((a, d) => a + d.points, 0)) }));
29515
+ return {
29516
+ graded: true,
29517
+ score: score2,
29518
+ grade: gradeOf(score2),
29519
+ deductions: deductions2.map(({ area: _a, ...d }) => d),
29520
+ ...capped ? { capped } : {},
29521
+ coverage,
29522
+ complete,
29523
+ areas: areas2
29524
+ };
29525
+ }
29526
+ var POINTS, capText;
29527
+ var init_score = __esm({
29528
+ "src/command-os/shopper/score.ts"() {
29529
+ "use strict";
29530
+ POINTS = {
29531
+ failed_holding: { points: 25, cap: 69, what: "A step failed and held every lead behind it" },
29532
+ failed_gave_up: { points: 20, cap: 69, what: "A step failed and GoHighLevel gave up on it" },
29533
+ wrong_business: { points: 20, cap: 69, what: "A message named another business" },
29534
+ did_nothing: { points: 15, cap: 79, what: "A step did nothing for a new lead" },
29535
+ missing: { points: 15, cap: 79, what: "A planned message was never sent" },
29536
+ code_showing: { points: 10, what: "Template code showed in a message" },
29537
+ blank_field: { points: 8, what: "A message had a blank name or field" },
29538
+ broken_link: { points: 8, what: "A link in a message did not open" },
29539
+ skipped: { points: 8, what: "A step was skipped" },
29540
+ duplicate: { points: 5, what: "The same message was sent twice" }
29541
+ };
29542
+ capText = (cap) => `Caps the score at ${cap} (${gradeOf(cap)})`;
29543
+ }
29544
+ });
29545
+
29425
29546
  // src/command-os/shopper/page.ts
29547
+ function ringHtml(score2, caption, complete = true) {
29548
+ const s = Math.max(0, Math.min(100, Math.round(score2)));
29549
+ const r = 52, c = 2 * Math.PI * r, g = gradeOf(s), k = complete ? `g-${g.toLowerCase()}` : "g-incomplete";
29550
+ return `<div class="ring" role="img" aria-label="Grade ${g}, ${s} out of 100${complete ? "" : ", incomplete check"}. ${esc2(caption)}">${complete ? "" : `<span class="chip warn">Incomplete check</span>`}<svg width="160" height="160" viewBox="0 0 120 120" aria-hidden="true"><circle class="trk" cx="60" cy="60" r="${r}" stroke-width="10"/><circle class="arc ${k}" cx="60" cy="60" r="${r}" stroke-width="10" stroke-dasharray="${c.toFixed(2)}" stroke-dashoffset="${(c * (1 - s / 100)).toFixed(2)}" transform="rotate(-90 60 60)"/><text class="letter ${k}" x="60" y="66" text-anchor="middle" font-size="44">${g}</text><text class="out" x="60" y="86" text-anchor="middle">${s}/100</text></svg><div class="cap">${esc2(caption)}</div></div>`;
29551
+ }
29552
+ function scoreHtml(card, run) {
29553
+ const total = card.deductions.reduce((a, d) => a + d.points, 0);
29554
+ const ded = card.deductions.length ? `<ul class="ded">${card.deductions.map((d) => `<li><span class="pts">${d.points ? `&minus;${d.points}` : "0"}</span><span>${esc2(d.text)}${d.cap !== void 0 ? `<span class="capline">${capText(d.cap)}</span>` : ""}${d.causedBy ? `<span class="causeline">${esc2(d.causedBy)}</span>` : ""}</span></li>`).join("")}<li class="total"><span class="pts">&minus;${total}</span><span>Points lost in total${card.capped ? `, then capped at ${card.capped.at}` : ""}</span></li></ul>` : `<p class="empty">No points lost: every check that ran passed.</p>`;
29555
+ const bars = `<div class="bars">${card.areas.map((a) => {
29556
+ const k = `g-${gradeOf(a.score).toLowerCase()}`;
29557
+ return `<div class="bar-row"><span class="name">${esc2(a.label)}</span><span class="trk"><i class="fill ${k}" style="width:${a.score}%"></i></span><span class="val"><span class="${k}">${gradeOf(a.score)}</span> ${a.score}</span></div>`;
29558
+ }).join("")}</div>
29559
+ <p class="src">From GoHighLevel's execution log and the test lead's inbox, ${when(run.startedAt)}${run.finishedAt ? ` to ${when(run.finishedAt)}` : ""}. Points follow the fixed report card table, the same on every report.</p>`;
29560
+ return `<section><div class="section-head"><div><div class="label">The score</div><h2>How ${card.score} was reached</h2></div><div class="right">${esc2(card.coverage)}</div></div>
29561
+ <div class="grid2"><div class="card">${ded}</div><div class="card"><h3>Score by area</h3>${bars}</div></div></section>`;
29562
+ }
29426
29563
  function stepsHtml(a) {
29427
29564
  if (!a) return "";
29428
29565
  if (!a.read) return `<section><div class="section-head"><div><div class="label">Every step</div><h2>What each workflow step did</h2></div></div><div class="callout">${esc2(a.unreadable ?? "The step log could not be read.")}</div></section>`;
@@ -29461,11 +29598,11 @@ ${p.sentBy ? `<dl class="kv"><dt>Sent by</dt><dd>${esc2(p.sentBy)}</dd></dl>` :
29461
29598
  </article>`;
29462
29599
  }
29463
29600
  function timelineRow(t) {
29464
- const chip = t.problems.length === 0 ? `<span class="chip ok">OK</span>` : t.problems.length === 1 ? `<span class="chip bad">${CHIP[t.problems[0]]}</span>` : `<span class="chip bad">${t.problems.length} problems</span>`;
29601
+ const chip2 = t.problems.length === 0 ? `<span class="chip ok">OK</span>` : t.problems.length === 1 ? `<span class="chip bad">${CHIP[t.problems[0]]}</span>` : `<span class="chip bad">${t.problems.length} problems</span>`;
29465
29602
  const what = t.channel === "email" ? `Email: "${esc2(t.subject ?? t.preview)}"` : `${CHANNEL[t.channel] ?? "Message"}: "${esc2(t.preview)}"`;
29466
- return `<li><span class="t">${mins(t.minute)}</span><span class="what">${what}<small>${esc2(t.sentBy ?? (t.source === "workflow" ? "Sent by a workflow" : t.source ? `Sent by ${t.source}` : ""))}</small></span>${chip}</li>`;
29603
+ return `<li><span class="t">${mins(t.minute)}</span><span class="what">${what}<small>${esc2(t.sentBy ?? (t.source === "workflow" ? "Sent by a workflow" : t.source ? `Sent by ${t.source}` : ""))}</small></span>${chip2}</li>`;
29467
29604
  }
29468
- function resultHtml(run, now) {
29605
+ function resultHtml(run, now, clientName = "") {
29469
29606
  if (run.status === "failed") {
29470
29607
  return `<div class="hero"><div class="kicker">Verify \xB7 Secret shopper</div><h1>The shopper could not run</h1>
29471
29608
  <blockquote>${esc2(run.error ?? "Unknown error")}<cite>${esc2(run.cleanup?.detail ?? "Nothing was created in the account.")}</cite></blockquote></div>`;
@@ -29488,11 +29625,25 @@ ${(() => {
29488
29625
  return opened ? `<div class="stat ${f.links.broken ? "bad" : "ok"}"><div class="big">${f.links.ok} of ${opened}</div><div class="lbl">Links open${f.links.skipped ? ` \xB7 ${f.links.skipped} not opened` : ""}</div></div>` : `<div class="stat"><div class="big">0</div><div class="lbl">Links opened${f.links.skipped ? ` \xB7 ${f.links.skipped} not` : ""}</div></div>`;
29489
29626
  })()}
29490
29627
  </div>` : "";
29491
- const hero = `<div class="hero"><div class="kicker">Verify \xB7 Secret shopper</div><h1>${head3}</h1>
29628
+ const card = scoreRun(run);
29629
+ const shortCoverage = card.graded ? `${card.coverage.split(". Not checked")[0]}${card.coverage.includes(". Not checked") ? ". Some parts not checked, listed below" : ""}` : "";
29630
+ const ring = card.graded ? ringHtml(card.score, card.capped ? `${shortCoverage}. ${capText(card.capped.at)}` : shortCoverage, card.complete) : "";
29631
+ const notGraded = !card.graded && run.status === "done" ? `<div class="callout warn-note"><b>Not graded.</b> ${esc2(card.reason)}</div>` : "";
29632
+ const hero = `<div class="hero"><div class="hero-row"><div class="hero-text"><div class="kicker">Verify \xB7 Secret shopper</div><h1>${head3}</h1>
29492
29633
  <p class="sub">Command OS made a test lead, then read every text and email this account sent it, word for word. ${esc2(run.entryNote)}</p>
29493
- <div class="meta">Started ${when(run.startedAt)} \xB7 Test lead: ${esc2(run.leadEmail)} (your address, not a customer's)${run.leadHasFirstName ? "" : " \xB7 No first name, on purpose"} \xB7 Watching for ${run.watchMinutes} minute${run.watchMinutes === 1 ? "" : "s"}${run.watchReason ? `. ${esc2(run.watchReason)}` : ""}</div>
29634
+ <div class="meta">Started ${when(run.startedAt)} \xB7 Test lead: ${esc2(run.leadEmail)} (your address, not a customer's)${run.leadHasFirstName ? "" : " \xB7 No first name, on purpose"} \xB7 Watching for ${run.watchMinutes} minute${run.watchMinutes === 1 ? "" : "s"}${run.watchReason ? `. ${esc2(run.watchReason)}` : ""}</div></div>${ring ? `<div class="hero-ring">${ring}</div>` : ""}</div>${notGraded}
29494
29635
  ${watching ? `<div class="callout watch" role="status"><b>Still watching.</b> ${minutesLeft ? `About ${minutesLeft} minute${minutesLeft === 1 ? "" : "s"} left; this page updates itself and finishes on its own.` : "Finishing now."} ${f ? "What you see below is a partial look." : ""} <button class="btn ghost" type="button" id="look-now">Look now</button></div>` : ""}
29495
29636
  ${stats}${quote}</div>`;
29637
+ if (run.status === "done" && card.graded && f) {
29638
+ const href = `/shopper/report?client=${encodeURIComponent(run.locationId)}&run=${encodeURIComponent(run.runId)}`;
29639
+ const charged = card.deductions.filter((d) => d.points > 0).length;
29640
+ return `<div class="hero"><div class="hero-row"><div class="hero-text"><div class="kicker">Verify \xB7 Live workflow audit \xB7 ${esc2(clientName)}</div><h1>${charged ? `${charged} ${charged === 1 ? "thing" : "things"} would let a new lead down` : "No problems in what was checked"}</h1>
29641
+ <p class="sub">The report card is ready: the grade, what to fix, how the score was reached, and every step. Open it to read, save as a PDF or hand to your client.</p>
29642
+ <div class="meta">Finished ${when(run.finishedAt ?? run.startedAt)} \xB7 ${esc2(card.coverage.split(". Not checked")[0])}</div>
29643
+ ${run.fastForward && !run.fastForward.removed ? `<div class="callout warn-note"><b>Not cleaned up:</b> temporary copies are still in the account: ${esc2(run.fastForward.copies.map((c) => `"${c.copyName}"`).join(", "))}. Delete them in GoHighLevel.</div>` : ""}${run.cleanup && !run.cleanup.removed ? `<div class="callout warn-note"><b>Test lead:</b> ${esc2(run.cleanup.detail)}</div>` : ""}
29644
+ <p class="report-actions"><a class="btn" href="${href}" target="_blank" rel="noopener">Open the report</a> <a class="btn ghost" href="${href}&amp;print=1" target="_blank" rel="noopener">Save as PDF</a></p></div>
29645
+ <div class="hero-ring">${ringHtml(card.score, card.coverage.includes(". Not checked") ? "Some parts not checked, see the report" : card.coverage, card.complete)}</div></div></div>`;
29646
+ }
29496
29647
  const draftNote = run.draftWorkflows?.length ? `<section><div class="callout" style="background:#FEF2F2;border-color:#F5C2C2"><b>Still a draft, so it did nothing:</b> ${esc2(run.draftWorkflows.join(", "))}. GoHighLevel accepts a lead added to a draft workflow and then runs none of its steps, with no error anywhere. Publish it, then run the shopper again.</div></section>` : "";
29497
29648
  if (!f) return hero + draftNote;
29498
29649
  const fixes = f.problems.length ? `<section><div class="section-head"><div><div class="label">Fix these</div><h2>What a customer would have seen wrong</h2></div><div class="right">${f.problems.length} problem${f.problems.length === 1 ? "" : "s"} \xB7 most serious first</div></div>
@@ -29500,6 +29651,10 @@ ${stats}${quote}</div>`;
29500
29651
  const missingRows = f.missing.map((m) => `<li class="missing"><span class="t">by ${mins(m.byMinute)}</span><span class="what">Planned but never sent: ${esc2(m.label)}<small>Its workflow may be a draft, its step may have failed, or a condition skipped it.</small></span><span class="chip warn">Missing</span></li>`).join("");
29501
29652
  const timeline = `<section><div class="section-head"><div><div class="label">Timeline</div><h2>Everything the lead received, in order</h2></div><div class="right">Minutes after the lead went in</div></div>
29502
29653
  ${f.timeline.length || missingRows ? `<ol class="timeline">${f.timeline.map(timelineRow).join("")}${missingRows}</ol>` : `<p class="empty">Nothing has arrived yet.</p>`}</section>`;
29654
+ const { coverage, cleanup } = coverageParts(run, f);
29655
+ return hero + draftNote + fixes + (card.graded ? scoreHtml(card, run) : "") + stepsHtml(run.steps) + timeline + coverage + cleanup;
29656
+ }
29657
+ function coverageParts(run, f) {
29503
29658
  const plan = run.planNote.source === "workflows" ? `Planned messages were read from the workflows: ${run.expect.length}.${run.planNote.conditional ? ` ${run.planNote.conditional} left out because they depend on what the lead does (an if/else or a wait for a reply).` : ""}${run.planNote.noWording ? ` ${run.planNote.noWording} left out because their wording is all merge fields.` : ""}${run.planNote.unread?.length ? ` ${run.planNote.unread.length} workflow${run.planNote.unread.length === 1 ? "" : "s"} could not be read.` : ""}${run.planNote.noPhone ? ` ${run.planNote.noPhone} text-message step${run.planNote.noPhone === 1 ? " was" : "s were"} not checked: no test phone was given.` : ""}` : run.planNote.source === "given" ? `Planned messages were given for this run: ${run.expect.length}.` : "No plan was known for this run, so nothing is reported as missing.";
29504
29659
  const notYet = [
29505
29660
  ...f.notYetDue.map((m) => m.heldByWait ? `${esc2(m.label)}: GoHighLevel was still holding the test lead at ${esc2(run.heldAt?.map((h) => `"${h.stepName}"`).join(", ") ?? "a wait")} after it should have ended${run.extendedMinutes ? `, even after the check waited ${run.extendedMinutes} extra minutes` : ""}. Not judged; run the check again.` : `${esc2(m.label)}, due after the window closed.`),
@@ -29524,11 +29679,11 @@ ${f.timeline.length || missingRows ? `<ol class="timeline">${f.timeline.map(time
29524
29679
  <details><summary>Not checked (${notYet.length})</summary><ul>${notYet.map((n) => `<li>${n}</li>`).join("")}</ul></details></section>`;
29525
29680
  const ffLine = run.fastForward ? run.fastForward.removed ? " The temporary fast-forward copies were removed." : run.fastForward.problems?.length ? ` ${run.fastForward.problems.join(" ")}` : ` Temporary copies still in the account: ${run.fastForward.copies.map((c) => `"${c.copyName}"`).join(", ")}. The cockpit retries every 30 minutes while it runs; if they are still there tomorrow, delete them in GoHighLevel.` : "";
29526
29681
  const cleanup = run.cleanup ? `<section><div class="callout"><b>${run.cleanup.removed ? "Cleaned up:" : "Test lead:"}</b> ${esc2(run.cleanup.detail + ffLine)} Anything that looked like a password or key in a message was hidden before it reached this page.</div></section>` : "";
29527
- return hero + draftNote + fixes + stepsHtml(run.steps) + timeline + coverage + cleanup;
29682
+ return { coverage, cleanup };
29528
29683
  }
29529
29684
  function startForm(d) {
29530
29685
  if (!d.client) return "";
29531
- return `<section id="start"><div class="section-head"><div><div class="label">New check</div><h2>Run the shopper on ${esc2(d.client.name)}</h2></div><div class="right">Writes one test contact, then removes it</div></div>
29686
+ return `<section id="start"><div class="section-head"><div><div class="label">New check</div><h2>${d.run?.status === "done" ? "Run another check" : "Run the shopper"} on ${esc2(d.client.name)}</h2></div><div class="right">Writes one test contact, then removes it</div></div>
29532
29687
  <form id="start-form" class="startform">
29533
29688
  <fieldset><legend>Workflows the test lead enters</legend><div class="wf" id="wf-list"><p class="empty">Loading this account's workflows\u2026</p></div></fieldset>
29534
29689
  <label for="owner-email">Your email (the test lead uses it, so every message comes to you)</label>
@@ -29549,7 +29704,7 @@ function startForm(d) {
29549
29704
  }
29550
29705
  function shopperPageHtml(d, seatToken, seatHeader) {
29551
29706
  const picker = `<form class="picker" method="get" action="/shopper"><label for="client">Client account</label><select id="client" name="client" onchange="this.form.submit()"><option value="">Choose a client\u2026</option>${d.accounts.map((a) => `<option value="${esc2(a.locationId)}"${d.client?.locationId === a.locationId ? " selected" : ""}>${esc2(a.name)}</option>`).join("")}</select><noscript><button type="submit">Open</button></noscript></form>`;
29552
- const body = !d.client ? `<div class="hero"><div class="kicker">Verify \xB7 Secret shopper</div><h1>See what a new lead actually receives</h1><p class="sub">Pick a client account. Command OS makes a test lead with your own email address, puts it through the workflows you choose, then reads every text and email the account sent it, word for word, and tells you what a real customer would have seen wrong.</p></div>` : (d.run ? resultHtml(d.run, d.now) : `<div class="hero"><div class="kicker">Verify \xB7 Secret shopper</div><h1>No check has run on ${esc2(d.client.name)} yet</h1><p class="sub">Choose the workflows a new lead goes through, usually speed-to-lead and the welcome sequence, then start.</p></div>`) + (d.run?.status === "watching" ? "" : startForm(d));
29707
+ const body = !d.client ? `<div class="hero"><div class="kicker">Verify \xB7 Secret shopper</div><h1>See what a new lead actually receives</h1><p class="sub">Pick a client account. Command OS makes a test lead with your own email address, puts it through the workflows you choose, then reads every text and email the account sent it, word for word, and tells you what a real customer would have seen wrong.</p></div>` : (d.run ? resultHtml(d.run, d.now, d.client.name) : `<div class="hero"><div class="kicker">Verify \xB7 Secret shopper</div><h1>No check has run on ${esc2(d.client.name)} yet</h1><p class="sub">Choose the workflows a new lead goes through, usually speed-to-lead and the welcome sequence, then start.</p></div>`) + (d.run?.status === "watching" ? "" : startForm(d));
29553
29708
  return `${cockpitHeadHtml("Secret shopper \xB7 Command OS", { extraCss: CSS })}${cockpitHeaderHtml("shopper")}
29554
29709
  <main class="shopper">${picker}${body}</main>${cockpitShellClose()}
29555
29710
  <script>
@@ -29617,6 +29772,8 @@ ${COCKPIT_TYPE_JS}
29617
29772
  }).catch(function(){usersLoaded=false;alertTo.innerHTML='<option value="">The users could not be read. Your own login is used if your email matches one.</option>'});
29618
29773
  }
29619
29774
  if(ff)ff.addEventListener("change",refreshAlertTo);
29775
+ window.addEventListener("beforeprint",function(){document.querySelectorAll("main.shopper details").forEach(function(d){d.setAttribute("data-was-open",d.open?"1":"0");d.open=true})});
29776
+ window.addEventListener("afterprint",function(){document.querySelectorAll("main.shopper details[data-was-open]").forEach(function(d){d.open=d.getAttribute("data-was-open")==="1"})});
29620
29777
  var look=document.getElementById("look-now");
29621
29778
  if(look&&run){look.addEventListener("click",function(){look.disabled=true;post("/api/shopper/finish",{client:client,runId:run.runId}).then(function(){location.reload()}).catch(function(){look.disabled=false})})}
29622
29779
  if(run&&run.status==="watching"){setTimeout(function(){location.reload()},30000)}
@@ -29629,6 +29786,7 @@ var init_page = __esm({
29629
29786
  "use strict";
29630
29787
  init_cockpit_shell();
29631
29788
  init_script_json();
29789
+ init_score();
29632
29790
  OUTCOME_CHIP = {
29633
29791
  ran: `<span class="chip ok">Ran</span>`,
29634
29792
  branch: `<span class="chip ch">Path taken</span>`,
@@ -29724,7 +29882,72 @@ main.shopper{--sh-bad:#B91C1C;--sh-ok:#15803D;--sh-warn:#B45309;--sh-accent-ink:
29724
29882
  .shopper .err{color:var(--sh-bad)}
29725
29883
  .shopper .result:empty{display:none}.shopper .result{border:1px solid var(--sh-line);border-radius:10px;padding:.6rem .8rem}
29726
29884
  .shopper .empty{color:var(--sh-muted)}
29885
+ .shopper .hero-row{display:flex;gap:2rem;align-items:center;justify-content:space-between;flex-wrap:wrap}
29886
+ .shopper .hero-text{flex:1 1 18rem;min-width:0}
29887
+ .shopper .hero-ring{flex:none;background:var(--sh-surface);border:1px solid var(--sh-line);border-radius:20px;padding:18px 22px;box-shadow:0 1px 2px rgba(10,22,40,.06),0 8px 24px rgba(10,22,40,.08)}
29888
+ .shopper{--g-a:#15803D;--g-b:#0E7490;--g-c:#B45309;--g-d:#C2410C;--g-f:#B91C1C}
29889
+ .shopper .g-a{stroke:var(--g-a);fill:var(--g-a);color:var(--g-a);background:var(--g-a)}.shopper .g-b{stroke:var(--g-b);fill:var(--g-b);color:var(--g-b);background:var(--g-b)}.shopper .g-c{stroke:var(--g-c);fill:var(--g-c);color:var(--g-c);background:var(--g-c)}.shopper .g-d{stroke:var(--g-d);fill:var(--g-d);color:var(--g-d);background:var(--g-d)}.shopper .g-f{stroke:var(--g-f);fill:var(--g-f);color:var(--g-f);background:var(--g-f)}
29890
+ .shopper .ring{display:grid;justify-items:center;gap:8px}
29891
+ .shopper .g-incomplete{stroke:var(--sh-muted);fill:var(--sh-muted);color:var(--sh-muted)}
29892
+ .shopper .ring svg{display:block}.shopper .ring .trk{fill:none;stroke:var(--sh-line)}.shopper .ring .arc{fill:none;stroke-linecap:round;background:none}
29893
+ .shopper .ring text.letter{font-weight:700;stroke:none;background:none}.shopper .ring .out{fill:var(--sh-muted);font-size:11px}
29894
+ .shopper .ring .cap{font-size:14px;color:var(--sh-muted);text-align:center;max-width:14rem;line-height:1.4}
29895
+ .shopper .warn-note{margin-top:1rem;background:#FFF8EB;border-color:#F3D3A8}
29896
+ .shopper .grid2{display:grid;grid-template-columns:repeat(auto-fit,minmax(20rem,1fr));gap:14px;align-items:start}
29897
+ .shopper .ded{list-style:none;margin:0;padding:0}
29898
+ .shopper .ded li{display:grid;grid-template-columns:3.5rem 1fr;gap:12px;padding:10px 0;border-top:1px solid var(--sh-line);font-size:15px}
29899
+ .shopper .ded li:first-child{border-top:0}
29900
+ .shopper .ded .pts{font-weight:700;color:var(--sh-bad);font-variant-numeric:tabular-nums}
29901
+ .shopper .ded .capline{display:block;font-size:12px;color:var(--sh-warn);font-weight:600;margin-top:2px}
29902
+ .shopper .ded .total{font-weight:700}
29903
+ .shopper .ded .causeline{display:block;font-size:12px;color:var(--sh-muted);margin-top:2px}
29904
+ .shopper .bars{display:grid;gap:12px;margin-top:.6rem}
29905
+ .shopper .bar-row{display:grid;grid-template-columns:minmax(7rem,11rem) 1fr 4rem;gap:12px;align-items:center;font-size:15px}
29906
+ .shopper .bar-row .trk{height:8px;border-radius:999px;background:var(--sh-line);overflow:hidden}
29907
+ .shopper .bar-row .fill{display:block;height:100%;border-radius:999px}
29908
+ .shopper .bar-row .val{text-align:right;font-weight:700;font-variant-numeric:tabular-nums}
29909
+ .shopper .bar-row .val span{background:none}
29910
+ .shopper .src{font-size:13px;color:var(--sh-muted);margin:.8rem 0 0}
29911
+ .shopper .report-actions{display:flex;gap:10px;flex-wrap:wrap;margin:1rem 0 0}
29912
+ .shopper .stats.four{grid-template-columns:repeat(auto-fit,minmax(8rem,1fr))}
29913
+ .shopper .fixcard{gap:8px}
29914
+ .shopper .fixhead{display:flex;justify-content:space-between;gap:10px;align-items:flex-start}
29915
+ .shopper .fixhead h3{font-size:1.05rem}
29916
+ .shopper .where{font-size:14px;color:var(--sh-muted)}
29917
+ .shopper .diff{display:grid;grid-template-columns:1fr 1fr;gap:10px}
29918
+ @media (max-width:40rem){.shopper .diff{grid-template-columns:1fr}}
29919
+ .shopper .diff>div{background:var(--sh-quote);border-radius:12px;padding:10px 12px;font-size:15px;line-height:1.5;overflow-wrap:anywhere}
29920
+ .shopper .diff .k{display:block;font-size:11px;letter-spacing:.12em;text-transform:uppercase;font-weight:600;margin-bottom:4px}
29921
+ .shopper .diff .was .k{color:var(--sh-bad)}.shopper .diff .now .k{color:var(--sh-ok)}
29922
+ .shopper .detailgrid{margin-top:14px;display:grid;grid-template-columns:minmax(0,7fr) minmax(0,5fr);gap:14px;align-items:start}
29923
+ @media (max-width:52rem){.shopper .detailgrid{grid-template-columns:1fr}}
29924
+ .shopper .stack{display:grid;gap:14px}
29925
+ .shopper .flowtitle{font-size:1rem;margin-bottom:.4rem}
29926
+ .shopper .flow{list-style:none;margin:0;padding:0}
29927
+ .shopper .flow li{display:grid;grid-template-columns:22px 1fr auto;gap:12px;align-items:center;padding:8px 6px;position:relative}
29928
+ .shopper .flow li:not(:last-child)::after{content:"";position:absolute;left:16px;top:28px;bottom:-8px;width:2px;background:var(--sh-line)}
29929
+ .shopper .flow li.hit{background:var(--sh-quote);border-radius:10px}
29930
+ .shopper .flow .pin{width:14px;height:14px;border-radius:50%;border:2px solid var(--sh-line);background:var(--sh-surface);position:relative;z-index:1;margin-left:4px}
29931
+ .shopper .flow .pin.ok{border-color:var(--sh-ok);background:var(--sh-ok)}.shopper .flow .pin.warn{border-color:var(--sh-warn)}.shopper .flow .pin.bad{border-color:var(--sh-bad);background:var(--sh-bad)}.shopper .flow .pin.wait{border-style:dashed;border-color:var(--sh-muted)}
29932
+ .shopper .flow .nm{font-size:15px}.shopper .flow .nm small{display:block;font-size:13px;color:var(--sh-muted)}
29933
+ .shopper .timeline.flat{border:0;background:none}
29934
+ .shopper .timeline.flat li{grid-template-columns:4rem 1fr auto;padding:10px 0}
29727
29935
  [hidden]{display:none!important}
29936
+ /* Print and PDF (design system guidelines/30-print.md): whole cards, headings with their content, no split paragraphs, US Letter. */
29937
+ @page{size:letter;margin:.6in .6in .7in}
29938
+ @media print{
29939
+ body{background:#fff}
29940
+ header,nav,.picker,#start,.btn,#look-now,.shopper .callout.watch{display:none!important}
29941
+ main.shopper{max-width:none;margin:0;padding:0;background:#fff;border-radius:0;font-size:12pt}
29942
+ .shopper,.shopper *{-webkit-print-color-adjust:exact;print-color-adjust:exact}
29943
+ .shopper .flow li,.shopper .diff>div,.shopper .card,.shopper .stat,.shopper .hero-ring,.shopper blockquote,.shopper .timeline li,.shopper .ded li,.shopper .bar-row,.shopper .callout,.shopper .keep,.shopper .checks li{break-inside:avoid;page-break-inside:avoid}
29944
+ .shopper h1,.shopper h2,.shopper h3,.shopper .section-head,.shopper .label,.shopper .kicker,.shopper summary{break-after:avoid;page-break-after:avoid}
29945
+ .shopper p,.shopper li,.shopper dd{orphans:3;widows:3;break-inside:avoid;page-break-inside:avoid}
29946
+ .shopper section{padding:1.2rem 0}
29947
+ .shopper .hero-row{flex-wrap:nowrap;gap:1.2rem}
29948
+ .shopper .hero-ring{box-shadow:none;padding:12px 14px}
29949
+ .shopper .ring svg{width:130px;height:130px}
29950
+ }
29728
29951
  `;
29729
29952
  }
29730
29953
  });
@@ -30098,6 +30321,7 @@ function judgeSteps(rawRows, contactId, workflowNames) {
30098
30321
  problems.push({
30099
30322
  _k: stepKey,
30100
30323
  kind: latest.outcome === "failed" ? latest.gaveUp ? "failed_gave_up" : "failed_holding" : knownSilent ? "did_nothing" : "skipped",
30324
+ workflowId: latest.workflowId,
30101
30325
  workflowName: latest.workflowName,
30102
30326
  stepName: latest.stepName,
30103
30327
  detail: t.detail,
@@ -30760,6 +30984,7 @@ async function startShopper(input, deps) {
30760
30984
  planNote,
30761
30985
  workflowNames: input.workflowNames,
30762
30986
  draftWorkflows: input.draftWorkflows?.length ? input.draftWorkflows : void 0,
30987
+ draftWorkflowIds: input.draftWorkflowIds?.length ? input.draftWorkflowIds : void 0,
30763
30988
  fastForward: input.fastForward?.length ? { copies: input.fastForward } : void 0,
30764
30989
  keepLead: Boolean(input.keepLead),
30765
30990
  watchReason,
@@ -30969,16 +31194,19 @@ function summarizeRun(run) {
30969
31194
  const heldNote = run.heldAt?.length && f?.notYetDue.some((m) => m.heldByWait) ? ` GoHighLevel was still holding the test lead at a wait that should have ended, so ${f.notYetDue.filter((m) => m.heldByWait).length} later message(s) were not judged; run the check again.` : "";
30970
31195
  const stepNote = heldNote + (st?.read && st.problems.length ? ` ${st.problems.length} workflow step${st.problems.length === 1 ? "" : "s"} failed, skipped or did nothing.` : "") + (st && !st.read ? " The workflow steps could not be read, so only the messages were checked." : "") + (st?.read && st.rows.length === 0 ? " No workflow steps were logged for the test lead, so the steps are not checked." : "") + (st?.unreadWorkflows?.length ? ` Could not read the steps of: ${st.unreadWorkflows.join(", ")}.` : "") + (st?.partialWorkflows?.length ? ` Only partly read (very busy): ${st.partialWorkflows.join(", ")}.` : "") + (st?.counts.unclear ? ` ${st.counts.unclear} step${st.counts.unclear === 1 ? " has" : "s have"} a status this version cannot read.` : "") + (run.draftWorkflows?.length ? ` ${run.draftWorkflows.length === 1 ? "A chosen workflow is" : `${run.draftWorkflows.length} chosen workflows are`} still a draft, so GoHighLevel took the lead and ran nothing: ${run.draftWorkflows.join(", ")}.` : "");
30971
31196
  const draftNow = run.draftWorkflows?.length ? ` Warning: ${run.draftWorkflows.join(", ")} ${run.draftWorkflows.length === 1 ? "is" : "are"} still a draft, so GoHighLevel will take the lead and run nothing there.` : "";
31197
+ const card = run.status === "done" ? scoreRun(run) : void 0;
31198
+ const gradeLine = !card ? "" : card.graded ? `Grade ${card.grade} (${card.score}/100${card.capped ? `, capped at ${card.capped.at}` : ""})${card.complete ? "" : ", incomplete check"}. ${card.coverage}. ` : `Not graded: ${card.reason} `;
30972
31199
  if (!f) return `Watching: the test lead went in at ${new Date(run.startedAt).toISOString()}. The window closes at ${new Date(run.closesAt).toISOString()}.${draftNow}`;
30973
31200
  const n = f.problems.length;
30974
31201
  if (f.sent === 0) {
30975
31202
  const fired = run.steps?.read && run.steps.rows.some((r) => ["entered", "ran", "failed", "waiting", "branch"].includes(r.outcome));
30976
31203
  const why = fired ? "The workflows did run: see the steps below." : "The workflows may not have fired, may be drafts, or send later than the window.";
30977
31204
  const nothing = `Nothing arrived in ${run.status === "watching" ? "the time so far" : `${run.watchMinutes} minutes`}. ${why}${stepNote}`;
30978
- return run.status === "watching" ? `So far: ${nothing} Still watching until ${new Date(run.closesAt).toISOString()}.` : nothing;
31205
+ return run.status === "watching" ? `So far: ${nothing} Still watching until ${new Date(run.closesAt).toISOString()}.` : gradeLine + nothing;
30979
31206
  }
30980
31207
  const head3 = `${f.sent} message${f.sent === 1 ? "" : "s"} sent, ${n === 0 ? "no problems a customer would notice" : `${n} problem${n === 1 ? "" : "s"} a customer would notice`}${f.missing.length ? `, ${f.missing.length} planned message${f.missing.length === 1 ? "" : "s"} never sent` : ""}.`;
30981
- return run.status === "watching" ? `So far: ${head3}${stepNote} Still watching until ${new Date(run.closesAt).toISOString()}.` : head3 + stepNote;
31208
+ if (run.status === "watching") return `So far: ${head3}${stepNote} Still watching until ${new Date(run.closesAt).toISOString()}.`;
31209
+ return gradeLine + head3 + stepNote;
30982
31210
  }
30983
31211
  var import_node_fs3, import_node_path3, import_node_crypto8, MAX_EXTEND_MINUTES, asObj4, asArr3, str5, SAFE_ID, notFound;
30984
31212
  var init_run = __esm({
@@ -30992,6 +31220,7 @@ var init_run = __esm({
30992
31220
  init_links();
30993
31221
  init_mask();
30994
31222
  init_account_rules();
31223
+ init_score();
30995
31224
  init_steps();
30996
31225
  MAX_EXTEND_MINUTES = 10;
30997
31226
  asObj4 = (v) => v && typeof v === "object" ? v : {};
@@ -31005,6 +31234,264 @@ var init_run = __esm({
31005
31234
  }
31006
31235
  });
31007
31236
 
31237
+ // src/command-os/shopper/report-css.generated.ts
31238
+ var DS_TOKENS_CSS, DS_BUNDLE_CSS;
31239
+ var init_report_css_generated = __esm({
31240
+ "src/command-os/shopper/report-css.generated.ts"() {
31241
+ "use strict";
31242
+ DS_TOKENS_CSS = `:root, [data-theme="light"] {
31243
+ --bg: #f6f7f9; --surface: #ffffff; --surface-sunk: #f1f5f9; --text: #0f172a; --muted: #5b6b7f; --border: #e3e8ef;
31244
+ --primary: #0a1628; --on-primary: #ffffff; --accent: #06b6d4; --accent-ink: #0e7490;
31245
+ --ok: #15803d; --warn: #b45309; --bad: #b91c1c;
31246
+ --grade-a: #15803d; --grade-b: #0e7490; --grade-c: #b45309; --grade-d: #c2410c; --grade-f: #b91c1c;
31247
+ --focus: #0e7490;
31248
+ --shadow-lift: 0 1px 2px rgba(10,22,40,0.06), 0 8px 24px rgba(10,22,40,0.08);
31249
+ }
31250
+ [data-theme="dark"] {
31251
+ --bg: #0b1220; --surface: #111a2b; --surface-sunk: #0e1626; --text: #e6edf5; --muted: #94a3b8; --border: #22304a;
31252
+ --primary: #e6edf5; --on-primary: #0a1628; --accent: #22d3ee; --accent-ink: #67e8f9;
31253
+ --ok: #4ade80; --warn: #fbbf24; --bad: #f87171;
31254
+ --grade-a: #4ade80; --grade-b: #22d3ee; --grade-c: #fbbf24; --grade-d: #fb923c; --grade-f: #f87171;
31255
+ --focus: #67e8f9;
31256
+ --shadow-lift: 0 1px 2px rgba(0,0,0,0.4), 0 8px 24px rgba(0,0,0,0.35);
31257
+ }
31258
+ :root {
31259
+ --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px; --space-5: 22px; --space-6: 28px; --space-7: 44px; --space-8: 56px;
31260
+ --radius-sm: 8px; --radius-md: 12px; --radius-lg: 14px; --radius-xl: 20px; --radius-pill: 9999px;
31261
+ --font-display: -apple-system, 'SF Pro Display', 'Helvetica Neue', Arial, sans-serif;
31262
+ --font-body: -apple-system, 'SF Pro Text', 'Helvetica Neue', Arial, sans-serif;
31263
+ }
31264
+ `;
31265
+ DS_BUNDLE_CSS = '.ao-row{display:flex;flex-wrap:wrap;gap:var(--space-3);align-items:center}\n.ao-stack{display:grid;gap:var(--space-3)}\n.ao-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:var(--space-4)}\n.ao-num{font-variant-numeric:tabular-nums}\n.ao-ring{display:inline-grid;justify-items:center;gap:var(--space-2);font-family:var(--font-display)}\n.ao-ring svg{display:block}\n.ao-ring .trk{fill:none;stroke:var(--border)}\n.ao-ring .arc{fill:none;stroke-linecap:round;transition:stroke-dashoffset 300ms}\n.ao-ring .letter{font-weight:700;letter-spacing:-0.02em}\n.ao-ring .out{fill:var(--muted);font-size:11px;font-variant-numeric:tabular-nums}\n.ao-ring .cap{font-family:var(--font-body);font-size:14px;color:var(--muted);text-align:center;max-width:220px}\n.ao-g-a{stroke:var(--grade-a);fill:var(--grade-a);color:var(--grade-a);background-color:var(--grade-a)}\n.ao-g-b{stroke:var(--grade-b);fill:var(--grade-b);color:var(--grade-b);background-color:var(--grade-b)}\n.ao-g-c{stroke:var(--grade-c);fill:var(--grade-c);color:var(--grade-c);background-color:var(--grade-c)}\n.ao-g-d{stroke:var(--grade-d);fill:var(--grade-d);color:var(--grade-d);background-color:var(--grade-d)}\n.ao-g-f{stroke:var(--grade-f);fill:var(--grade-f);color:var(--grade-f);background-color:var(--grade-f)}\n.ao-g-incomplete{stroke:var(--muted);fill:var(--muted);color:var(--muted)}\n.ao-ring .ao-g-a,.ao-ring .ao-g-b,.ao-ring .ao-g-c,.ao-ring .ao-g-d,.ao-ring .ao-g-f{background-color:transparent}\n.ao-ring text.ao-g-a,.ao-ring text.ao-g-b,.ao-ring text.ao-g-c,.ao-ring text.ao-g-d,.ao-ring text.ao-g-f{stroke:none}\n.ao-stat{background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-lg);padding:14px var(--space-4);min-width:140px}\n.ao-stat .big{font-family:var(--font-display);font-weight:700;font-size:20px;line-height:26px;color:var(--text);font-variant-numeric:tabular-nums}\n.ao-stat .big.bad{color:var(--bad)}.ao-stat .big.warn{color:var(--warn)}.ao-stat .big.ok{color:var(--ok)}\n.ao-stat .lbl{font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);margin-top:var(--space-1);font-weight:600}\n.ao-chip{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:var(--radius-pill);font-size:12px;font-weight:600;border:1px solid var(--border);color:var(--muted);background:var(--surface);font-family:var(--font-body)}\n.ao-chip.ok{color:var(--ok);border-color:var(--ok)}.ao-chip.warn{color:var(--warn);border-color:var(--warn)}.ao-chip.bad{color:var(--bad);border-color:var(--bad)}.ao-chip.info{color:var(--accent-ink);border-color:var(--accent)}\n.ao-chip.locked{border-style:dashed}\n.ao-bars{display:grid;gap:var(--space-3);font-family:var(--font-body)}\n.ao-bar-row{display:grid;grid-template-columns:minmax(120px,200px) 1fr 64px;gap:var(--space-3);align-items:center}\n.ao-bar-row .name{font-size:14px;color:var(--text)}\n.ao-bar-row .trk{height:8px;border-radius:var(--radius-pill);background:var(--border);overflow:hidden}\n.ao-bar-row .fill{display:block;height:100%;border-radius:var(--radius-pill);transition:width 300ms}\n.ao-bar-row .val{font-size:14px;font-weight:700;text-align:right;font-variant-numeric:tabular-nums;font-family:var(--font-display)}\n.ao-bar-row .val .ao-g-a,.ao-bar-row .val .ao-g-b,.ao-bar-row .val .ao-g-c,.ao-bar-row .val .ao-g-d,.ao-bar-row .val .ao-g-f{background-color:transparent}\n.ao-src{font-size:12px;color:var(--muted);margin-top:var(--space-2);font-family:var(--font-body)}\n@media (max-width:520px){.ao-bar-row{grid-template-columns:1fr 56px}.ao-bar-row .trk{grid-column:1 / -1;grid-row:2}}\n.ao-ded{list-style:none;margin:0;padding:0;font-family:var(--font-body)}\n.ao-ded li{display:grid;grid-template-columns:56px 1fr;gap:var(--space-3);padding:10px 0;border-top:1px solid var(--border);font-size:15px;color:var(--text)}\n.ao-ded li:first-child{border-top:0}\n.ao-ded .pts{font-family:var(--font-display);font-weight:700;color:var(--bad);font-variant-numeric:tabular-nums}\n.ao-ded .cap{display:block;font-size:12px;color:var(--warn);font-weight:600;margin-top:2px}\n.ao-ded .total{font-weight:700}\n.ao-ded .note{display:block;font-size:12px;color:var(--muted);margin-top:2px}\n.ao-ded li.excused .pts{color:var(--muted)}\n.ao-ba{display:flex;align-items:center;gap:var(--space-5);flex-wrap:wrap}\n.ao-ba .arrow{font-size:24px;color:var(--muted)}\n.ao-ba .lbl{font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:600;text-align:center;margin-bottom:var(--space-2)}\n.ao-tl{list-style:none;margin:0;padding:0;font-family:var(--font-body)}\n.ao-tl li{display:grid;grid-template-columns:64px 14px 1fr auto;gap:var(--space-3);align-items:start;padding:10px 0}\n.ao-tl .t{font-size:13px;color:var(--muted);font-variant-numeric:tabular-nums;padding-top:2px}\n.ao-tl .dot{width:10px;height:10px;border-radius:var(--radius-pill);margin-top:6px;background:var(--border)}\n.ao-tl .dot.ok{background:var(--ok)}.ao-tl .dot.warn{background:var(--warn)}.ao-tl .dot.bad{background:var(--bad)}\n.ao-tl .what{font-size:15px;color:var(--text)}.ao-tl .what small{display:block;font-size:13px;color:var(--muted)}\n.ao-flow{display:grid;gap:0;font-family:var(--font-body)}\n.ao-node{display:grid;grid-template-columns:22px 1fr auto;gap:var(--space-3);align-items:center;position:relative;padding:8px 0}\n.ao-node .pin{width:14px;height:14px;border-radius:var(--radius-pill);border:2px solid var(--border);background:var(--surface);margin-left:4px;position:relative;z-index:1}\n.ao-node .pin.ok{border-color:var(--ok);background:var(--ok)}.ao-node .pin.warn{border-color:var(--warn);background:var(--surface)}.ao-node .pin.bad{border-color:var(--bad);background:var(--bad)}.ao-node .pin.wait{border-style:dashed;border-color:var(--muted)}\n.ao-node:not(:last-child)::after{content:"";position:absolute;left:10px;top:26px;bottom:-8px;width:2px;background:var(--border)}\n.ao-node .nm{font-size:15px;color:var(--text)}.ao-node .nm small{display:block;font-size:12px;color:var(--muted)}\n.ao-node.hit{background:var(--surface-sunk);border-radius:var(--radius-md)}\n.ao-fix{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:var(--space-4);display:grid;gap:var(--space-3);font-family:var(--font-body)}\n.ao-fix h4{margin:0;font-family:var(--font-display);font-size:18px;color:var(--text)}\n.ao-fix p{margin:0;font-size:15px;color:var(--muted)}\n.ao-diff{display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)}\n@media (max-width:520px){.ao-diff{grid-template-columns:1fr}}\n.ao-diff div{background:var(--surface-sunk);border-radius:var(--radius-md);padding:10px 12px;font-size:14px;color:var(--text)}\n.ao-diff .k{display:block;font-size:11px;letter-spacing:.12em;text-transform:uppercase;font-weight:600;margin-bottom:4px}\n.ao-diff .was .k{color:var(--bad)}.ao-diff .now .k{color:var(--ok)}\n.ao-callout{border:1px solid var(--border);background:var(--surface);border-radius:var(--radius-lg);padding:var(--space-4);font-family:var(--font-body);color:var(--text)}\n.ao-callout.quote{font-family:var(--font-display);font-size:20px;line-height:1.4;font-weight:600}\n.ao-callout.quote cite{display:block;font-family:var(--font-body);font-size:14px;font-weight:400;color:var(--muted);font-style:normal;margin-top:var(--space-2)}\n.ao-callout.bad{border-color:var(--bad)}.ao-callout.warn{border-color:var(--warn)}.ao-callout.info{border-color:var(--accent)}\n.ao-callout .ttl{display:block;font-weight:700;margin-bottom:4px}\n.ao-btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:var(--radius-pill);background:var(--primary);color:var(--on-primary);border:1px solid var(--primary);font-family:var(--font-body);font-weight:600;font-size:14px;cursor:pointer}\n.ao-btn.ghost{background:transparent;color:var(--text);border-color:var(--border)}\n.ao-btn:disabled{opacity:.55;cursor:default}\n.ao-btn:focus-visible,.ao-chip:focus-visible{outline:2px solid var(--focus);outline-offset:2px}\n.ao-sample{font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:600}\n@media (prefers-reduced-motion:reduce){.ao-ring .arc,.ao-bar-row .fill{transition:none}}\n.ao-matrix-wrap{overflow-x:auto}\n.ao-matrix{width:100%;border-collapse:collapse;font-family:var(--font-body);font-size:14px;color:var(--text)}\n.ao-matrix th,.ao-matrix td{padding:9px 10px;border-bottom:1px solid var(--border);text-align:center;vertical-align:middle}\n.ao-matrix thead th{font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:600;border-bottom:1px solid var(--text)}\n.ao-matrix tbody th{text-align:left;font-weight:600}\n.ao-matrix tbody th small{display:block;font-weight:400;font-size:12px;color:var(--muted)}\n.ao-matrix .hl{background:var(--surface-sunk)}\n.ao-matrix thead th.hl{color:var(--text)}\n.ao-yes{color:var(--ok);font-weight:700}.ao-no{color:var(--bad);font-weight:700}.ao-unk{color:var(--muted)}\n/* Print: whole cards, headings stay with their content, no split paragraphs (guidelines/30-print.md) */\n.ao-keep{break-inside:avoid;page-break-inside:avoid}\n.ao-keep-next{break-after:avoid;page-break-after:avoid}\n.ao-page-break{break-before:page;page-break-before:always}\n@media print{\n .ao-fix > .ao-row:last-child{display:none!important}\n .ao-card,.ao-stat,.ao-fix,.ao-callout,.ao-ring,.ao-ba,.ao-bar-row,.ao-ded li,.ao-tl li,.ao-node,.ao-diff div,.ao-keep,table,tr,figure,blockquote{break-inside:avoid;page-break-inside:avoid}\n h1,h2,h3,h4,h5,.ao-keep-next,.label{break-after:avoid;page-break-after:avoid}\n p,li,dd{orphans:3;widows:3}\n .ao-no-print,nav,.ao-btn{display:none!important}\n .ao-ring .arc,.ao-bar-row .fill{transition:none}\n body{background:var(--surface);-webkit-print-color-adjust:exact;print-color-adjust:exact}\n}\n@page{size:letter;margin:0.6in 0.6in 0.7in}\n';
31266
+ }
31267
+ });
31268
+
31269
+ // src/command-os/shopper/report-page.ts
31270
+ function gradeRing(score2, caption, complete, size = 176) {
31271
+ const s = Math.max(0, Math.min(100, Math.round(score2)));
31272
+ const r = 52, c = 2 * Math.PI * r, g = gradeOf(s), k = complete ? gcls(s) : "ao-g-incomplete";
31273
+ return `<div class="ao-ring" role="img" aria-label="Grade ${g}, ${s} out of 100${complete ? "" : ", incomplete check"}. ${esc3(caption)}">${complete ? "" : chip("warn", "Incomplete check")}<svg width="${size}" height="${size}" viewBox="0 0 120 120"><circle class="trk" cx="60" cy="60" r="${r}" stroke-width="10"></circle><circle class="arc ${k}" cx="60" cy="60" r="${r}" stroke-width="10" stroke-dasharray="${c.toFixed(2)}" stroke-dashoffset="${(c * (1 - s / 100)).toFixed(2)}" transform="rotate(-90 60 60)"></circle><text class="letter ${k}" x="60" y="66" text-anchor="middle" font-size="${size < 120 ? 30 : 44}">${g}</text><text class="out" x="60" y="86" text-anchor="middle">${s}/100</text></svg>${caption ? `<div class="cap">${esc3(caption)}</div>` : ""}</div>`;
31274
+ }
31275
+ function scoreBars(items, source) {
31276
+ return `<div><div class="ao-bars">${items.map((it) => {
31277
+ const k = gcls(it.score);
31278
+ return `<div class="ao-bar-row"><span class="name">${esc3(it.label)}</span><span class="trk"><i class="fill ${k}" style="width:${it.score}%"></i></span><span class="val"><span class="${k}">${gradeOf(it.score)}</span> ${it.score}</span></div>`;
31279
+ }).join("")}</div><div class="ao-src">${esc3(source)}</div></div>`;
31280
+ }
31281
+ function deductions(items, capped) {
31282
+ const total = items.reduce((a, d) => a + d.points, 0);
31283
+ return `<ul class="ao-ded">${items.map((d) => `<li class="${d.points ? "" : "excused"}"><span class="pts">${d.points ? `&minus;${d.points}` : "0"}</span><span>${esc3(d.text)}${d.cap !== void 0 && d.points ? `<span class="cap">${esc3(capText(d.cap))}</span>` : ""}${d.causedBy ? `<span class="note">${esc3(d.causedBy)}</span>` : ""}</span></li>`).join("")}<li class="total"><span class="pts">&minus;${total}</span><span>Points lost in total${capped !== void 0 ? `, then capped at ${capped}` : ""}</span></li></ul>`;
31284
+ }
31285
+ function workflowFlow(steps) {
31286
+ return `<div class="ao-flow">${steps.map((r) => {
31287
+ const [tone, word] = r.gaveUp ? ["bad", "Failed, gave up"] : OUTCOME[r.outcome] ?? ["", "Unknown"];
31288
+ return `<div class="ao-node ${tone === "bad" || tone === "warn" ? "hit" : ""}"><span class="pin ${tone}"></span><span class="nm">${esc3(r.stepName)}${r.detail ? `<small>${esc3(r.detail)}</small>` : ""}</span>${chip(tone === "wait" ? "" : tone, word)}</div>`;
31289
+ }).join("")}</div>`;
31290
+ }
31291
+ function fixCard(title2, points, detail, now, after) {
31292
+ return `<div class="ao-fix"><div class="ao-row fixhead"><h4>${esc3(title2)}</h4>${chip("bad", `\u2212${points} points`)}</div>${detail ? `<p>${esc3(detail)}</p>` : ""}<div class="ao-diff"><div class="was"><span class="k">Now</span>${now}</div><div class="now"><span class="k">After the fix</span>${esc3(after)}</div></div><div class="ao-row"><button class="ao-btn" type="button" disabled>Apply this fix</button><button class="ao-btn ghost" type="button" disabled>Not now</button></div><p class="rp-soon ao-no-print">One-click fixes arrive with the next update.</p></div>`;
31293
+ }
31294
+ function beforeAfter(before, after, complete) {
31295
+ return `<div class="ao-ba"><div><div class="lbl">Before fixes</div>${gradeRing(before, "", complete, 112)}</div><span class="arrow" aria-hidden="true">\u2192</span><div><div class="lbl">After fixes</div>${gradeRing(after, "", complete, 112)}</div></div>`;
31296
+ }
31297
+ function marked2(quote, highlight) {
31298
+ const i = highlight ? quote.indexOf(highlight) : -1;
31299
+ return i < 0 ? esc3(quote) : `${esc3(quote.slice(0, i))}<mark>${esc3(highlight)}</mark>${esc3(quote.slice(i + highlight.length))}`;
31300
+ }
31301
+ function reportDocumentHtml(run, card, opts) {
31302
+ const brand = opts.branding ?? DEFAULT_BRANDING;
31303
+ const f = run.findings;
31304
+ const st = run.steps;
31305
+ const stepProblems = st?.read ? st.problems : [];
31306
+ const charged = card.deductions.filter((d) => d.points > 0);
31307
+ const byWf = /* @__PURE__ */ new Map();
31308
+ for (const r of st?.read ? st.rows : []) {
31309
+ if (r.outcome === "entered" || r.outcome === "finished") continue;
31310
+ const w = byWf.get(r.workflowId) ?? { name: r.workflowName, steps: /* @__PURE__ */ new Map() };
31311
+ w.steps.set(r.stepId ?? r.stepName, r);
31312
+ byWf.set(r.workflowId, w);
31313
+ }
31314
+ const allSteps = [...byWf.values()].flatMap((w) => [...w.steps.values()]);
31315
+ const judged = allSteps.filter((r) => r.outcome !== "unclear" && r.outcome !== "waiting").length;
31316
+ const wholeLog = !!st?.read && allSteps.length > 0 && !st.unreadWorkflows?.length && !st.partialWorkflows?.length;
31317
+ const names = [...new Set(run.fastForward?.copies.length ? run.fastForward.copies.map((c) => c.sourceName) : Object.values(run.workflowNames ?? {}))];
31318
+ const subject = names.length === 1 ? `"${names[0]}"` : names.length ? `these ${names.length} workflows` : "this account";
31319
+ const h1 = charged.length === 0 ? card.complete && f.sent > 0 ? "Every step worked and every message read correctly" : card.complete ? "Every step that ran worked" : "No problems in what could be checked" : `${n2w(charged.length)} ${charged.length === 1 ? "thing" : "things"} would let a new lead down`;
31320
+ const whatOf = (d) => d.text.startsWith("A chosen workflow is still a draft") ? "a workflow is still a draft, so it ran nothing" : POINTS[d.kind].what.charAt(0).toLowerCase() + POINTS[d.kind].what.slice(1);
31321
+ const uniq = [...new Set(charged.map(whatOf))];
31322
+ const listed = uniq.slice(0, 3), more = uniq.length - listed.length;
31323
+ const joined = listed.length <= 1 ? listed.join("") : `${listed.slice(0, -1).join(", ")} and ${listed[listed.length - 1]}`;
31324
+ const went = `${names.length ? `In ${subject}, a` : "A"} test lead went through ${wholeLog ? "every step" : "the workflows"}${run.fastForward ? " with each wait cut to one minute" : ""}`;
31325
+ const sub = charged.length === 0 ? `${went}, and everything that was checked worked.${f.sent === 0 ? card.complete ? " No message was due during the check." : " No message arrived during the check." : ""}` : `${went}. ${joined.charAt(0).toUpperCase() + joined.slice(1)}${more ? `, and ${more} more kind${more === 1 ? "" : "s"} of problem below` : ""}.`;
31326
+ const meta = `${localTime2(run.startedAt)}${run.finishedAt ? ` to ${localTime2(run.finishedAt)}` : ""} \xB7 ${st?.read ? `${judged} ${judged === 1 ? "step" : "steps"} checked \xB7 ` : ""}${f.sent} ${f.sent === 1 ? "message" : "messages"} \xB7 scored with the fixed report card table`;
31327
+ const ringCaption = `${card.coverage.split(". Not checked")[0]}${card.capped ? `. ${capText(card.capped.at)}` : ""}${card.coverage.includes(". Not checked") ? ". Some parts not checked, listed below" : ""}`;
31328
+ const didNothing = stepProblems.filter((p) => p.kind === "did_nothing" || p.kind === "skipped").length;
31329
+ const failed = stepProblems.filter((p) => p.kind === "failed_holding" || p.kind === "failed_gave_up").length;
31330
+ const stepsKnown = !!st?.read && allSteps.length > 0;
31331
+ const period = `${localTime2(run.startedAt)}${run.finishedAt ? ` to ${localTime2(run.finishedAt)}` : ""}`;
31332
+ const clock = (ms) => new Date(ms).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
31333
+ const tiles = [
31334
+ stepsKnown ? statTile(judged, "Steps checked") : statTile("\u2013", "Steps checked", "warn"),
31335
+ statTile(f.sent, "Messages sent"),
31336
+ statTile(f.problems.length, "A customer would notice", f.problems.length ? "bad" : ""),
31337
+ (() => {
31338
+ const n = didNothing + failed + (run.draftWorkflows?.length ?? 0);
31339
+ return stepsKnown || n ? statTile(n, failed ? "Steps that failed or did nothing" : "Steps that did nothing", failed ? "bad" : n ? "warn" : "") : statTile("\u2013", "Steps failed, skipped or did nothing", "warn");
31340
+ })()
31341
+ ].join("");
31342
+ const worst = f.problems[0];
31343
+ const sentAt = worst ? f.timeline.find((t) => t.messageId === worst.messageId) : void 0;
31344
+ const quote = worst ? callout(`A new lead got "${marked2(worst.quote, "")}"`, { quote: true, tone: "bad", cite: `${worst.sentBy ?? worst.title}${sentAt ? `, sent ${clock(run.startedAt + sentAt.minute * 6e4)}` : ""}${worst.kind === "blank_field" && !run.leadHasFirstName ? " to a lead with no first name on file" : ""}` }) : stepProblems[0] ? callout(`"${esc3(stepProblems[0].stepName)}" ${stepProblems[0].kind === "did_nothing" ? "did nothing for the test lead, and the workflow carried on as if it had worked" : stepProblems[0].kind.startsWith("failed") ? "failed" : "was skipped"}.`, { quote: true, tone: "bad", cite: stepProblems[0].workflowName }) : run.draftWorkflows?.length ? callout(`A chosen workflow is still a draft, so GoHighLevel took the test lead and ran none of its steps.`, { quote: true, tone: "bad", cite: run.draftWorkflows.join(", ") }) : callout(f.sent ? "Nothing a customer would notice: every message that arrived read correctly." : "No message arrived during the check.", { quote: true, tone: f.sent ? "info" : "warn" });
31345
+ const stepTitle = (p) => POINTS[p.kind].what;
31346
+ const stepCard = (p) => fixCard(stepTitle(p), POINTS[p.kind].points, p.detail, esc3(`"${p.stepName}" in ${p.workflowName}`), p.fix);
31347
+ const cards = [
31348
+ ...(run.draftWorkflows ?? []).map((d) => [POINTS.did_nothing.points, fixCard("Still a draft, so it did nothing", POINTS.did_nothing.points, "GoHighLevel took the test lead into this workflow and ran none of its steps, with no error anywhere.", esc3(`${d}: draft`), "Publish the workflow, then run the check again")]),
31349
+ ...stepProblems.map((p) => [POINTS[p.kind].points, stepCard(p)]),
31350
+ ...f.problems.map((p) => [POINTS[p.kind].points, fixCard(p.title, POINTS[p.kind].points, p.sentBy ?? "", marked2(p.quote, p.highlight), p.fix)]),
31351
+ ...charged.filter((d) => d.kind === "missing").map((d) => [POINTS.missing.points, fixCard("A planned message never arrived", POINTS.missing.points, d.text.replace(/^A planned message was never sent: /, ""), "Nothing arrived inside the check", "The step published, not skipped by a condition, and not behind a step that failed")])
31352
+ ];
31353
+ const fixCards = cards.sort((a, b) => b[0] - a[0]).map(([, h]) => h);
31354
+ const projected = 100;
31355
+ const fixesHead = fixCards.length ? `${n2w(fixCards.length)} ${fixCards.length === 1 ? "fix would take" : "fixes would take"} this to ${gradeOf(card.score) === gradeOf(projected) ? projected : `an ${gradeOf(projected)}`}` : "Nothing to fix";
31356
+ const s5 = `<section data-section="fixes" id="fixes" class="rp-section"><div class="rp-head"><div><div class="rp-label">Fix these</div><h2>${esc3(fixesHead)}</h2></div><div class="rp-right">Each fix changes only the step shown, then the check runs again</div></div>${fixCards.length ? `<div class="rp-two">${fixCards.join("")}</div>` : `<div class="rp-card"><p class="rp-meta">Every check that ran passed, so there is nothing to change.</p></div>`}</section>`;
31357
+ const s6 = `<section data-section="proof" id="proof" class="rp-section"><div class="rp-two">
31358
+ <div class="rp-card"><div class="rp-card-head"><h3>After the fixes</h3><span class="rp-tag">Projected until re\u2011run</span></div>${beforeAfter(card.score, charged.length ? projected : card.score, card.complete)}<p class="rp-meta">The after grade is confirmed only by running the same check again.</p></div>
31359
+ <div class="rp-card"><h3>How ${card.score} was reached</h3>${card.deductions.length ? deductions(card.deductions, card.capped?.at) : `<p class="rp-meta">No points lost: every check that ran passed.</p>`}</div>
31360
+ </div></section>`;
31361
+ const s7 = `<section data-section="detail" id="detail" class="rp-section"><div class="rp-head rp-divider"><div><div class="rp-label">The detail</div><h2>Score by area, and every step</h2></div><div class="rp-right">For whoever maintains the workflow</div></div><div class="rp-card">${card.areas.length ? scoreBars(card.areas, `From GoHighLevel's execution log and the test lead's inbox, ${period}.`) : `<p class="rp-meta">No area had anything to score.</p>`}</div></section>`;
31362
+ const stepNotes = !st ? ["The workflow steps were not read for this check."] : !st.read ? [st.unreadable ?? "The step log could not be read."] : [
31363
+ ...st.unreadWorkflows?.length ? [`Could not read the steps of: ${st.unreadWorkflows.join(", ")}.`] : [],
31364
+ ...st.partialWorkflows?.length ? [`Very busy workflow, only partly read: ${st.partialWorkflows.join(", ")}. Some steps may be missing below.`] : [],
31365
+ ...st.rows.length === 0 ? ["No steps were logged for the test lead. That is not a pass: the lead may not have entered, or GoHighLevel had not logged it yet."] : [],
31366
+ ...st.counts.unclear ? [`${st.counts.unclear} step${st.counts.unclear === 1 ? " has" : "s have"} a status this version cannot read; check GoHighLevel's Execution logs.`] : []
31367
+ ];
31368
+ const multi = byWf.size > 1;
31369
+ const stepsCard = `<div class="rp-card rp-flowcard"><div class="rp-card-head"><div><div class="rp-label">Every step</div><h3>What each step really did</h3></div><span class="rp-tag">From GoHighLevel's own log</span></div>${stepNotes.length ? callout(stepNotes.map(esc3).join("<br>"), { tone: "warn" }) : ""}${[...byWf.values()].map((w) => `${multi ? `<div class="rp-tag">${esc3(w.name)}</div>` : ""}${workflowFlow([...w.steps.values()])}`).join("")}</div>`;
31370
+ const entered = (st?.read ? st.rows : []).find((r) => r.outcome === "entered");
31371
+ const tlRows = [
31372
+ ...entered ? [`<li><span class="t">+0:00</span><span class="dot ok"></span><span class="what">Entered the workflow</span><span></span></li>`] : [],
31373
+ ...f.timeline.map((t) => {
31374
+ const tone = t.problems.length ? "bad" : "ok";
31375
+ const mins2 = Math.floor(t.minute), sec = Math.round((t.minute - mins2) * 60);
31376
+ const stepName = t.sentBy ? t.sentBy.slice(t.sentBy.lastIndexOf(": ") + 2) : t.channel === "email" ? t.subject ?? t.preview : t.preview;
31377
+ const problem = f.problems.find((p) => p.messageId === t.messageId);
31378
+ return `<li><span class="t">+${mins2}:${String(sec).padStart(2, "0")}</span><span class="dot ${tone}"></span><span class="what">${esc3(stepName)}${problem ? `<small>"${esc3(problem.quote)}"</small>` : ""}</span>${chip(tone, t.problems.length ? t.problems[0] === "blank_field" ? "Blank name" : "Problem" : "Arrived")}</li>`;
31379
+ })
31380
+ ].join("");
31381
+ const broke = stepProblems.slice().sort((a, b) => POINTS[b.kind].points - POINTS[a.kind].points)[0];
31382
+ const s8 = `<section data-section="steps" class="rp-band"><div class="rp-detail">${stepsCard}
31383
+ <div class="rp-col"><div class="rp-card"><h3>What the lead received</h3>${tlRows ? `<ol class="ao-tl">${tlRows}</ol>` : `<p class="rp-meta">Nothing arrived.</p>`}</div>
31384
+ <div class="rp-col"><div class="rp-tag">The step that broke</div>${broke ? stepCard(broke) : run.draftWorkflows?.length ? fixCard("Still a draft, so it did nothing", POINTS.did_nothing.points, "GoHighLevel took the test lead into this workflow and ran none of its steps, with no error anywhere.", esc3(`${run.draftWorkflows[0]}: draft`), "Publish the workflow, then run the check again") : `<div class="rp-card"><p class="rp-meta">No step failed, was skipped or did nothing.</p></div>`}</div></div></div></section>`;
31385
+ const gaps = card.coverage.includes(". Not checked: ") ? card.coverage.split(". Not checked: ")[1].split("; ") : [];
31386
+ const ff = run.fastForward;
31387
+ const notTested = [
31388
+ ...ff ? [`real wait timing, because every wait was cut to one minute (${ff.copies.flatMap((c) => c.waits).join("; ") || "no waits"})`] : [],
31389
+ ...ff ? ff.copies.filter((c) => c.window).map((c) => `${c.sourceName} ${c.window} in real life; the copy ran at any hour`) : [],
31390
+ ...gaps,
31391
+ ...f.links.skipped ? ["links other than the client's own website: a tracked link counts as the lead clicking, and an unsubscribe link opts them out"] : [],
31392
+ "booking an actual time, replying as the lead, phone calls, and whether the wording persuades"
31393
+ ];
31394
+ const leftCopies = ff && !ff.removed ? ff.problems?.length ? ff.problems.join(" ") : `Temporary copies still in the account: ${ff.copies.map((c) => `"${c.copyName}"`).join(", ")}. Delete them in GoHighLevel.` : "";
31395
+ const cleanupText = [run.cleanup?.detail ?? "", leftCopies || (ff?.removed ? "The temporary fast-forward copies were removed." : ""), "Anything that looked like a password or key in a message was hidden before it reached this report."].filter(Boolean).join(" ");
31396
+ const cleanedUp = !leftCopies && run.cleanup?.removed === true;
31397
+ const s9 = `<section data-section="coverage" class="rp-band">${callout(`${esc3(notTested.map((g) => g.charAt(0).toUpperCase() + g.slice(1)).join(". "))}.`, { tone: "info", title: "Not tested in this run" })}<div class="rp-gap"></div>${callout(esc3(cleanupText), { tone: cleanedUp ? "info" : "warn", title: cleanedUp ? "Cleaned up" : leftCopies ? "Not cleaned up" : "Test lead" })}</section>`;
31398
+ const source = ff ? "From GoHighLevel's execution log and the test lead's inbox. The original workflows were only read." : "From GoHighLevel's execution log and the test lead's inbox. Nothing in the workflows was changed.";
31399
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc3(`Workflow audit \xB7 ${opts.clientName}`)}</title>
31400
+ <style>${DS_TOKENS_CSS}</style>
31401
+ <style>${DS_BUNDLE_CSS}</style>
31402
+ <style>${LAYOUT_CSS}</style></head><body>
31403
+ <div class="rp">
31404
+ <div class="rp-tools ao-no-print">${opts.backHref ? `<a class="ao-btn ghost" href="${esc3(opts.backHref)}">Back to Secret shopper</a>` : ""}<button class="ao-btn" type="button" id="save-pdf">Save as PDF</button></div>
31405
+ <header data-section="bar" class="rp-bar"><div class="rp-brand">${esc3(brand.name)} <span>Live Workflow Audit</span></div><nav class="rp-nav"><a class="on" href="#fixes">Fixes</a><a href="#proof">Proof</a><a href="#detail">Detail</a></nav></header>
31406
+ <section data-section="hero" class="rp-hero"><div class="rp-hero-text"><div class="rp-kicker">Verify \xB7 Live workflow audit \xB7 ${esc3(opts.clientName)}</div><h1>${esc3(h1)}</h1><p class="rp-sub">${esc3(sub)}</p><p class="rp-meta">${esc3(meta)}</p><p class="rp-meta">${esc3(run.entryNote)}</p></div><div class="rp-ringbox">${gradeRing(card.score, ringCaption, card.complete)}</div></section>
31407
+ <section data-section="tiles" class="rp-band"><div class="rp-tiles">${tiles}</div></section>
31408
+ <section data-section="quote" class="rp-band">${quote}</section>
31409
+ ${s5}${s6}${s7}${s8}${s9}
31410
+ <footer data-section="footer" class="rp-foot"><span>${esc3(source)}</span><span>${esc3(brand.footer)}</span></footer>
31411
+ </div>
31412
+ <script>document.getElementById("save-pdf").addEventListener("click",function(){window.print()});if(/[?&]print=1\b/.test(location.search))window.addEventListener("load",function(){setTimeout(function(){window.print()},300)});</script>
31413
+ </body></html>`;
31414
+ }
31415
+ var esc3, n2w, localTime2, gcls, chip, statTile, callout, OUTCOME, LAYOUT_CSS, DEFAULT_BRANDING;
31416
+ var init_report_page = __esm({
31417
+ "src/command-os/shopper/report-page.ts"() {
31418
+ "use strict";
31419
+ init_report_css_generated();
31420
+ init_score();
31421
+ esc3 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
31422
+ n2w = (n) => ["No", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten"][n] ?? String(n);
31423
+ localTime2 = (ms) => new Date(ms).toLocaleString("en-US", { weekday: "short", month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
31424
+ gcls = (score2) => `ao-g-${gradeOf(score2).toLowerCase()}`;
31425
+ chip = (tone, text2) => `<span class="ao-chip ${tone}">${esc3(text2)}</span>`;
31426
+ statTile = (value, label, tone = "") => `<div class="ao-stat"><div class="big ${tone}">${esc3(String(value))}</div><div class="lbl">${esc3(label)}</div></div>`;
31427
+ callout = (inner, opts) => `<div class="ao-callout ${opts.quote ? "quote" : ""} ${opts.tone ?? ""}">${opts.title ? `<span class="ttl">${esc3(opts.title)}</span>` : ""}${inner}${opts.cite ? `<cite>${esc3(opts.cite)}</cite>` : ""}</div>`;
31428
+ OUTCOME = { ran: ["ok", "Ran"], branch: ["ok", "Path taken"], did_nothing: ["warn", "Did nothing"], waiting: ["wait", "Waiting"], failed: ["bad", "Failed, holding"], unclear: ["warn", "Unclear"] };
31429
+ LAYOUT_CSS = `
31430
+ body{margin:0;background:var(--bg);color:var(--text);font-family:var(--font-body)}
31431
+ a{color:var(--accent-ink)}
31432
+ .rp{max-width:1280px;margin:0 auto;background:var(--bg)}
31433
+ .rp-tools a{text-decoration:none}
31434
+ .rp-tools{display:flex;justify-content:flex-end;gap:var(--space-2);padding:var(--space-3) 56px;background:var(--bg)}
31435
+ .rp-bar{display:flex;justify-content:space-between;align-items:center;padding:0 56px;height:60px;background:var(--surface);border-bottom:1px solid var(--border)}
31436
+ .rp-brand{font-family:var(--font-display);font-weight:700;font-size:15px}
31437
+ .rp-brand span{color:var(--muted);font-weight:500;font-size:12px;letter-spacing:.12em;text-transform:uppercase;margin-left:10px}
31438
+ .rp-nav{display:flex;gap:var(--space-2)}
31439
+ .rp-nav a{font-size:13px;padding:7px 12px;border-radius:var(--radius-pill);color:var(--muted);text-decoration:none}
31440
+ .rp-nav a.on{background:var(--primary);color:var(--on-primary)}
31441
+ .rp-hero{padding:48px 56px 44px;background:var(--surface);border-bottom:1px solid var(--border);display:flex;gap:48px;align-items:center}
31442
+ .rp-hero-text{flex-grow:1;display:flex;flex-direction:column;gap:14px;min-width:0}
31443
+ .rp-kicker{font-size:12px;letter-spacing:.2em;text-transform:uppercase;color:var(--accent-ink);font-weight:600}
31444
+ .rp-hero h1{margin:0;font-family:var(--font-display);font-size:48px;line-height:52px;font-weight:700;letter-spacing:-.01em;text-wrap:balance}
31445
+ .rp-sub{margin:0;font-size:18px;line-height:1.65;color:var(--muted);max-width:640px}
31446
+ .rp-meta{margin:0;font-size:14px;color:var(--muted)}
31447
+ .rp-ringbox{flex-shrink:0;padding:24px;border-radius:var(--radius-xl);background:var(--surface);box-shadow:var(--shadow-lift);border:1px solid var(--border)}
31448
+ .rp-band{padding:28px 56px 0}
31449
+ .rp-tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:var(--space-3)}
31450
+ .rp-section{padding:44px 56px 0;display:flex;flex-direction:column;gap:18px}
31451
+ .rp-head{display:flex;justify-content:space-between;align-items:flex-end;gap:var(--space-4)}
31452
+ .rp-label{font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:var(--accent-ink);font-weight:600;margin-bottom:10px}
31453
+ .rp-head h2{margin:0;font-family:var(--font-display);font-size:32px;line-height:38px;font-weight:700}
31454
+ .rp-head .rp-right{font-size:13px;color:var(--muted)}
31455
+ .rp-two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--space-3);align-items:start}
31456
+ .rp-detail{display:grid;grid-template-columns:minmax(0,7fr) minmax(0,5fr);gap:var(--space-3);align-items:start}
31457
+ .rp-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px 24px;display:flex;flex-direction:column;gap:10px}
31458
+ .rp-card h3{margin:0;font-family:var(--font-display);font-size:20px}
31459
+ .rp-card-head{display:flex;justify-content:space-between;align-items:center;gap:var(--space-3)}
31460
+ .rp-tag{font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:600}
31461
+ .rp-divider{border-top:1px solid var(--border);padding-top:40px}
31462
+ .rp-list{margin:0;padding-left:1.1em;font-size:14px;line-height:1.6;color:var(--text)}
31463
+ .rp-col{display:flex;flex-direction:column;gap:var(--space-3)}
31464
+ .rp-gap{height:var(--space-3)}
31465
+ .rp-soon{margin:0;font-size:12px;color:var(--muted)}
31466
+ .rp-foot{padding:32px 56px;margin-top:32px;font-size:13px;color:var(--muted);display:flex;justify-content:space-between;gap:var(--space-4);border-top:1px solid var(--border)}
31467
+ .fixhead{justify-content:space-between;align-items:flex-start;flex-wrap:nowrap}
31468
+ .fixhead h4{flex:1;min-width:0}
31469
+ mark{background:var(--surface-sunk);color:var(--bad);font-weight:700;border-radius:4px;padding:0 3px}
31470
+ @media screen and (max-width:760px){.rp-hero{flex-direction:column;align-items:flex-start;padding:32px 16px}.rp-hero h1{font-size:32px;line-height:37px}.rp-band,.rp-section,.rp-bar,.rp-tools,.rp-foot{padding-left:16px;padding-right:16px}.rp-two,.rp-detail{grid-template-columns:1fr}}
31471
+ @media print{
31472
+ .rp-tools,.rp-nav{display:none!important}
31473
+ .rp,body{max-width:none;background:var(--surface)}
31474
+ .rp-bar{border-bottom:1px solid var(--border)}
31475
+ .rp-tiles{grid-template-columns:repeat(4,minmax(0,1fr))}
31476
+ .rp-tiles .ao-stat{min-width:0}
31477
+ .rp-detail{grid-template-columns:1fr}
31478
+ .rp-card.rp-flowcard{break-inside:auto;page-break-inside:auto}
31479
+ .rp-bar,.rp-hero,.rp-band,.rp-section,.rp-foot{padding-left:0;padding-right:0}
31480
+ .rp-hero{padding-top:24px;padding-bottom:24px;gap:24px}
31481
+ .rp-hero h1{font-size:34px;line-height:40px}
31482
+ .rp-head h2{font-size:26px;line-height:32px;text-wrap:balance}
31483
+ .rp-section{padding-top:28px}
31484
+ .rp-card,.rp-ringbox,.rp-head,.rp-list li{break-inside:avoid;page-break-inside:avoid}
31485
+ .rp-head,.rp-label{break-after:avoid;page-break-after:avoid}
31486
+ .rp-ringbox{box-shadow:none}
31487
+ .rp-foot{margin-top:12px;padding-top:10px;padding-bottom:0}
31488
+ .rp-band{padding-top:16px}
31489
+ .rp-gap{height:var(--space-2)}
31490
+ }`;
31491
+ DEFAULT_BRANDING = { name: "GHL Command", footer: "Made with GHL Command \xB7 ghlcommand.com" };
31492
+ }
31493
+ });
31494
+
31008
31495
  // src/command-os/shopper/plan.ts
31009
31496
  function waitMinutes(node) {
31010
31497
  const a = asObj5(node.attributes);
@@ -31082,18 +31569,22 @@ async function planFromWorkflowIds(reader, ids) {
31082
31569
  const unread = [];
31083
31570
  const names = {};
31084
31571
  const drafts = [];
31572
+ const draftIds = [];
31085
31573
  for (const id of ids) {
31086
31574
  try {
31087
31575
  const wf = asObj5(await reader.getWorkflow(id));
31088
31576
  const name = str6(wf.name) ?? id;
31089
31577
  names[id] = name;
31090
- if (str6(wf.status)?.toLowerCase() === "draft") drafts.push(name);
31578
+ if (str6(wf.status)?.toLowerCase() === "draft") {
31579
+ drafts.push(name);
31580
+ draftIds.push(id);
31581
+ }
31091
31582
  workflows.push({ id, name, templates: asArr4(asObj5(wf.workflowData).templates) });
31092
31583
  } catch {
31093
31584
  unread.push(id);
31094
31585
  }
31095
31586
  }
31096
- return { ...plannedMessages(workflows), unread, names, drafts };
31587
+ return { ...plannedMessages(workflows), unread, names, drafts, draftIds };
31097
31588
  }
31098
31589
  var asObj5, asArr4, str6, isBranch;
31099
31590
  var init_plan2 = __esm({
@@ -31748,25 +32239,25 @@ var init_instructions = __esm({
31748
32239
  function cardHtml(c) {
31749
32240
  const entry = CONNECTOR_REGISTRY.find((entry2) => entry2.id === c.id);
31750
32241
  if (!entry) return "";
31751
- if (c.state === "not-built") return `<section class="connection" id="${c.id}"><h2>${esc3(c.name)}</h2><p><span class="dot grey" aria-hidden="true"></span>Not built yet</p></section>`;
31752
- if (c.state === "via-claude") return `<section class="connection" id="${c.id}"><h2>${esc3(c.name)}</h2>${connectorInstructions(entry, "html")}</section>`;
32242
+ if (c.state === "not-built") return `<section class="connection" id="${c.id}"><h2>${esc4(c.name)}</h2><p><span class="dot grey" aria-hidden="true"></span>Not built yet</p></section>`;
32243
+ if (c.state === "via-claude") return `<section class="connection" id="${c.id}"><h2>${esc4(c.name)}</h2>${connectorInstructions(entry, "html")}</section>`;
31753
32244
  const proven = c.state === "proven";
31754
32245
  const google = c.id === "google-calendar";
31755
32246
  const needsSetup = google && c.setup === "needs-setup";
31756
32247
  return `<section class="connection" id="${c.id}" data-system="${c.id}">
31757
- <h2 class="${proven ? "compact" : ""}">${esc3(c.name)}</h2>
31758
- <p><span class="dot ${c.readError ? "saved-unproven" : c.state}" aria-hidden="true"></span><span class="status">${esc3(statusText(c))}</span> <span class="needs-marker"${needsYou(c) ? "" : " hidden"}>Needs you today</span></p>
32248
+ <h2 class="${proven ? "compact" : ""}">${esc4(c.name)}</h2>
32249
+ <p><span class="dot ${c.readError ? "saved-unproven" : c.state}" aria-hidden="true"></span><span class="status">${esc4(statusText(c))}</span> <span class="needs-marker"${needsYou(c) ? "" : " hidden"}>Needs you today</span></p>
31759
32250
  <button class="instructions-toggle" type="button" aria-controls="instructions-${c.id}" aria-expanded="${!proven && !needsSetup}"${proven && !needsSetup ? "" : " hidden"}>Instructions</button>
31760
32251
  <div class="connector-instructions" id="instructions-${c.id}"${proven || needsSetup ? " hidden" : ""}>${connectorInstructions(entry, "html")}</div>
31761
- ${google ? `<p class="setup-notice"${needsSetup ? "" : " hidden"}>${esc3(CALENDAR_SETUP_MESSAGE)}</p>` : ""}
32252
+ ${google ? `<p class="setup-notice"${needsSetup ? "" : " hidden"}>${esc4(CALENDAR_SETUP_MESSAGE)}</p>` : ""}
31762
32253
  <div class="action-controls">
31763
32254
  <button class="replace-toggle" type="button" aria-controls="connect-${c.id}" aria-expanded="false"${proven && !needsSetup ? "" : " hidden"}>${google ? "Reconnect" : "Replace key"}</button>
31764
32255
  <button class="disconnect-toggle" type="button" aria-controls="disconnect-${c.id}" aria-expanded="false"${c.connectedAt ? "" : " hidden"}>Disconnect</button>
31765
- ${google ? `<div class="actions" id="connect-${c.id}"${proven || needsSetup ? " hidden" : ""}><button type="button" data-action="start">Connect</button><button type="button" data-action="finish">Finish</button></div>` : `<form class="connect-form" id="connect-${c.id}" autocomplete="off"${proven ? " hidden" : ""}><label for="key-${c.id}">Paste ${c.id === "stripe" ? "restricted key" : "bot token"}</label><input id="key-${c.id}" name="key" type="password" minlength="8" required autocomplete="new-password"><label for="label-${c.id}">Account label (optional)</label><input id="label-${c.id}" name="label" value="${esc3(c.label || "")}"><button type="submit">Connect</button></form>`}
32256
+ ${google ? `<div class="actions" id="connect-${c.id}"${proven || needsSetup ? " hidden" : ""}><button type="button" data-action="start">Connect</button><button type="button" data-action="finish">Finish</button></div>` : `<form class="connect-form" id="connect-${c.id}" autocomplete="off"${proven ? " hidden" : ""}><label for="key-${c.id}">Paste ${c.id === "stripe" ? "restricted key" : "bot token"}</label><input id="key-${c.id}" name="key" type="password" minlength="8" required autocomplete="new-password"><label for="label-${c.id}">Account label (optional)</label><input id="label-${c.id}" name="label" value="${esc4(c.label || "")}"><button type="submit">Connect</button></form>`}
31766
32257
  <form class="disconnect-form" id="disconnect-${c.id}" hidden><label for="confirm-${c.id}">Type DISCONNECT to remove this connection</label><input id="confirm-${c.id}" name="confirm" autocomplete="off" required pattern="DISCONNECT"><button type="submit">Disconnect</button></form>
31767
32258
  </div>
31768
32259
  <p class="result" role="status" aria-live="polite"></p><a class="sign-in" hidden target="_blank" rel="noopener noreferrer">Open Google sign-in</a>
31769
- ${google ? `<details class="operator-details"${needsSetup ? "" : " hidden"}><summary>Details for the operator</summary><p>${esc3(c.setupDetail || "")}</p></details>` : ""}
32260
+ ${google ? `<details class="operator-details"${needsSetup ? "" : " hidden"}><summary>Details for the operator</summary><p>${esc4(c.setupDetail || "")}</p></details>` : ""}
31770
32261
  </section>`;
31771
32262
  }
31772
32263
  function connectionsPageHtml(cards, seatToken = "") {
@@ -31799,8 +32290,8 @@ main.connections{max-width:50rem;margin:0 auto;padding:1.6rem 1.2rem 4rem;color:
31799
32290
  <p id="loaded">Loading saved records\u2026</p>
31800
32291
  <p id="honesty">${unreadable.length ? `Could not read this load: ${unreadable.join(", ")}.` : "All saved connection states could be read this load."}</p>
31801
32292
  <button id="refresh" type="button">Refresh saved states</button>
31802
- <section class="needs-box"><h2>Needs you today</h2><ul id="needs">${cards.filter(needsYou).map((c) => `<li><a href="#${c.id}">${esc3(c.name)}: ${c.readError ? "check the unreadable connection" : c.state === "saved-unproven" ? "reconnect to prove access" : "connect when you want it in your brief"}</a></li>`).join("")}</ul><p id="no-needs"${cards.some(needsYou) ? " hidden" : ""}>No connection needs your attention.</p></section>
31803
- <nav class="jump" aria-label="Jump to system">${cards.map((c) => `<a href="#${c.id}">${esc3(c.name)}</a>`).join("")}</nav>
32293
+ <section class="needs-box"><h2>Needs you today</h2><ul id="needs">${cards.filter(needsYou).map((c) => `<li><a href="#${c.id}">${esc4(c.name)}: ${c.readError ? "check the unreadable connection" : c.state === "saved-unproven" ? "reconnect to prove access" : "connect when you want it in your brief"}</a></li>`).join("")}</ul><p id="no-needs"${cards.some(needsYou) ? " hidden" : ""}>No connection needs your attention.</p></section>
32294
+ <nav class="jump" aria-label="Jump to system">${cards.map((c) => `<a href="#${c.id}">${esc4(c.name)}</a>`).join("")}</nav>
31804
32295
  ${cards.map(cardHtml).join("")}</main>${cockpitShellClose()}
31805
32296
  <script>
31806
32297
  ${COCKPIT_TYPE_JS}
@@ -31902,7 +32393,7 @@ refresh();
31902
32393
  })();
31903
32394
  </script></body></html>`;
31904
32395
  }
31905
- var esc3, needsYou, statusText;
32396
+ var esc4, needsYou, statusText;
31906
32397
  var init_page2 = __esm({
31907
32398
  "src/command-os/connectors/page.ts"() {
31908
32399
  "use strict";
@@ -31910,7 +32401,7 @@ var init_page2 = __esm({
31910
32401
  init_script_json();
31911
32402
  init_instructions();
31912
32403
  init_registry();
31913
- esc3 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
32404
+ esc4 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
31914
32405
  needsYou = (c) => c.builtState === "built" && (Boolean(c.readError) || c.state !== "proven");
31915
32406
  statusText = (c) => [
31916
32407
  c.name,
@@ -33288,19 +33779,19 @@ function renderClientStatusHtml(input) {
33288
33779
  const stamp = friendlyStamp(input.checkedAt);
33289
33780
  const day = friendlyDate(input.checkedAt);
33290
33781
  const latest = scrubLatestCheck(input.latestCheck);
33291
- const latestHtml = latest ? `<section id="latest"><h2>What the latest check found</h2>${latest.summary ? `<p class="intro">${esc4(latest.summary)}</p>` : ""}${latest.issues?.length ? `<ul class="issues">${latest.issues.map((i) => `<li>${esc4(i)}</li>`).join("")}</ul>` : `<p class="intro">Nothing needed your attention this time.</p>`}</section>` : "";
33782
+ const latestHtml = latest ? `<section id="latest"><h2>What the latest check found</h2>${latest.summary ? `<p class="intro">${esc5(latest.summary)}</p>` : ""}${latest.issues?.length ? `<ul class="issues">${latest.issues.map((i) => `<li>${esc5(i)}</li>`).join("")}</ul>` : `<p class="intro">Nothing needed your attention this time.</p>`}</section>` : "";
33292
33783
  const entry = (it) => {
33293
33784
  const cls = it.state === "in-place" ? "ok" : it.state === "waiting" ? "wait" : "soon";
33294
- const detail = it.state === "waiting" && (it.resolvedBy || it.roughly) ? `<p class="who"><b>Who sorts it:</b> ${esc4(scrubForClient(it.resolvedBy ?? "") || "not stated")}.${it.roughly ? ` <b>Roughly when:</b> ${esc4(scrubForClient(it.roughly))}.` : ""}</p>` : "";
33295
- const body = it.sentence ? `<p>${esc4(it.sentence)}</p>` : "";
33296
- return `<article class="item ${cls}"><span class="pill">${esc4(CLIENT_STATE_LABEL[it.state])}</span><h3>${esc4(it.title)}</h3>${body}${detail}</article>`;
33785
+ const detail = it.state === "waiting" && (it.resolvedBy || it.roughly) ? `<p class="who"><b>Who sorts it:</b> ${esc5(scrubForClient(it.resolvedBy ?? "") || "not stated")}.${it.roughly ? ` <b>Roughly when:</b> ${esc5(scrubForClient(it.roughly))}.` : ""}</p>` : "";
33786
+ const body = it.sentence ? `<p>${esc5(it.sentence)}</p>` : "";
33787
+ return `<article class="item ${cls}"><span class="pill">${esc5(CLIENT_STATE_LABEL[it.state])}</span><h3>${esc5(it.title)}</h3>${body}${detail}</article>`;
33297
33788
  };
33298
- const section2 = (id, title2, intro, list2) => list2.length ? `<section id="${id}"><h2>${esc4(title2)}</h2><p class="intro">${esc4(intro)}</p>${list2.map(entry).join("\n")}</section>` : "";
33789
+ const section2 = (id, title2, intro, list2) => list2.length ? `<section id="${id}"><h2>${esc5(title2)}</h2><p class="intro">${esc5(intro)}</p>${list2.map(entry).join("\n")}</section>` : "";
33299
33790
  const footLeft = `${agency ? `Prepared by ${agency} for ` : "Prepared for "}${input.accountName} \xB7 last checked ${day}`;
33300
33791
  const revLine = input.revision && input.revision > 0 ? `Update ${input.revision} \xB7 last checked ${stamp}` : `Last checked ${stamp}`;
33301
33792
  return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="robots" content="noindex">
33302
- <title>${esc4(statusPageTitle(input))}</title>
33303
- <meta name="description" content="${esc4(`What is live in the ${input.accountName} account right now.`)}">
33793
+ <title>${esc5(statusPageTitle(input))}</title>
33794
+ <meta name="description" content="${esc5(`What is live in the ${input.accountName} account right now.`)}">
33304
33795
  <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
33305
33796
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600;700&family=DM+Sans:wght@400;500;700&display=swap">
33306
33797
  <style>
@@ -33403,7 +33894,7 @@ function renderClientStatusHtml(input) {
33403
33894
  }
33404
33895
  </style></head><body>
33405
33896
  <div class="chrome"><div class="chrome-in">
33406
- <span class="who">${esc4(agency || input.accountName)}</span>
33897
+ <span class="who">${esc5(agency || input.accountName)}</span>
33407
33898
  <nav>${latestHtml ? '<a href="#latest">Latest check</a>' : ""}${live.length ? '<a href="#live">Live now</a>' : ""}${notYet.length ? '<a href="#soon">To confirm</a>' : ""}${waiting.length ? '<a href="#waiting">Waiting</a>' : ""}${next.length ? '<a href="#next">What happens next</a>' : ""}</nav>
33408
33899
  <div class="tools">
33409
33900
  <button type="button" id="print">Print / save PDF</button>
@@ -33412,11 +33903,11 @@ function renderClientStatusHtml(input) {
33412
33903
  </div>
33413
33904
  </div></div>
33414
33905
  <div class="page"><div class="sheet">
33415
- ${agency || isRasterDataUri(input.logoDataUri) ? `<div class="brand">${isRasterDataUri(input.logoDataUri) ? `<img src="${esc4(input.logoDataUri)}" alt="${esc4(agency ?? "")}">` : ""}${agency ? `<span class="agency">${esc4(agency)}</span>` : ""}<span class="for">Prepared for<br><b>${esc4(input.accountName)}</b></span></div>` : ""}
33906
+ ${agency || isRasterDataUri(input.logoDataUri) ? `<div class="brand">${isRasterDataUri(input.logoDataUri) ? `<img src="${esc5(input.logoDataUri)}" alt="${esc5(agency ?? "")}">` : ""}${agency ? `<span class="agency">${esc5(agency)}</span>` : ""}<span class="for">Prepared for<br><b>${esc5(input.accountName)}</b></span></div>` : ""}
33416
33907
  <p class="eyebrow">What is live in your account</p>
33417
- <h1>${esc4(input.accountName)}</h1>
33418
- <p class="stamp">${esc4(revLine)}</p>
33419
- <div class="verdict"><b>${esc4(headline3)}</b><span>${esc4(sub)}</span></div>
33908
+ <h1>${esc5(input.accountName)}</h1>
33909
+ <p class="stamp">${esc5(revLine)}</p>
33910
+ <div class="verdict"><b>${esc5(headline3)}</b><span>${esc5(sub)}</span></div>
33420
33911
  <p class="living">This page checks itself. Every time your account is re-checked, this page is rewritten at the same address, so what you see here is what is true today. Bookmark it and come back whenever you want to know where things stand.</p>
33421
33912
  <div class="tiles">
33422
33913
  <div class="tile ok"><div class="n">${live.length}</div><div class="l">Live now</div></div>
@@ -33427,8 +33918,8 @@ ${latestHtml}
33427
33918
  ${section2("live", "Live in your account", "Each of these was read back from your account, not assumed.", live)}
33428
33919
  ${section2("soon", "Built, not confirmed yet", "The work is done. These are the ones we have not been able to confirm for you yet, and why.", notYet)}
33429
33920
  ${section2("waiting", "Waiting on someone else", "Outside our hands. Here is who sorts each one, and roughly when.", waiting)}
33430
- ${next.length ? `<section id="next"><h2>What happens next</h2><ol>${next.map((n) => `<li>${esc4(n)}</li>`).join("")}</ol></section>` : ""}
33431
- <div class="foot"><span>${esc4(footLeft)}</span>${input.contact ? `<span>Questions? ${input.contact.href ? `<a href="${esc4(input.contact.href)}">${esc4(input.contact.label)}</a>` : esc4(input.contact.label)}</span>` : ""}</div>
33921
+ ${next.length ? `<section id="next"><h2>What happens next</h2><ol>${next.map((n) => `<li>${esc5(n)}</li>`).join("")}</ol></section>` : ""}
33922
+ <div class="foot"><span>${esc5(footLeft)}</span>${input.contact ? `<span>Questions? ${input.contact.href ? `<a href="${esc5(input.contact.href)}">${esc5(input.contact.label)}</a>` : esc5(input.contact.label)}</span>` : ""}</div>
33432
33923
  </div></div>
33433
33924
  <script>
33434
33925
  (function(){
@@ -33455,7 +33946,7 @@ ${next.length ? `<section id="next"><h2>What happens next</h2><ol>${next.map((n)
33455
33946
  </script>
33456
33947
  </body></html>`;
33457
33948
  }
33458
- var CLIENT_STATE_LABEL, CLIENT_TITLE_RULES, DE_JARGON, LEAK_RULES, NEXT_STEP_RULES, toHex2, esc4, cssStr;
33949
+ var CLIENT_STATE_LABEL, CLIENT_TITLE_RULES, DE_JARGON, LEAK_RULES, NEXT_STEP_RULES, toHex2, esc5, cssStr;
33459
33950
  var init_client_status = __esm({
33460
33951
  "src/command-os/client-status.ts"() {
33461
33952
  "use strict";
@@ -33517,7 +34008,7 @@ var init_client_status = __esm({
33517
34008
  { re: /handoff call|on your own|without help/i, step: "On the handover call, do one task in the account yourself. That is what confirms you can run it without us." }
33518
34009
  ];
33519
34010
  toHex2 = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
33520
- esc4 = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
34011
+ esc5 = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
33521
34012
  cssStr = (s) => String(s).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/[\r\n]+/g, " ");
33522
34013
  }
33523
34014
  });
@@ -33953,7 +34444,7 @@ async function prepareHandoffEmail(client, locationId2, name, email, url) {
33953
34444
  const found = await client.get("/contacts/", { params: { locationId: locationId2, query: email, limit: 5 } });
33954
34445
  const contact = found.contacts?.find((c) => c.email?.toLowerCase() === email.toLowerCase());
33955
34446
  if (!contact) throw new Error("Add this email as a contact in the client account, then review the handoff email again.");
33956
- const body = { locationId: locationId2, contactId: contact.id, email, subject: `Your ${name} handoff`, html: `<p>Your account handoff lists what was delivered and what still needs attention.</p><p><a href="${esc5(url)}">Open your handoff</a></p>` };
34447
+ const body = { locationId: locationId2, contactId: contact.id, email, subject: `Your ${name} handoff`, html: `<p>Your account handoff lists what was delivered and what still needs attention.</p><p><a href="${esc6(url)}">Open your handoff</a></p>` };
33957
34448
  return { ...body, hash: contentHash(body) };
33958
34449
  }
33959
34450
  async function sendHandoffEmail(client, preview, confirmation) {
@@ -33964,12 +34455,12 @@ async function sendHandoffEmail(client, preview, confirmation) {
33964
34455
  const sent = await client.post("/conversations/messages", { body: { type: "Email", contactId: preview.contactId, emailTo: preview.email, subject: preview.subject, html: preview.html }, noRetry: true });
33965
34456
  return { accepted: Boolean(sent.messageId), messageId: sent.messageId, note: sent.messageId ? "The email was accepted by GoHighLevel. Client receipt has not been verified." : "The send result is uncertain. Check Conversations before trying again." };
33966
34457
  }
33967
- var esc5;
34458
+ var esc6;
33968
34459
  var init_handoff_delivery = __esm({
33969
34460
  "src/command-os/handoff-delivery.ts"() {
33970
34461
  "use strict";
33971
34462
  init_build_contract();
33972
- esc5 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
34463
+ esc6 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
33973
34464
  }
33974
34465
  });
33975
34466
 
@@ -35469,8 +35960,8 @@ function parseCalendarLines(text2, knownStaffNames = []) {
35469
35960
  let line3 = rawLine.replace(/^[\s\-*•]+/, "").trim();
35470
35961
  if (!line3) continue;
35471
35962
  line3 = line3.replace(LEADING_COUNT_WITH_NOUN, "").replace(LEADING_COUNT_BEFORE_MARKER, "");
35472
- for (const marked2 of splitOnMarkers(line3)) {
35473
- for (const clause of splitOnAnd(marked2)) {
35963
+ for (const marked3 of splitOnMarkers(line3)) {
35964
+ for (const clause of splitOnAnd(marked3)) {
35474
35965
  const cleaned = clause.replace(/^\(?\d{1,2}[).]\s+/, "").replace(/^[\s\-*•]+/, "").trim();
35475
35966
  if (cleaned) entries.push(cleaned);
35476
35967
  }
@@ -37866,22 +38357,22 @@ function agencyProfilePageHtml(p, clients = [], seatToken = "") {
37866
38357
  const floor = c.engagement ? floorFor(c.engagement, p) : void 0;
37867
38358
  const cur = e.currency || p.pricing?.currency || "";
37868
38359
  const recorded = Object.keys(e).filter((k) => k !== "updated_at").length > 0;
37869
- return ` <details class="cl" data-id="${esc6(c.locationId)}"${recorded ? "" : ' data-empty="1"'}>
37870
- <summary><span class="cn">${esc6(c.name)}</span> <span class="cs">${recorded ? esc6([e.offer, e.price !== void 0 ? `${fmt(e.price, cur)}${e.cadence === "monthly" ? "/mo" : ""}` : "", e.status && e.status !== "active" ? e.status : ""].filter(Boolean).join(" \xB7 ") || "recorded") : "nothing recorded yet"}</span>${under && floor !== void 0 ? `<span class="warn">under your floor of ${esc6(fmt(floor, cur))}</span>` : ""}<span class="open">Open</span></summary>
38360
+ return ` <details class="cl" data-id="${esc7(c.locationId)}"${recorded ? "" : ' data-empty="1"'}>
38361
+ <summary><span class="cn">${esc7(c.name)}</span> <span class="cs">${recorded ? esc7([e.offer, e.price !== void 0 ? `${fmt(e.price, cur)}${e.cadence === "monthly" ? "/mo" : ""}` : "", e.status && e.status !== "active" ? e.status : ""].filter(Boolean).join(" \xB7 ") || "recorded") : "nothing recorded yet"}</span>${under && floor !== void 0 ? `<span class="warn">under your floor of ${esc7(fmt(floor, cur))}</span>` : ""}<span class="open">Open</span></summary>
37871
38362
  <div class="row">
37872
38363
  <div><label>What this is</label><select class="f" data-k="kind"><option value="client"${(e.kind ?? "client") === "client" ? " selected" : ""}>A client whose GoHighLevel account you run</option><option value="software"${e.kind === "software" ? " selected" : ""}>Software they bought from you</option><option value="other"${e.kind === "other" ? " selected" : ""}>Other income: event fee, revenue share, training</option></select></div>
37873
- <div><label>Offer</label><input class="f" data-k="offer" value="${esc6(e.offer)}" placeholder="${esc6(p.offers?.[0]?.name || "Speed to Lead")}"></div>
37874
- <div><label>They pay</label><input class="f" data-k="price" inputmode="numeric" value="${esc6(e.price)}"></div>
38364
+ <div><label>Offer</label><input class="f" data-k="offer" value="${esc7(e.offer)}" placeholder="${esc7(p.offers?.[0]?.name || "Speed to Lead")}"></div>
38365
+ <div><label>They pay</label><input class="f" data-k="price" inputmode="numeric" value="${esc7(e.price)}"></div>
37875
38366
  <div><label>How often</label><select class="f" data-k="cadence"><option value=""${e.cadence ? "" : " selected"}>.</option><option value="monthly"${e.cadence === "monthly" ? " selected" : ""}>Monthly</option><option value="one-time"${e.cadence === "one-time" ? " selected" : ""}>One-time</option></select></div>
37876
38367
  </div>
37877
38368
  <div class="row">
37878
- <div><label>Started</label><input class="f" data-k="startedOn" placeholder="2026-01-15" value="${esc6(e.startedOn)}"></div>
37879
- <div><label>Renews</label><input class="f" data-k="renewsOn" placeholder="2027-01-15" value="${esc6(e.renewsOn)}"></div>
38369
+ <div><label>Started</label><input class="f" data-k="startedOn" placeholder="2026-01-15" value="${esc7(e.startedOn)}"></div>
38370
+ <div><label>Renews</label><input class="f" data-k="renewsOn" placeholder="2027-01-15" value="${esc7(e.renewsOn)}"></div>
37880
38371
  <div><label>Status</label><select class="f" data-k="status"><option value=""${e.status ? "" : " selected"}>.</option><option value="active"${e.status === "active" ? " selected" : ""}>Active</option><option value="paused"${e.status === "paused" ? " selected" : ""}>Paused</option><option value="ended"${e.status === "ended" ? " selected" : ""}>Ended</option></select></div>
37881
- <div><label>Who runs it</label><input class="f" data-k="owner" value="${esc6(e.owner)}" placeholder="${esc6(p.staff?.[0]?.name || "")}"></div>
38372
+ <div><label>Who runs it</label><input class="f" data-k="owner" value="${esc7(e.owner)}" placeholder="${esc7(p.staff?.[0]?.name || "")}"></div>
37882
38373
  </div>
37883
38374
  <label>Anything specific to this client: one per line</label>
37884
- <textarea class="f" data-k="sops" placeholder="Call the owner before any send">${esc6((e.sops ?? []).join("\n"))}</textarea>
38375
+ <textarea class="f" data-k="sops" placeholder="Call the owner before any send">${esc7((e.sops ?? []).join("\n"))}</textarea>
37885
38376
  <div class="row" style="margin-top:.6rem"><div><button type="button" class="save-cl">Save this client</button> <span class="cmsg small"></span></div></div>
37886
38377
  </details>`;
37887
38378
  }).join("\n");
@@ -37933,34 +38424,34 @@ ${cockpitHeaderHtml("agency")}
37933
38424
  </fieldset>
37934
38425
  <fieldset><legend>Brand</legend>
37935
38426
  <label for="agencyName">Agency name, as clients see it</label>
37936
- <input id="agencyName" name="agencyName" value="${esc6(p.agencyName)}" placeholder="Your Agency">
38427
+ <input id="agencyName" name="agencyName" value="${esc7(p.agencyName)}" placeholder="Your Agency">
37937
38428
  <div class="row">
37938
- <div><label for="accent">Brand colour</label><input type="color" id="accent" name="accent" value="${esc6(p.accent || "#b8934a")}"></div>
37939
- <div><label for="website">Website</label><input id="website" name="website" value="${esc6(p.website)}" placeholder="https://youragency.com"></div>
38429
+ <div><label for="accent">Brand colour</label><input type="color" id="accent" name="accent" value="${esc7(p.accent || "#b8934a")}"></div>
38430
+ <div><label for="website">Website</label><input id="website" name="website" value="${esc7(p.website)}" placeholder="https://youragency.com"></div>
37940
38431
  </div>
37941
38432
  <label for="logoUrl">Logo URL</label>
37942
- <input id="logoUrl" name="logoUrl" value="${esc6(p.logoUrl)}" placeholder="https://youragency.com/logo.svg">
38433
+ <input id="logoUrl" name="logoUrl" value="${esc7(p.logoUrl)}" placeholder="https://youragency.com/logo.svg">
37943
38434
  <p class="hint">Must start with https. A logo link that isn't https is refused, because this one goes on pages your prospects open.</p>
37944
38435
  <label for="legalName">Registered entity, if different</label>
37945
- <input id="legalName" name="legalName" value="${esc6(p.legalName)}" placeholder="Your Agency LLC, used on contracts and A2P">
38436
+ <input id="legalName" name="legalName" value="${esc7(p.legalName)}" placeholder="Your Agency LLC, used on contracts and A2P">
37946
38437
  </fieldset>
37947
38438
 
37948
38439
  <fieldset><legend>Who messages come from</legend>
37949
38440
  <div class="row">
37950
- <div><label for="fromName">From name</label><input id="fromName" name="fromName" value="${esc6(p.sender?.fromName)}" placeholder="Dr Pat Owner"></div>
37951
- <div><label for="replyTo">Reply-to email</label><input id="replyTo" name="replyTo" value="${esc6(p.sender?.replyTo)}"></div>
37952
- <div><label for="phone">Phone</label><input id="phone" name="phone" value="${esc6(p.sender?.phone)}"></div>
38441
+ <div><label for="fromName">From name</label><input id="fromName" name="fromName" value="${esc7(p.sender?.fromName)}" placeholder="Dr Pat Owner"></div>
38442
+ <div><label for="replyTo">Reply-to email</label><input id="replyTo" name="replyTo" value="${esc7(p.sender?.replyTo)}"></div>
38443
+ <div><label for="phone">Phone</label><input id="phone" name="phone" value="${esc7(p.sender?.phone)}"></div>
37953
38444
  </div>
37954
38445
  </fieldset>
37955
38446
 
37956
38447
  <fieldset><legend>What you sell</legend>
37957
38448
  <label for="offers">Offers: one per line</label>
37958
- <textarea id="offers" name="offers" placeholder="Speed to Lead - 3000/mo&#10;Reactivation campaign - 2500">${esc6(offers)}</textarea>
38449
+ <textarea id="offers" name="offers" placeholder="Speed to Lead - 3000/mo&#10;Reactivation campaign - 2500">${esc7(offers)}</textarea>
37959
38450
  <p class="hint">Write them as "name - price". A price ending in /mo is recorded as monthly.</p>
37960
38451
  <div class="row">
37961
- <div><label for="minMonthly">Minimum monthly</label><input id="minMonthly" name="minMonthly" inputmode="numeric" value="${esc6(p.pricing?.minMonthly)}"></div>
37962
- <div><label for="minSetup">Minimum setup fee</label><input id="minSetup" name="minSetup" inputmode="numeric" value="${esc6(p.pricing?.minSetup)}"></div>
37963
- <div><label for="currency">Currency</label><input id="currency" name="currency" value="${esc6(p.pricing?.currency || "USD")}"></div>
38452
+ <div><label for="minMonthly">Minimum monthly</label><input id="minMonthly" name="minMonthly" inputmode="numeric" value="${esc7(p.pricing?.minMonthly)}"></div>
38453
+ <div><label for="minSetup">Minimum setup fee</label><input id="minSetup" name="minSetup" inputmode="numeric" value="${esc7(p.pricing?.minSetup)}"></div>
38454
+ <div><label for="currency">Currency</label><input id="currency" name="currency" value="${esc7(p.pricing?.currency || "USD")}"></div>
37964
38455
  </div>
37965
38456
  <p class="hint">The floors are the numbers below which you don't take the work. Anything quoting a price checks them first.</p>
37966
38457
  </fieldset>
@@ -37968,58 +38459,58 @@ ${cockpitHeaderHtml("agency")}
37968
38459
  <fieldset><legend>Your brand: who you are</legend>
37969
38460
  <p class="hint">Filled in once, in your own words. Used on the assessment report, in proposals, in every Claude session's opening brief, and by every Command OS build.</p>
37970
38461
  <label for="story">Your story: why this agency exists, in two or three sentences</label>
37971
- <textarea id="story" name="story" placeholder="I ran a clinic for twelve years and watched good practices lose patients to voicemail\u2026">${esc6(os8.story)}</textarea>
38462
+ <textarea id="story" name="story" placeholder="I ran a clinic for twelve years and watched good practices lose patients to voicemail\u2026">${esc7(os8.story)}</textarea>
37972
38463
  <label for="method">Your method: how you get results, and what you call it</label>
37973
- <textarea id="method" name="method" placeholder="The Full Room method: one weekend, a full calendar, a system that keeps booking after we leave">${esc6(os8.method)}</textarea>
38464
+ <textarea id="method" name="method" placeholder="The Full Room method: one weekend, a full calendar, a system that keeps booking after we leave">${esc7(os8.method)}</textarea>
37974
38465
  <div class="two">
37975
- <div><label for="voice">How you sound: three words, then a sentence</label><textarea id="voice" name="voice" placeholder="Direct, warm, clinical. Specifics over hype.">${esc6(os8.voice)}</textarea></div>
37976
- <div><label for="goals">Your goal for the next twelve months</label><textarea id="goals" name="goals" placeholder="$50k a month from 51 subscribers">${esc6(os8.goals)}</textarea></div>
38466
+ <div><label for="voice">How you sound: three words, then a sentence</label><textarea id="voice" name="voice" placeholder="Direct, warm, clinical. Specifics over hype.">${esc7(os8.voice)}</textarea></div>
38467
+ <div><label for="goals">Your goal for the next twelve months</label><textarea id="goals" name="goals" placeholder="$50k a month from 51 subscribers">${esc7(os8.goals)}</textarea></div>
37977
38468
  </div>
37978
38469
  <label for="values">What you stand for and what you refuse to do: one per line</label>
37979
- <textarea id="values" name="values" placeholder="Never a false all-clear&#10;No cold texting anyone who did not opt in">${esc6(lines(os8.values))}</textarea>
38470
+ <textarea id="values" name="values" placeholder="Never a false all-clear&#10;No cold texting anyone who did not opt in">${esc7(lines(os8.values))}</textarea>
37980
38471
  <label for="proof">Proof you may state, with permission: one per line</label>
37981
- <textarea id="proof" name="proof" placeholder="Radiance Med Spa: 34 consults booked in one weekend (permission on file)">${esc6(lines(os8.proof))}</textarea>
38472
+ <textarea id="proof" name="proof" placeholder="Radiance Med Spa: 34 consults booked in one weekend (permission on file)">${esc7(lines(os8.proof))}</textarea>
37982
38473
  </fieldset>
37983
38474
 
37984
38475
  <fieldset><legend>Ideal client: who you serve</legend>
37985
38476
  <label for="icaWho">Who they are: role, business, size, where</label>
37986
- <textarea id="icaWho" name="icaWho" placeholder="The owner of a two-provider med spa in a mid-size US city, doing $40k a month and answering the phone herself">${esc6(os8.ica?.who)}</textarea>
38477
+ <textarea id="icaWho" name="icaWho" placeholder="The owner of a two-provider med spa in a mid-size US city, doing $40k a month and answering the phone herself">${esc7(os8.ica?.who)}</textarea>
37987
38478
  <div class="two">
37988
- <div><label for="icaPains">Their pains, worst first: one per line</label><textarea id="icaPains" name="icaPains">${esc6(lines(os8.ica?.pains))}</textarea></div>
37989
- <div><label for="icaDesires">What they want, most first: one per line</label><textarea id="icaDesires" name="icaDesires">${esc6(lines(os8.ica?.desires))}</textarea></div>
38479
+ <div><label for="icaPains">Their pains, worst first: one per line</label><textarea id="icaPains" name="icaPains">${esc7(lines(os8.ica?.pains))}</textarea></div>
38480
+ <div><label for="icaDesires">What they want, most first: one per line</label><textarea id="icaDesires" name="icaDesires">${esc7(lines(os8.ica?.desires))}</textarea></div>
37990
38481
  </div>
37991
38482
  <div class="two">
37992
- <div><label for="icaObjections">Why they say no: one per line</label><textarea id="icaObjections" name="icaObjections">${esc6(lines(os8.ica?.objections))}</textarea></div>
37993
- <div><label for="icaTriggers">What makes them buy now: one per line</label><textarea id="icaTriggers" name="icaTriggers">${esc6(lines(os8.ica?.triggers))}</textarea></div>
38483
+ <div><label for="icaObjections">Why they say no: one per line</label><textarea id="icaObjections" name="icaObjections">${esc7(lines(os8.ica?.objections))}</textarea></div>
38484
+ <div><label for="icaTriggers">What makes them buy now: one per line</label><textarea id="icaTriggers" name="icaTriggers">${esc7(lines(os8.ica?.triggers))}</textarea></div>
37994
38485
  </div>
37995
38486
  <div class="two">
37996
- <div><label for="icaVocabulary">Their own words for the problem: one per line</label><textarea id="icaVocabulary" name="icaVocabulary">${esc6(lines(os8.ica?.vocabulary))}</textarea></div>
37997
- <div><label for="icaHangouts">Where they read, listen and gather: one per line</label><textarea id="icaHangouts" name="icaHangouts">${esc6(lines(os8.ica?.hangouts))}</textarea></div>
38487
+ <div><label for="icaVocabulary">Their own words for the problem: one per line</label><textarea id="icaVocabulary" name="icaVocabulary">${esc7(lines(os8.ica?.vocabulary))}</textarea></div>
38488
+ <div><label for="icaHangouts">Where they read, listen and gather: one per line</label><textarea id="icaHangouts" name="icaHangouts">${esc7(lines(os8.ica?.hangouts))}</textarea></div>
37998
38489
  </div>
37999
38490
  <label for="icaVillains">What they have tried and blame: one per line</label>
38000
- <textarea id="icaVillains" name="icaVillains" placeholder="The last agency that charged $3,000 a month for two posts a week">${esc6(lines(os8.ica?.villains))}</textarea>
38491
+ <textarea id="icaVillains" name="icaVillains" placeholder="The last agency that charged $3,000 a month for two posts a week">${esc7(lines(os8.ica?.villains))}</textarea>
38001
38492
  </fieldset>
38002
38493
 
38003
38494
  <fieldset><legend>The offer: what you promise</legend>
38004
38495
  <div class="two">
38005
- <div><label for="offerName">The offer's name</label><input id="offerName" name="offerName" value="${esc6(os8.offer?.name)}" placeholder="The Full Room"></div>
38006
- <div><label for="offerPrice">Price, and what it is per</label><input id="offerPrice" name="offerPrice" value="${esc6(os8.offer?.price)}" placeholder="$5,000 per event plus 30% of revenue"></div>
38496
+ <div><label for="offerName">The offer's name</label><input id="offerName" name="offerName" value="${esc7(os8.offer?.name)}" placeholder="The Full Room"></div>
38497
+ <div><label for="offerPrice">Price, and what it is per</label><input id="offerPrice" name="offerPrice" value="${esc7(os8.offer?.price)}" placeholder="$5,000 per event plus 30% of revenue"></div>
38007
38498
  </div>
38008
38499
  <label for="offerPromise">The promise: one sentence a prospect could repeat</label>
38009
- <textarea id="offerPromise" name="offerPromise" placeholder="A full room in one weekend, and a system that keeps booking after we leave">${esc6(os8.offer?.promise)}</textarea>
38500
+ <textarea id="offerPromise" name="offerPromise" placeholder="A full room in one weekend, and a system that keeps booking after we leave">${esc7(os8.offer?.promise)}</textarea>
38010
38501
  <label for="offerStack">What is in it: one per line</label>
38011
- <textarea id="offerStack" name="offerStack" placeholder="The campaign page&#10;The AI receptionist&#10;The 30-day nurture">${esc6(lines(os8.offer?.valueStack))}</textarea>
38502
+ <textarea id="offerStack" name="offerStack" placeholder="The campaign page&#10;The AI receptionist&#10;The 30-day nurture">${esc7(lines(os8.offer?.valueStack))}</textarea>
38012
38503
  <div class="two">
38013
- <div><label for="offerGuarantee">The guarantee, exactly as you would say it</label><textarea id="offerGuarantee" name="offerGuarantee">${esc6(os8.offer?.guarantee)}</textarea></div>
38014
- <div><label for="offerAccelerators">Why now: bonuses, deadlines, scarcity, one per line</label><textarea id="offerAccelerators" name="offerAccelerators">${esc6(lines(os8.offer?.accelerators))}</textarea></div>
38504
+ <div><label for="offerGuarantee">The guarantee, exactly as you would say it</label><textarea id="offerGuarantee" name="offerGuarantee">${esc7(os8.offer?.guarantee)}</textarea></div>
38505
+ <div><label for="offerAccelerators">Why now: bonuses, deadlines, scarcity, one per line</label><textarea id="offerAccelerators" name="offerAccelerators">${esc7(lines(os8.offer?.accelerators))}</textarea></div>
38015
38506
  </div>
38016
38507
  </fieldset>
38017
38508
 
38018
38509
  <fieldset><legend>How you deliver</legend>
38019
38510
  <label for="sops">Delivery SOPs: one per line</label>
38020
- <textarea id="sops" name="sops" placeholder="Every build gets a rollback journal before go-live">${esc6(sops)}</textarea>
38511
+ <textarea id="sops" name="sops" placeholder="Every build gets a rollback journal before go-live">${esc7(sops)}</textarea>
38021
38512
  <label for="staff">Staff: one per line, "name \xB7 role \xB7 email"</label>
38022
- <textarea id="staff" name="staff" placeholder="Dr Pat Owner \xB7 owner \xB7 pat@youragency.com">${esc6(staff)}</textarea>
38513
+ <textarea id="staff" name="staff" placeholder="Dr Pat Owner \xB7 owner \xB7 pat@youragency.com">${esc7(staff)}</textarea>
38023
38514
  <p class="hint">Who may approve what is recorded here for reference. The approval gates themselves are still enforced by the ritual, not by this list.</p>
38024
38515
  </fieldset>
38025
38516
 
@@ -38163,7 +38654,7 @@ document.querySelectorAll(".save-cl").forEach(btn=>btn.addEventListener("click",
38163
38654
  </script>
38164
38655
  <script>${COCKPIT_TYPE_JS}</script></body></html>`;
38165
38656
  }
38166
- var READ_NUMBER_JS, fmt, esc6;
38657
+ var READ_NUMBER_JS, fmt, esc7;
38167
38658
  var init_agency_profile_page = __esm({
38168
38659
  "src/agency-profile-page.ts"() {
38169
38660
  "use strict";
@@ -38180,7 +38671,7 @@ var init_agency_profile_page = __esm({
38180
38671
  return Number.isFinite(n)?{value:n}:{bad:true};
38181
38672
  }`;
38182
38673
  fmt = (n, currency) => `${currency ? currency + " " : ""}${n.toLocaleString("en-US")}`;
38183
- esc6 = (x) => String(x ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
38674
+ esc7 = (x) => String(x ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
38184
38675
  }
38185
38676
  });
38186
38677
 
@@ -40429,12 +40920,12 @@ Seed while GHL Command is stopped, or restart it afterwards.`;
40429
40920
 
40430
40921
  // src/command-os/design/brand-page.ts
40431
40922
  function head(title2) {
40432
- return `${cockpitHeadHtml(`${esc7(title2)} \u2014 Command OS`, { extraCss: STYLE })}<body>
40923
+ return `${cockpitHeadHtml(`${esc8(title2)} \u2014 Command OS`, { extraCss: STYLE })}<body>
40433
40924
  ${cockpitHeaderHtml("brands")}<main>`;
40434
40925
  }
40435
40926
  function brandsPageHtml(rows2, house, refs, seatToken = "") {
40436
- const row = (r) => `<div class="card">${r.problem ? `<b>${esc7(r.slug)}</b><p class="small">Cannot be read: ${esc7(r.problem)}</p>` : `<a class="title" href="/brand/${encodeURIComponent(r.slug)}">${esc7(r.name)}</a><div class="sw"><span style="background:${esc7(r.accent)}" title="accent ${esc7(r.accent)}"></span></div><div class="small">${esc7(r.display)} \xB7 ${esc7(r.body)} \xB7 ${esc7(r.scheme)}</div><div style="margin-top:8px"><span class="pill">${esc7(r.extractedVia)}</span><span class="pill ${r.confidence >= 0.7 ? "ok" : "warn"}">confidence ${Math.round(r.confidence * 100)}%</span></div>`}</div>`;
40437
- const houseRow = (d) => `<div class="card"><b>${esc7(d.name)}</b> <span class="small">house look</span><div class="sw"><span style="background:${d.colors.bg.hex}" title="background"></span><span style="background:${d.colors.fg.hex}" title="text"></span><span style="background:${d.colors.accent.hex}" title="accent"></span><span style="background:${d.colors.surface.hex}" title="surface"></span></div><div class="small">${esc7(d.typography.display.family)} \xB7 ${esc7(d.typography.body.family)} \xB7 ${esc7(d.voice.tone ?? "")}</div><div style="margin-top:10px"><button class="ghost" data-house="${esc7(d.slug)}">Copy as a brand</button></div></div>`;
40927
+ const row = (r) => `<div class="card">${r.problem ? `<b>${esc8(r.slug)}</b><p class="small">Cannot be read: ${esc8(r.problem)}</p>` : `<a class="title" href="/brand/${encodeURIComponent(r.slug)}">${esc8(r.name)}</a><div class="sw"><span style="background:${esc8(r.accent)}" title="accent ${esc8(r.accent)}"></span></div><div class="small">${esc8(r.display)} \xB7 ${esc8(r.body)} \xB7 ${esc8(r.scheme)}</div><div style="margin-top:8px"><span class="pill">${esc8(r.extractedVia)}</span><span class="pill ${r.confidence >= 0.7 ? "ok" : "warn"}">confidence ${Math.round(r.confidence * 100)}%</span></div>`}</div>`;
40928
+ const houseRow = (d) => `<div class="card"><b>${esc8(d.name)}</b> <span class="small">house look</span><div class="sw"><span style="background:${d.colors.bg.hex}" title="background"></span><span style="background:${d.colors.fg.hex}" title="text"></span><span style="background:${d.colors.accent.hex}" title="accent"></span><span style="background:${d.colors.surface.hex}" title="surface"></span></div><div class="small">${esc8(d.typography.display.family)} \xB7 ${esc8(d.typography.body.family)} \xB7 ${esc8(d.voice.tone ?? "")}</div><div style="margin-top:10px"><button class="ghost" data-house="${esc8(d.slug)}">Copy as a brand</button></div></div>`;
40438
40929
  return `${head("Brands")}
40439
40930
  <h1>Brands</h1>
40440
40931
  <p class="muted">The shelf every deck, page and report starts from. Pull a client's brand from their website, or start from a reference look and put the client's own logo and accent on it.</p>
@@ -40451,7 +40942,7 @@ ${rows2.length ? `<div class="grid">${rows2.map(row).join("")}</div>` : `<p clas
40451
40942
  <div class="grid">${house.map(houseRow).join("")}</div>
40452
40943
  <h2>Reference looks</h2>
40453
40944
  <p class="small">${refs.length} real-brand palettes (Apple, Notion, Stripe, Linear\u2026) fetched from a pinned public source when you pick one. Colours, type and shape only: no logo, and the pages you make never name the reference brand.</p>
40454
- <div class="refs" id="refs">${refs.map((r) => `<button class="ghost" data-look="${esc7(r.slug)}" title="${esc7(r.website)}">${esc7(r.name)}</button>`).join("")}</div>
40945
+ <div class="refs" id="refs">${refs.map((r) => `<button class="ghost" data-look="${esc8(r.slug)}" title="${esc8(r.website)}">${esc8(r.name)}</button>`).join("")}</div>
40455
40946
  <div class="status" id="look-status"></div>
40456
40947
  ${foot(SHELL_JS(seatToken) + `
40457
40948
  document.getElementById("extract").addEventListener("submit",async(e)=>{e.preventDefault();const b=document.getElementById("extract-btn");b.disabled=true;say("extract-status","Reading the site\u2026 this takes a few seconds, longer with the browser pass.");try{const j=await post("/api/brand/extract",{url:document.getElementById("url").value,rights:document.getElementById("rights").value,name:document.getElementById("name").value||undefined,useBrowser:document.getElementById("useBrowser").checked});say("extract-status","Stored as "+j.brand.slug+". Opening the card\u2026","ok");location.href="/brand/"+encodeURIComponent(j.brand.slug)}catch(err){say("extract-status",err.message,"err")}finally{b.disabled=false}});
@@ -40461,26 +40952,26 @@ for(const btn of document.querySelectorAll("[data-house]")){btn.addEventListener
40461
40952
  }
40462
40953
  function brandCardHtml(d, opts = {}) {
40463
40954
  const c = d.colors;
40464
- const sw = (label, t) => `<tr><td><span class="sw" style="margin:0"><span style="background:${t.hex}"></span></span></td><td>${esc7(label)}</td><td class="hex">${esc7(t.hex)}</td><td class="small">${esc7(t.source)}</td></tr>`;
40465
- const face = (label, f) => `<tr><td>${esc7(label)}</td><td><b>${esc7(f.family)}</b></td><td>${f.renderIntent === "google" ? `<span class="pill ok">Google Fonts</span>` : `<span class="pill warn">${f.renderIntent === "licensed-webfont" ? "licensed" : "system"}</span> renders as <b>${esc7(renderedFamily(f))}</b>`}</td><td class="small">${esc7(f.source)}</td></tr>`;
40466
- const pairs = d.contrast.pairs.map((p) => `<tr><td class="hex">${esc7(p.fg)} on ${esc7(p.bg)}</td><td>${p.ratio}:1 ${p.ratio >= 4.5 ? `<span class="pill ok">AA</span>` : `<span class="pill warn">under 4.5</span>`}</td><td class="small">${p.correctedFg ? `text shade corrected to <span class="hex">${esc7(p.correctedFg)}</span> (${p.correctedRatio}:1)` : ""}</td></tr>`).join("");
40955
+ const sw = (label, t) => `<tr><td><span class="sw" style="margin:0"><span style="background:${t.hex}"></span></span></td><td>${esc8(label)}</td><td class="hex">${esc8(t.hex)}</td><td class="small">${esc8(t.source)}</td></tr>`;
40956
+ const face = (label, f) => `<tr><td>${esc8(label)}</td><td><b>${esc8(f.family)}</b></td><td>${f.renderIntent === "google" ? `<span class="pill ok">Google Fonts</span>` : `<span class="pill warn">${f.renderIntent === "licensed-webfont" ? "licensed" : "system"}</span> renders as <b>${esc8(renderedFamily(f))}</b>`}</td><td class="small">${esc8(f.source)}</td></tr>`;
40957
+ const pairs = d.contrast.pairs.map((p) => `<tr><td class="hex">${esc8(p.fg)} on ${esc8(p.bg)}</td><td>${p.ratio}:1 ${p.ratio >= 4.5 ? `<span class="pill ok">AA</span>` : `<span class="pill warn">under 4.5</span>`}</td><td class="small">${p.correctedFg ? `text shade corrected to <span class="hex">${esc8(p.correctedFg)}</span> (${p.correctedRatio}:1)` : ""}</td></tr>`).join("");
40467
40958
  const cleanSvg = d.logo?.inlineSvg ? sanitizeSvgOrDrop(d.logo.inlineSvg) : void 0;
40468
- const logo = cleanSvg ? `<img src="data:image/svg+xml;base64,${Buffer.from(cleanSvg.replace(/currentColor/g, c.fg.hex)).toString("base64")}" alt="">` : d.logo?.inlineSvg ? `<span class="small">the stored logo is not a plain drawing, so it is not shown</span>` : isRasterDataUri(d.logo?.dataUri) ? `<img src="${esc7(d.logo.dataUri)}" alt="">` : d.logo?.dataUri ? `<span class="small">the stored logo image is not a plain picture, so it is not shown</span>` : `<span class="small">no logo stored</span>`;
40959
+ const logo = cleanSvg ? `<img src="data:image/svg+xml;base64,${Buffer.from(cleanSvg.replace(/currentColor/g, c.fg.hex)).toString("base64")}" alt="">` : d.logo?.inlineSvg ? `<span class="small">the stored logo is not a plain drawing, so it is not shown</span>` : isRasterDataUri(d.logo?.dataUri) ? `<img src="${esc8(d.logo.dataUri)}" alt="">` : d.logo?.dataUri ? `<span class="small">the stored logo image is not a plain picture, so it is not shown</span>` : `<span class="small">no logo stored</span>`;
40469
40960
  return `${head(`${d.name} \xB7 brand`)}
40470
- <h1>${esc7(d.name)}</h1>
40471
- <p class="muted">${esc7(d.website ?? "")} \xB7 <span class="pill">${esc7(d.provenance.extractedVia)}</span><span class="pill ${d.provenance.confidence >= 0.7 ? "ok" : "warn"}">confidence ${Math.round(d.provenance.confidence * 100)}%</span> ${esc7(d.scheme)} scheme \xB7 ${esc7(d.provenance.extractedAt.slice(0, 10))}</p>
40472
- ${d.pageStudioTheme.warnings.length ? `<div class="card" style="border-left:4px solid var(--warn)"><b>Before this reaches a client:</b><ul>${d.pageStudioTheme.warnings.map((w) => `<li>${esc7(w)}</li>`).join("")}</ul></div>` : ""}
40961
+ <h1>${esc8(d.name)}</h1>
40962
+ <p class="muted">${esc8(d.website ?? "")} \xB7 <span class="pill">${esc8(d.provenance.extractedVia)}</span><span class="pill ${d.provenance.confidence >= 0.7 ? "ok" : "warn"}">confidence ${Math.round(d.provenance.confidence * 100)}%</span> ${esc8(d.scheme)} scheme \xB7 ${esc8(d.provenance.extractedAt.slice(0, 10))}</p>
40963
+ ${d.pageStudioTheme.warnings.length ? `<div class="card" style="border-left:4px solid var(--warn)"><b>Before this reaches a client:</b><ul>${d.pageStudioTheme.warnings.map((w) => `<li>${esc8(w)}</li>`).join("")}</ul></div>` : ""}
40473
40964
  <div class="two">
40474
40965
  <div><h2>Colours</h2><table><tr><th></th><th>Role</th><th>Hex</th><th>Where it came from</th></tr>${sw("Background", c.bg)}${sw("Text", c.fg)}${sw("Accent", c.accent)}${sw("Muted text", c.muted)}${sw("Border", c.border)}${sw("Surface", c.surface)}${sw("Accent, soft", c.primarySoft)}</table>
40475
40966
  <h2>Contrast</h2><table><tr><th>Pair</th><th>Ratio</th><th></th></tr>${pairs}</table></div>
40476
40967
  <div><h2>Type</h2><table><tr><th></th><th>Face</th><th>Renders as</th><th>Source</th></tr>${face("Display", d.typography.display)}${face("Body", d.typography.body)}</table>
40477
- <h2>Logo</h2><div class="logo-box" style="color:${c.fg.hex};background:${c.bg.hex}">${logo}</div><p class="small">${esc7(d.provenance.evidence["logo"] ?? "")}${d.logo?.whiteFillFixed ? " \xB7 white fills replaced with currentColor" : ""}</p>
40478
- <h2>Shape</h2><p>Radius ${d.shape.radius.sm} / ${d.shape.radius.md} / ${d.shape.radius.lg} px \xB7 shadows ${esc7(d.shape.shadows)} \xB7 ${d.shape.gridBase}-point grid</p>
40479
- ${d.voice.samples.length ? `<h2>Voice</h2><ul>${d.voice.samples.map((s) => `<li>${esc7(s)}</li>`).join("")}</ul>` : ""}${d.voice.tone ? `<p class="small">Tone: ${esc7(d.voice.tone)}${d.voice.audience ? ` \xB7 Audience: ${esc7(d.voice.audience)}` : ""}</p>` : ""}</div>
40968
+ <h2>Logo</h2><div class="logo-box" style="color:${c.fg.hex};background:${c.bg.hex}">${logo}</div><p class="small">${esc8(d.provenance.evidence["logo"] ?? "")}${d.logo?.whiteFillFixed ? " \xB7 white fills replaced with currentColor" : ""}</p>
40969
+ <h2>Shape</h2><p>Radius ${d.shape.radius.sm} / ${d.shape.radius.md} / ${d.shape.radius.lg} px \xB7 shadows ${esc8(d.shape.shadows)} \xB7 ${d.shape.gridBase}-point grid</p>
40970
+ ${d.voice.samples.length ? `<h2>Voice</h2><ul>${d.voice.samples.map((s) => `<li>${esc8(s)}</li>`).join("")}</ul>` : ""}${d.voice.tone ? `<p class="small">Tone: ${esc8(d.voice.tone)}${d.voice.audience ? ` \xB7 Audience: ${esc8(d.voice.audience)}` : ""}</p>` : ""}</div>
40480
40971
  </div>
40481
- ${opts.screenshot ? `<h2>Their site, as rendered</h2><img class="shot" src="/brand/${encodeURIComponent(d.slug)}/screenshot.png" alt="Screenshot of ${esc7(d.name)}'s website">` : ""}
40972
+ ${opts.screenshot ? `<h2>Their site, as rendered</h2><img class="shot" src="/brand/${encodeURIComponent(d.slug)}/screenshot.png" alt="Screenshot of ${esc8(d.name)}'s website">` : ""}
40482
40973
  <h2>Make something in this brand</h2>
40483
- <p class="small">Runs on <b>your own Claude</b>, on this computer, never our key. ${esc7(RUN_EXPECTATION)} The run keeps going if you close this tab. Write the headline and the points you want made, not instructions.</p>
40974
+ <p class="small">Runs on <b>your own Claude</b>, on this computer, never our key. ${esc8(RUN_EXPECTATION)} The run keeps going if you close this tab. Write the headline and the points you want made, not instructions.</p>
40484
40975
  <form id="make" novalidate><div class="two">
40485
40976
  <div><label for="mk-medium">What</label><select id="mk-medium"><option value="deck">A deck</option><option value="site">A landing page</option><option value="report">A report</option></select></div>
40486
40977
  <div><label for="mk-slides">Slides (a deck only)</label><input id="mk-slides" type="number" min="${DESIGN_FORM_LIMITS.slidesMin}" max="${DESIGN_FORM_LIMITS.slidesMax}" step="1" value="6"></div>
@@ -40496,16 +40987,16 @@ ${opts.screenshot ? `<h2>Their site, as rendered</h2><img class="shot" src="/bra
40496
40987
  <div class="status" id="mk-status"></div><pre id="mk-log" class="small" style="white-space:pre-wrap;max-height:220px;overflow:auto;margin:8px 0 0" hidden></pre></form>
40497
40988
 
40498
40989
  <h2>Change something</h2>
40499
- <form id="override"><div class="two"><div><label for="accent">Accent (#rrggbb)</label><input id="accent" name="accent" type="text" placeholder="${esc7(c.accent.hex)}" autocomplete="off"></div><div><label for="displayFont">Display face</label><input id="displayFont" type="text" placeholder="${esc7(d.typography.display.family)}" autocomplete="off"></div><div><label for="bodyFont">Body face</label><input id="bodyFont" type="text" placeholder="${esc7(d.typography.body.family)}" autocomplete="off"></div><div><label for="brandName">Name</label><input id="brandName" type="text" placeholder="${esc7(d.name)}" autocomplete="off"></div></div>
40990
+ <form id="override"><div class="two"><div><label for="accent">Accent (#rrggbb)</label><input id="accent" name="accent" type="text" placeholder="${esc8(c.accent.hex)}" autocomplete="off"></div><div><label for="displayFont">Display face</label><input id="displayFont" type="text" placeholder="${esc8(d.typography.display.family)}" autocomplete="off"></div><div><label for="bodyFont">Body face</label><input id="bodyFont" type="text" placeholder="${esc8(d.typography.body.family)}" autocomplete="off"></div><div><label for="brandName">Name</label><input id="brandName" type="text" placeholder="${esc8(d.name)}" autocomplete="off"></div></div>
40500
40991
  <label for="logoSvg">Client's own logo (paste one &lt;svg&gt;)</label><input id="logoSvg" type="text" placeholder="<svg \u2026>\u2026</svg>" autocomplete="off">
40501
40992
  <div><button type="submit" id="save">Save changes</button></div><div class="status" id="save-status"></div></form>
40502
40993
  <h2>For Page Studio</h2>
40503
40994
  <p class="small">The six-slot theme <code>compose_page</code> and <code>compose_website</code> accept. A re-skin of the donor template, not a port of the design system.</p>
40504
- <pre class="card" style="overflow:auto;font-size:var(--fs-sm)">${esc7(JSON.stringify({ primary: d.pageStudioTheme.primary, primarySoft: d.pageStudioTheme.primarySoft, surface: d.pageStudioTheme.surface, textDark: d.pageStudioTheme.textDark, headingFont: d.pageStudioTheme.headingFont, bodyFont: d.pageStudioTheme.bodyFont }, null, 2))}</pre>
40995
+ <pre class="card" style="overflow:auto;font-size:var(--fs-sm)">${esc8(JSON.stringify({ primary: d.pageStudioTheme.primary, primarySoft: d.pageStudioTheme.primarySoft, surface: d.pageStudioTheme.surface, textDark: d.pageStudioTheme.textDark, headingFont: d.pageStudioTheme.headingFont, bodyFont: d.pageStudioTheme.bodyFont }, null, 2))}</pre>
40505
40996
  <h2>Where every value came from</h2>
40506
- <table><tr><th>Value</th><th>Source</th></tr>${Object.entries(d.provenance.evidence).map(([k, v]) => `<tr><td class="hex">${esc7(k)}</td><td class="small">${esc7(v)}</td></tr>`).join("")}</table>
40507
- ${d.provenance.source ? `<p class="small">Source: ${esc7(d.provenance.source.url)}${d.provenance.source.commit ? ` @ ${esc7(d.provenance.source.commit.slice(0, 7))}` : ""}${d.provenance.source.license ? ` \xB7 ${esc7(d.provenance.source.license)}` : ""}</p>` : ""}
40508
- ${opts.dir ? `<p class="small">Stored at ${esc7(opts.dir)}</p>` : ""}
40997
+ <table><tr><th>Value</th><th>Source</th></tr>${Object.entries(d.provenance.evidence).map(([k, v]) => `<tr><td class="hex">${esc8(k)}</td><td class="small">${esc8(v)}</td></tr>`).join("")}</table>
40998
+ ${d.provenance.source ? `<p class="small">Source: ${esc8(d.provenance.source.url)}${d.provenance.source.commit ? ` @ ${esc8(d.provenance.source.commit.slice(0, 7))}` : ""}${d.provenance.source.license ? ` \xB7 ${esc8(d.provenance.source.license)}` : ""}</p>` : ""}
40999
+ ${opts.dir ? `<p class="small">Stored at ${esc8(opts.dir)}</p>` : ""}
40509
41000
  ${foot(SHELL_JS(opts.seatToken ?? "") + `
40510
41001
  (function(){
40511
41002
  var poll=null,runId=null,started=0,ticker=null,passes=0;
@@ -40564,7 +41055,7 @@ document.getElementById("mk-cancel").addEventListener("click",async function(){
40564
41055
  document.getElementById("override").addEventListener("submit",async(e)=>{e.preventDefault();const g=(id)=>document.getElementById(id).value.trim();const body={slug:${scriptJson(d.slug)}};if(g("accent"))body.accent=g("accent");if(g("displayFont"))body.displayFont=g("displayFont");if(g("bodyFont"))body.bodyFont=g("bodyFont");if(g("brandName"))body.name=g("brandName");if(g("logoSvg"))body.logoSvg=g("logoSvg");if(Object.keys(body).length===1){say("save-status","Nothing to change.");return}const b=document.getElementById("save");b.disabled=true;try{await post("/api/brand/set",body);say("save-status","Saved.","ok");location.reload()}catch(err){say("save-status",err.message,"err");b.disabled=false}});
40565
41056
  `)}`;
40566
41057
  }
40567
- var esc7, STYLE, SHELL_JS, foot;
41058
+ var esc8, STYLE, SHELL_JS, foot;
40568
41059
  var init_brand_page = __esm({
40569
41060
  "src/command-os/design/brand-page.ts"() {
40570
41061
  "use strict";
@@ -40574,7 +41065,7 @@ var init_brand_page = __esm({
40574
41065
  init_cockpit_shell();
40575
41066
  init_script_json();
40576
41067
  init_svg_sanitize();
40577
- esc7 = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
41068
+ esc8 = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
40578
41069
  STYLE = `
40579
41070
  main{max-width:1080px;margin:0 auto;padding:28px 24px 80px}
40580
41071
  h1{font-size:1.5rem;margin:0 0 8px}h2{font-size:1.15rem;margin:32px 0 8px}h3{font-size:var(--fs-body);font-weight:700;margin:0 0 8px}
@@ -40815,9 +41306,9 @@ async function generateReview(spawnClaude, input) {
40815
41306
  function reviewDocumentHtml(review, opts) {
40816
41307
  const accent = opts.accent && /^#[0-9a-fA-F]{6}$/.test(opts.accent) ? opts.accent : "#B8934A";
40817
41308
  const agency = opts.agencyName?.trim() || "";
40818
- const esc10 = (v) => String(v ?? "").replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
41309
+ const esc11 = (v) => String(v ?? "").replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
40819
41310
  const date = (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, { year: "numeric", month: "long", day: "numeric" });
40820
- const gap = (g) => `<div class="gap ${g.priority}"><div class="gt">${esc10(g.title)}<span class="pri">${g.priority === "now" ? "Fix first" : g.priority === "soon" ? "Next" : "Later"}</span></div>${g.cost ? `<div class="gc">${esc10(g.cost)}</div>` : ""}${g.fix ? `<div class="gf">What we do: ${esc10(g.fix)}</div>` : ""}</div>`;
41311
+ const gap = (g) => `<div class="gap ${g.priority}"><div class="gt">${esc11(g.title)}<span class="pri">${g.priority === "now" ? "Fix first" : g.priority === "soon" ? "Next" : "Later"}</span></div>${g.cost ? `<div class="gc">${esc11(g.cost)}</div>` : ""}${g.fix ? `<div class="gf">What we do: ${esc11(g.fix)}</div>` : ""}</div>`;
40821
41312
  const o = review.offer;
40822
41313
  const confirmed = opts.scope && !opts.scope.wholePreset && !opts.scope.awaitingConfirmation ? opts.scope : void 0;
40823
41314
  const purchased = confirmed || opts.purchasedFeatureIds ? [.../* @__PURE__ */ new Set([
@@ -40827,8 +41318,8 @@ function reviewDocumentHtml(review, opts) {
40827
41318
  const unbuilt = opts.scope?.notBuilt ?? [];
40828
41319
  const represented = [...opts.scope?.built ?? [], ...unbuilt].map((f) => BUILD_ASSESSMENT_FEATURE[f.id]).concat(opts.scope && !opts.scope.awaitingConfirmation ? ["pipeline"] : []);
40829
41320
  const assessmentNext = opts.assessment && purchased ? assessmentFeatureLines(opts.assessment, [...purchased, ...represented]) : [];
40830
- const available = unbuilt.map((f) => `<div class="build"><b>${esc10(f.name)}</b><span>${esc10(f.what)}${f.figure ? ` <b>${esc10(f.figure)}</b>` : ""}</span></div>`).join("") + assessmentNext.map((line3) => `<p>${esc10(line3)}</p>`).join("");
40831
- return `<!doctype html><html><head><meta charset="utf-8"><title>${esc10(opts.clientName)} \u2014 Growth Blueprint</title><style>
41321
+ const available = unbuilt.map((f) => `<div class="build"><b>${esc11(f.name)}</b><span>${esc11(f.what)}${f.figure ? ` <b>${esc11(f.figure)}</b>` : ""}</span></div>`).join("") + assessmentNext.map((line3) => `<p>${esc11(line3)}</p>`).join("");
41322
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${esc11(opts.clientName)} \u2014 Growth Blueprint</title><style>
40832
41323
  @page{margin:0.6in}
40833
41324
  *{box-sizing:border-box}
40834
41325
  body{font-family:-apple-system,Segoe UI,Helvetica,sans-serif;color:#1f2430;margin:0;line-height:1.55}
@@ -40863,36 +41354,36 @@ function reviewDocumentHtml(review, opts) {
40863
41354
  <div class="wrap">
40864
41355
  <div class="cover">
40865
41356
  <p class="eyebrow">Growth Blueprint</p>
40866
- <h1>${esc10(opts.clientName)}</h1>
40867
- <p class="sub">${agency ? `Prepared by ${esc10(agency)} \xB7 ` : ""}${date}</p>
41357
+ <h1>${esc11(opts.clientName)}</h1>
41358
+ <p class="sub">${agency ? `Prepared by ${esc11(agency)} \xB7 ` : ""}${date}</p>
40868
41359
  </div>
40869
- ${opts.planLines?.length ? `<div class="head">Executable plan for approval</div>${opts.planLines.map((line3) => `<p style="white-space:pre-wrap">${esc10(line3)}</p>`).join("")}` : ""}
40870
- ${review.headline ? `<div class="lead">${esc10(review.headline)}</div>` : ""}
41360
+ ${opts.planLines?.length ? `<div class="head">Executable plan for approval</div>${opts.planLines.map((line3) => `<p style="white-space:pre-wrap">${esc11(line3)}</p>`).join("")}` : ""}
41361
+ ${review.headline ? `<div class="lead">${esc11(review.headline)}</div>` : ""}
40871
41362
  ${review.ica ? `<div class="head">Who you're really selling to</div>
40872
- <p>${esc10(review.ica.who)}</p>
40873
- ${review.ica.painPoints?.length ? `<p class="kv"><b>What keeps them up:</b> ${esc10(review.ica.painPoints.join(" \xB7 "))}</p>` : ""}
40874
- ${review.ica.objections?.length ? `<p class="kv"><b>Why they hesitate:</b> ${esc10(review.ica.objections.join(" \xB7 "))}</p>` : ""}
40875
- ${review.ica.buyingTrigger ? `<p class="kv"><b>What makes them finally buy:</b> ${esc10(review.ica.buyingTrigger)}</p>` : ""}
40876
- ${review.ica.whereTheyAre ? `<p class="kv"><b>Where they already are:</b> ${esc10(review.ica.whereTheyAre)}</p>` : ""}` : ""}
41363
+ <p>${esc11(review.ica.who)}</p>
41364
+ ${review.ica.painPoints?.length ? `<p class="kv"><b>What keeps them up:</b> ${esc11(review.ica.painPoints.join(" \xB7 "))}</p>` : ""}
41365
+ ${review.ica.objections?.length ? `<p class="kv"><b>Why they hesitate:</b> ${esc11(review.ica.objections.join(" \xB7 "))}</p>` : ""}
41366
+ ${review.ica.buyingTrigger ? `<p class="kv"><b>What makes them finally buy:</b> ${esc11(review.ica.buyingTrigger)}</p>` : ""}
41367
+ ${review.ica.whereTheyAre ? `<p class="kv"><b>Where they already are:</b> ${esc11(review.ica.whereTheyAre)}</p>` : ""}` : ""}
40877
41368
  ${o ? `<div class="head">Your offer, graded</div>
40878
41369
  <div class="scorebar"><span class="scorenum">${o.score}/10</span><span class="track"><span class="fill" style="width:${Math.max(0, Math.min(100, o.score * 10))}%"></span></span></div>
40879
- ${o.dreamOutcome ? `<p class="kv"><b>Worth wanting:</b> ${esc10(o.dreamOutcome)}</p>` : ""}
40880
- ${o.believability ? `<p class="kv"><b>Believable:</b> ${esc10(o.believability)}</p>` : ""}
40881
- ${o.speed ? `<p class="kv"><b>Speed to result:</b> ${esc10(o.speed)}</p>` : ""}
40882
- ${o.effort ? `<p class="kv"><b>Effort asked of them:</b> ${esc10(o.effort)}</p>` : ""}
40883
- ${o.biggestLever ? `<p class="kv"><b>Biggest lever:</b> ${esc10(o.biggestLever)}</p>` : ""}
40884
- ${o.recommendedOffer ? `<div class="lead"><b>Run this instead:</b> ${esc10(o.recommendedOffer)}</div>` : ""}` : ""}
41370
+ ${o.dreamOutcome ? `<p class="kv"><b>Worth wanting:</b> ${esc11(o.dreamOutcome)}</p>` : ""}
41371
+ ${o.believability ? `<p class="kv"><b>Believable:</b> ${esc11(o.believability)}</p>` : ""}
41372
+ ${o.speed ? `<p class="kv"><b>Speed to result:</b> ${esc11(o.speed)}</p>` : ""}
41373
+ ${o.effort ? `<p class="kv"><b>Effort asked of them:</b> ${esc11(o.effort)}</p>` : ""}
41374
+ ${o.biggestLever ? `<p class="kv"><b>Biggest lever:</b> ${esc11(o.biggestLever)}</p>` : ""}
41375
+ ${o.recommendedOffer ? `<div class="lead"><b>Run this instead:</b> ${esc11(o.recommendedOffer)}</div>` : ""}` : ""}
40885
41376
  ${review.gaps?.length ? `<div class="head">What's leaking today (${review.gaps.length})</div>${review.gaps.map(gap).join("")}` : ""}
40886
- ${review.build?.length ? `<div class="head">What we're building</div>${review.build.map((b) => `<div class="build"><b>${esc10(b.item)}</b><span>${esc10(b.because)}</span></div>`).join("")}` : ""}
41377
+ ${review.build?.length ? `<div class="head">What we're building</div>${review.build.map((b) => `<div class="build"><b>${esc11(b.item)}</b><span>${esc11(b.because)}</span></div>`).join("")}` : ""}
40887
41378
  ${opts.scope?.awaitingConfirmation ? `<div class="head">What this build includes</div>
40888
41379
  <p>Not confirmed yet. What you bought is recorded, and the build waits until the features to build are confirmed.</p>
40889
41380
  ` : opts.scope ? `<div class="head">What this build includes</div>
40890
- <p>${opts.scope.package ? `Built to your <b>${esc10(opts.scope.package)}</b> package: ` : ""}${opts.scope.built.length ? esc10(opts.scope.built.map((b) => b.name).join(", ")) : "the foundation only"}. Always included: the pipeline and its stages, the fields, tags and custom values, the intake form, the calendar, and the reply routing.</p>
41381
+ <p>${opts.scope.package ? `Built to your <b>${esc11(opts.scope.package)}</b> package: ` : ""}${opts.scope.built.length ? esc11(opts.scope.built.map((b) => b.name).join(", ")) : "the foundation only"}. Always included: the pipeline and its stages, the fields, tags and custom values, the intake form, the calendar, and the reply routing.</p>
40891
41382
  ` : ""}
40892
- ${review.unknowns?.length ? `<div class="head">What we still need from you</div><div class="ask"><ul>${review.unknowns.map((u) => `<li>${esc10(u)}</li>`).join("")}</ul></div>` : ""}
40893
- ${opts.assessment ? `<div class="head">Assessment</div><p>Snapshot attached on ${esc10(opts.assessment.attachedAt.slice(0, 10))}.</p>${assessmentFeatureLines(opts.assessment, [], false).filter((line3) => line3.includes("by your own numbers")).map((line3) => `<p>${esc10(line3)}</p>`).join("")}` : ""}
41383
+ ${review.unknowns?.length ? `<div class="head">What we still need from you</div><div class="ask"><ul>${review.unknowns.map((u) => `<li>${esc11(u)}</li>`).join("")}</ul></div>` : ""}
41384
+ ${opts.assessment ? `<div class="head">Assessment</div><p>Snapshot attached on ${esc11(opts.assessment.attachedAt.slice(0, 10))}.</p>${assessmentFeatureLines(opts.assessment, [], false).filter((line3) => line3.includes("by your own numbers")).map((line3) => `<p>${esc11(line3)}</p>`).join("")}` : ""}
40894
41385
  ${available ? `<div class="head">Available next</div>${available}` : opts.assessment ? `<div class="head">Available next</div><p>${purchased ? "Every listed feature is included in what you bought." : "What you bought has not been recorded yet."}</p>` : ""}
40895
- <footer><span>${agency ? esc10(agency) : "Growth Blueprint"}</span><span>${esc10(opts.clientName)} \xB7 ${date}</span></footer>
41386
+ <footer><span>${agency ? esc11(agency) : "Growth Blueprint"}</span><span>${esc11(opts.clientName)} \xB7 ${date}</span></footer>
40896
41387
  </div></body></html>`;
40897
41388
  }
40898
41389
  var GAP_CHECKLIST;
@@ -41043,7 +41534,7 @@ var init_client_fields = __esm({
41043
41534
  });
41044
41535
 
41045
41536
  // src/command-os/assessment/report.js
41046
- function esc8(s) {
41537
+ function esc9(s) {
41047
41538
  return String(s === void 0 || s === null ? "" : s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
41048
41539
  }
41049
41540
  function isFigure(v) {
@@ -41063,7 +41554,7 @@ function renderReport2(opts) {
41063
41554
  var assumptionLabels = opts.assumptionLabels || {};
41064
41555
  var date = opts.date || "";
41065
41556
  var accent = safeColour(agency.accent);
41066
- var biz = esc8(state.biz || "your business");
41557
+ var biz = esc9(state.biz || "your business");
41067
41558
  var F = Array.isArray(r.F) ? r.F : [];
41068
41559
  var O = Array.isArray(r.O) ? r.O : [];
41069
41560
  var hrs = Array.isArray(r.hrs) ? r.hrs : [];
@@ -41074,23 +41565,23 @@ function renderReport2(opts) {
41074
41565
  out.push("<title>Where " + biz + " is leaking</title>");
41075
41566
  out.push("<style>" + css(accent) + "</style></head><body><div class=wrap>");
41076
41567
  out.push("<header>");
41077
- if (agency.name) out.push('<p class="by">Prepared by ' + esc8(agency.name) + "</p>");
41568
+ if (agency.name) out.push('<p class="by">Prepared by ' + esc9(agency.name) + "</p>");
41078
41569
  out.push("<h1>Where " + biz + " is leaking</h1>");
41079
- var who = state.client && state.client.name ? esc8(state.client.name) + (state.client.role ? ", " + esc8(state.client.role) : "") : "";
41570
+ var who = state.client && state.client.name ? esc9(state.client.name) + (state.client.role ? ", " + esc9(state.client.role) : "") : "";
41080
41571
  if (who) out.push('<p class="for">Prepared for ' + who + "</p>");
41081
- if (date) out.push('<p class="when">' + esc8(date) + "</p>");
41572
+ if (date) out.push('<p class="when">' + esc9(date) + "</p>");
41082
41573
  if (agency.method || agency.promise) {
41083
- out.push('<p class="about">' + [agency.method, agency.promise].filter(Boolean).map(esc8).join(" ") + "</p>");
41574
+ out.push('<p class="about">' + [agency.method, agency.promise].filter(Boolean).map(esc9).join(" ") + "</p>");
41084
41575
  }
41085
41576
  out.push("</header>");
41086
41577
  if (r.verdict === "insufficient") {
41087
- out.push('<div class="verdict stop"><b>We could not put a number on this yet</b>' + esc8(r.line || "") + "</div>");
41578
+ out.push('<div class="verdict stop"><b>We could not put a number on this yet</b>' + esc9(r.line || "") + "</div>");
41088
41579
  out.push(measureFirst(O));
41089
41580
  out.push(footer(agency));
41090
41581
  return out.join("\n") + "</div></body></html>";
41091
41582
  }
41092
41583
  if (r.verdict === "thin") {
41093
- out.push('<div class="verdict warn"><b>Honestly, there is not much here</b>' + esc8(r.line || "") + "</div>");
41584
+ out.push('<div class="verdict warn"><b>Honestly, there is not much here</b>' + esc9(r.line || "") + "</div>");
41094
41585
  }
41095
41586
  out.push('<div class="hgrid">');
41096
41587
  if (r.hrsWk > 0) out.push(head2(r.hrsWk.toFixed(1), "hours a week going into work a system should do"));
@@ -41102,7 +41593,7 @@ function renderReport2(opts) {
41102
41593
  for (var a = 0; a < opts.answered.length; a++) {
41103
41594
  var q3 = opts.answered[a];
41104
41595
  var numeric = q3.unit !== void 0 ? q3.unit !== "text" : isFigure(q3.value);
41105
- out.push("<tr><td>" + esc8(q3.ask) + '</td><td class="' + (numeric ? "num" : "said") + '">' + (q3.unknown ? '<span class="unk">you did not know</span>' : esc8(q3.value)) + "</td></tr>");
41596
+ out.push("<tr><td>" + esc9(q3.ask) + '</td><td class="' + (numeric ? "num" : "said") + '">' + (q3.unknown ? '<span class="unk">you did not know</span>' : esc9(q3.value)) + "</td></tr>");
41106
41597
  }
41107
41598
  out.push("</table>");
41108
41599
  }
@@ -41111,15 +41602,15 @@ function renderReport2(opts) {
41111
41602
  out.push('<tr><th>What is happening</th><th class="num">A month</th></tr>');
41112
41603
  for (var i = 0; i < F.length; i++) {
41113
41604
  var f = F[i];
41114
- var note = f.assumption ? '<span class="assume">' + esc8(assumptionLabels[f.assumption] || "This is your assumption, not a measurement.") + "</span>" : "";
41115
- out.push("<tr><td><strong>" + esc8(f.title) + "</strong>" + note + '</td><td class="num">' + money4(f.monthly) + "</td></tr>");
41605
+ var note = f.assumption ? '<span class="assume">' + esc9(assumptionLabels[f.assumption] || "This is your assumption, not a measurement.") + "</span>" : "";
41606
+ out.push("<tr><td><strong>" + esc9(f.title) + "</strong>" + note + '</td><td class="num">' + money4(f.monthly) + "</td></tr>");
41116
41607
  }
41117
41608
  out.push('<tr class="tot"><td><strong>Leaking, every month</strong></td><td class="num"><strong>' + money4(r.monthly) + "</strong></td></tr>");
41118
41609
  if (r.stop > 0) {
41119
- out.push('<tr><td><strong>What we expect to actually stop</strong><span class="assume">' + esc8(assumptionLabels.recovery || "No system catches everything. This is your figure.") + '</span></td><td class="num"><strong>' + money4(r.stop) + "</strong></td></tr>");
41610
+ out.push('<tr><td><strong>What we expect to actually stop</strong><span class="assume">' + esc9(assumptionLabels.recovery || "No system catches everything. This is your figure.") + '</span></td><td class="num"><strong>' + money4(r.stop) + "</strong></td></tr>");
41120
41611
  }
41121
41612
  if (r.oneoff !== null && r.oneoff !== void 0 && r.oneoff > 0) {
41122
- out.push('<tr><td>Your dormant list, worked once<span class="assume">' + esc8(assumptionLabels.reactivation || "Your assumption, not a measurement.") + ' This is a one-off and is left out of every annual figure above.</span></td><td class="num">' + money4(r.oneoff) + "</td></tr>");
41613
+ out.push('<tr><td>Your dormant list, worked once<span class="assume">' + esc9(assumptionLabels.reactivation || "Your assumption, not a measurement.") + ' This is a one-off and is left out of every annual figure above.</span></td><td class="num">' + money4(r.oneoff) + "</td></tr>");
41123
41614
  }
41124
41615
  out.push("</table>");
41125
41616
  }
@@ -41130,14 +41621,14 @@ function renderReport2(opts) {
41130
41621
  out.push("<h2>Named, and deliberately not priced</h2><ul>");
41131
41622
  for (var j = 0; j < named.length; j++) {
41132
41623
  var o = named[j];
41133
- out.push("<li><strong>" + esc8(o.title) + "</strong> &mdash; " + esc8(o.declined ? "priced only on an assumption you chose not to make." : o.reason) + "</li>");
41624
+ out.push("<li><strong>" + esc9(o.title) + "</strong> &mdash; " + esc9(o.declined ? "priced only on an assumption you chose not to make." : o.reason) + "</li>");
41134
41625
  }
41135
41626
  out.push("</ul><p class=small>We can see these matter and we are not going to invent a number for them.</p>");
41136
41627
  }
41137
41628
  if (hrs.length) {
41138
41629
  out.push("<h2>Where the time goes</h2><table>");
41139
41630
  for (var h = 0; h < hrs.length; h++) {
41140
- out.push("<tr><td>" + esc8(hrs[h].t) + '</td><td class="num">' + Number(hrs[h].h).toFixed(1) + " hrs</td></tr>");
41631
+ out.push("<tr><td>" + esc9(hrs[h].t) + '</td><td class="num">' + Number(hrs[h].h).toFixed(1) + " hrs</td></tr>");
41141
41632
  }
41142
41633
  out.push('<tr class="tot"><td><strong>A week</strong></td><td class="num"><strong>' + Number(r.hrsWk).toFixed(1) + "</strong></td></tr></table>");
41143
41634
  }
@@ -41149,7 +41640,7 @@ function renderReport2(opts) {
41149
41640
  var svc = services.find(function(s) {
41150
41641
  return Array.isArray(s.covers) && s.covers.indexOf(fx.id) !== -1;
41151
41642
  });
41152
- out.push("<tr><td>" + esc8(fx.title) + "</td><td>" + esc8(svc ? svc.name : fx.fix || "") + "</td></tr>");
41643
+ out.push("<tr><td>" + esc9(fx.title) + "</td><td>" + esc9(svc ? svc.name : fx.fix || "") + "</td></tr>");
41153
41644
  }
41154
41645
  out.push("</table>");
41155
41646
  if (!services.length) {
@@ -41165,7 +41656,7 @@ function renderReport2(opts) {
41165
41656
  }, 0);
41166
41657
  out.push("<h2>The investment</h2><table>");
41167
41658
  for (var p = 0; p < priced.length; p++) {
41168
- out.push("<tr><td>" + esc8(priced[p].name) + '</td><td class="num">' + money4(priced[p].price) + "</td></tr>");
41659
+ out.push("<tr><td>" + esc9(priced[p].name) + '</td><td class="num">' + money4(priced[p].price) + "</td></tr>");
41169
41660
  }
41170
41661
  out.push('<tr class="tot"><td><strong>Total</strong></td><td class="num"><strong>' + money4(total) + "</strong></td></tr>");
41171
41662
  out.push('<tr><td>Against what we expect to stop, first year</td><td class="num">' + money4(r.annualStop) + "</td></tr>");
@@ -41180,7 +41671,7 @@ function renderReport2(opts) {
41180
41671
  return out.join("\n") + "</div></body></html>";
41181
41672
  }
41182
41673
  function head2(n, label) {
41183
- return '<div><span class="hn">' + esc8(n) + '</span><span class="hl">' + esc8(label) + "</span></div>";
41674
+ return '<div><span class="hn">' + esc9(n) + '</span><span class="hl">' + esc9(label) + "</span></div>";
41184
41675
  }
41185
41676
  function measureFirst(O) {
41186
41677
  var seen = [];
@@ -41189,18 +41680,18 @@ function measureFirst(O) {
41189
41680
  for (var j = 0; j < m.length; j++) if (seen.indexOf(m[j]) === -1) seen.push(m[j]);
41190
41681
  }
41191
41682
  if (!seen.length) return "";
41192
- return "<h2>What to measure first</h2><p>These are the numbers that would let us price the rest. None of them need a system &mdash; a week of writing them down is enough.</p><ul><li>" + seen.map(esc8).join("</li><li>") + "</li></ul>";
41683
+ return "<h2>What to measure first</h2><p>These are the numbers that would let us price the rest. None of them need a system &mdash; a week of writing them down is enough.</p><ul><li>" + seen.map(esc9).join("</li><li>") + "</li></ul>";
41193
41684
  }
41194
41685
  function sourceLine(state, r, labels) {
41195
41686
  var used = [];
41196
41687
  var F = r.F || [];
41197
41688
  for (var i = 0; i < F.length; i++) if (F[i].assumption && used.indexOf(F[i].assumption) === -1) used.push(F[i].assumption);
41198
41689
  var n = used.length + (r.stop > 0 ? 1 : 0);
41199
- return '<p class="src">Every figure here came from what you told us' + (state.trade ? ", or from your own " + esc8(state.trade) + " business" : "") + ". " + (n > 0 ? n + " of them " + (n === 1 ? "is an estimate" : "are estimates") + " we chose together, each marked where it is used. " : "") + "Nothing here is an industry average applied to your business.</p>";
41690
+ return '<p class="src">Every figure here came from what you told us' + (state.trade ? ", or from your own " + esc9(state.trade) + " business" : "") + ". " + (n > 0 ? n + " of them " + (n === 1 ? "is an estimate" : "are estimates") + " we chose together, each marked where it is used. " : "") + "Nothing here is an industry average applied to your business.</p>";
41200
41691
  }
41201
41692
  function footer(agency) {
41202
41693
  if (!agency.name) return "";
41203
- return "<footer>" + esc8(agency.name) + (agency.contact ? " &middot; " + esc8(agency.contact) : "") + (agency.guarantee ? '<br><span class="guarantee">' + esc8(agency.guarantee) + "</span>" : "") + "</footer>";
41694
+ return "<footer>" + esc9(agency.name) + (agency.contact ? " &middot; " + esc9(agency.contact) : "") + (agency.guarantee ? '<br><span class="guarantee">' + esc9(agency.guarantee) + "</span>" : "") + "</footer>";
41204
41695
  }
41205
41696
  function css(accent) {
41206
41697
  return [
@@ -44636,6 +45127,21 @@ async function runDashboard(argv) {
44636
45127
  };
44637
45128
  const shopperRoutes = (req, res, url) => {
44638
45129
  const u = new URL(req.url ?? "/", "http://127.0.0.1");
45130
+ if (req.method === "GET" && url === "/shopper/report") {
45131
+ const locationId2 = u.searchParams.get("client") ?? "";
45132
+ const client = currentRoster().find((r) => r.locationId === locationId2 && readableAccount(r.locationId));
45133
+ const runId = u.searchParams.get("run") ?? "";
45134
+ const run = client ? listRuns(locationId2).find((r) => r.runId === runId) : void 0;
45135
+ const card = run ? scoreRun(run) : void 0;
45136
+ if (!client || !run || !card?.graded || !run.findings) {
45137
+ res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
45138
+ res.end("No finished, graded check with that id for this client.");
45139
+ return;
45140
+ }
45141
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
45142
+ res.end(reportDocumentHtml(run, card, { clientName: client.name, backHref: `/shopper?client=${encodeURIComponent(locationId2)}` }));
45143
+ return;
45144
+ }
44639
45145
  if (req.method === "GET" && url === "/shopper") {
44640
45146
  const accounts = currentRoster().filter((r) => readableAccount(r.locationId)).map((r) => ({ locationId: r.locationId, name: r.name })).sort((a, b) => a.name.localeCompare(b.name));
44641
45147
  const client = accounts.find((a) => a.locationId === (u.searchParams.get("client") ?? ""));
@@ -44770,12 +45276,14 @@ async function runDashboard(argv) {
44770
45276
  let planNote;
44771
45277
  let workflowNames;
44772
45278
  let draftWorkflows;
45279
+ let draftWorkflowIds;
44773
45280
  try {
44774
45281
  const plan = await planFromWorkflowIds(workflowReadConnection(registry2, c), workflowIds);
44775
45282
  expect = plan.expect;
44776
45283
  planNote = { source: "workflows", conditional: plan.leftOut.conditional, noWording: plan.leftOut.noWording, unread: plan.unread };
44777
45284
  workflowNames = plan.names;
44778
45285
  draftWorkflows = plan.drafts;
45286
+ draftWorkflowIds = plan.draftIds;
44779
45287
  } catch (e) {
44780
45288
  logQuietly(`secret shopper plan: ${(0, import_node_util4.inspect)(e)}`);
44781
45289
  }
@@ -44791,7 +45299,8 @@ async function runDashboard(argv) {
44791
45299
  expect,
44792
45300
  planNote,
44793
45301
  workflowNames,
44794
- draftWorkflows
45302
+ draftWorkflows,
45303
+ draftWorkflowIds
44795
45304
  }, { client: c, otherBusinessNames: others });
44796
45305
  scheduleShopperFinish(run);
44797
45306
  shopperJson(res, 200, run.status === "failed" ? { ok: false, error: run.error } : { ok: true, runId: run.runId });
@@ -44863,7 +45372,7 @@ async function runDashboard(argv) {
44863
45372
  const url = cockpitPath(req);
44864
45373
  if (connectionsRoutes(req, res)) return;
44865
45374
  if (briefRoutes(req, res)) return;
44866
- if (url === "/shopper" || url.startsWith("/api/shopper/")) {
45375
+ if (url === "/shopper" || url === "/shopper/report" || url.startsWith("/api/shopper/")) {
44867
45376
  shopperRoutes(req, res, url);
44868
45377
  return;
44869
45378
  }
@@ -46246,8 +46755,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
46246
46755
  );
46247
46756
  if (gate3) throw new Error(gate3);
46248
46757
  }
46249
- const marked2 = recordHandMark(setStage(readCockpitState(), locationId2, stage, status), locationId2, stage, by, status);
46250
- const withSig = stage === 6 && status === "done" ? { ...marked2, goLive: { ...marked2.goLive ?? {}, [locationId2]: signOff({ verify: marked2.outcomes?.[`${locationId2}:4`], manualTasks: marked2.manualTasks?.[locationId2] ?? [], by }) } } : stage === 6 ? { ...marked2, goLive: Object.fromEntries(Object.entries(marked2.goLive ?? {}).filter(([k]) => k !== locationId2)) } : marked2;
46758
+ const marked3 = recordHandMark(setStage(readCockpitState(), locationId2, stage, status), locationId2, stage, by, status);
46759
+ const withSig = stage === 6 && status === "done" ? { ...marked3, goLive: { ...marked3.goLive ?? {}, [locationId2]: signOff({ verify: marked3.outcomes?.[`${locationId2}:4`], manualTasks: marked3.manualTasks?.[locationId2] ?? [], by }) } } : stage === 6 ? { ...marked3, goLive: Object.fromEntries(Object.entries(marked3.goLive ?? {}).filter(([k]) => k !== locationId2)) } : marked3;
46251
46760
  writeCockpitState(withSig);
46252
46761
  pushShared(locationId2);
46253
46762
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -46304,6 +46813,8 @@ var init_dashboard = __esm({
46304
46813
  init_brief_panels();
46305
46814
  init_page();
46306
46815
  init_run();
46816
+ init_score();
46817
+ init_report_page();
46307
46818
  init_plan2();
46308
46819
  init_steps();
46309
46820
  init_fastforward();
@@ -52093,10 +52604,10 @@ function renderVerificationHtml(v, branding = {}, base) {
52093
52604
  ];
52094
52605
  const s = v.summary;
52095
52606
  const verdictClass = v.verdict === "PASS" ? "ok" : v.verdict === "FAIL" ? "fail" : "warn";
52096
- const row = (r) => `<tr class="${STATE_CLASS[r.state]}"><td class="id">${esc9(r.ruleId)}</td><td><b>${esc9(r.name)}</b><div class="thr">${esc9(r.threshold)}</div></td><td><span class="pill ${STATE_CLASS[r.state]}">${STATE_LABEL[r.state]}</span></td><td>${esc9(r.measured ?? "")}${r.detail ? `<div class="det">${esc9(r.detail)}</div>` : ""}</td><td class="ev">${esc9(r.evidence ?? "")}</td></tr>`;
52607
+ const row = (r) => `<tr class="${STATE_CLASS[r.state]}"><td class="id">${esc10(r.ruleId)}</td><td><b>${esc10(r.name)}</b><div class="thr">${esc10(r.threshold)}</div></td><td><span class="pill ${STATE_CLASS[r.state]}">${STATE_LABEL[r.state]}</span></td><td>${esc10(r.measured ?? "")}${r.detail ? `<div class="det">${esc10(r.detail)}</div>` : ""}</td><td class="ev">${esc10(r.evidence ?? "")}</td></tr>`;
52097
52608
  return `<!doctype html>
52098
52609
  <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
52099
- <title>Design check \u2014 ${esc9(v.target.split("/").pop() ?? v.target)}</title>
52610
+ <title>Design check \u2014 ${esc10(v.target.split("/").pop() ?? v.target)}</title>
52100
52611
  <style>
52101
52612
  :root{--accent:${b.accent};--ink:#1F2428;--muted:#5C6570;--line:#E3E6E9;--bg:#FFFFFF;--ok:#2F7A5A;--fail:#A63D3D;--warn:#B9621C;--na:#6E7681}
52102
52613
  body{margin:0;background:var(--bg);color:var(--ink);font:18px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}
@@ -52127,23 +52638,23 @@ td.ev{font-family:ui-monospace,Menlo,monospace;font-size:12.5px;color:var(--mute
52127
52638
  .shots a{font-size:13px;color:var(--accent)}
52128
52639
  footer{margin-top:40px;color:var(--muted);font-size:13px;border-top:1px solid var(--line);padding-top:12px}
52129
52640
  </style></head><body><div class="wrap">
52130
- <header><div>${isRasterDataUri(b.logoDataUri) ? `<img src="${esc9(b.logoDataUri)}" alt="" style="height:28px;margin-bottom:8px">` : ""}<h1>Design check</h1><div class="agency">${b.agencyName ? esc9(b.agencyName) + " \xB7 " : ""}${esc9(v.medium)} \xB7 ${esc9(v.target)} \xB7 ${esc9(v.at.slice(0, 16).replace("T", " "))} \xB7 ${v.renderedWith === "browser" ? "rendered in a browser" : "static checks only"}</div></div><div class="verdict ${verdictClass}">${v.verdict === "PASS" ? "Passes every measured rule" : v.verdict === "FAIL" ? "Fails a measured rule" : "Not graded"}</div></header>
52641
+ <header><div>${isRasterDataUri(b.logoDataUri) ? `<img src="${esc10(b.logoDataUri)}" alt="" style="height:28px;margin-bottom:8px">` : ""}<h1>Design check</h1><div class="agency">${b.agencyName ? esc10(b.agencyName) + " \xB7 " : ""}${esc10(v.medium)} \xB7 ${esc10(v.target)} \xB7 ${esc10(v.at.slice(0, 16).replace("T", " "))} \xB7 ${v.renderedWith === "browser" ? "rendered in a browser" : "static checks only"}</div></div><div class="verdict ${verdictClass}">${v.verdict === "PASS" ? "Passes every measured rule" : v.verdict === "FAIL" ? "Fails a measured rule" : "Not graded"}</div></header>
52131
52642
  <p class="scope"><strong>What this report is not.</strong> Every rule here measures craft \u2014 type, spacing, colour, contrast, structure, performance, and the tells that make a page look machine-made. <strong>None of them measures whether the page persuades anyone.</strong> A page can pass every rule and still be boring, unconvincing and unsellable: no hook, no proof, no reason to care. Judge the writing separately, and never read a pass here as "this is a good page".</p>
52132
- <div class="conf ${v.conformance.ok ? "ok" : ""}"><b>${v.conformance.ok ? "The file honours the output contract." : "The file breaks the output contract, so the design rules were not graded."}</b>${v.conformance.problems.length ? `<ul>${v.conformance.problems.map((p) => `<li>${esc9(p)}</li>`).join("")}</ul>` : ""}${v.conformance.warnings.length ? `<div class="det">Warnings: ${v.conformance.warnings.map(esc9).join(" \xB7 ")}</div>` : ""}</div>
52643
+ <div class="conf ${v.conformance.ok ? "ok" : ""}"><b>${v.conformance.ok ? "The file honours the output contract." : "The file breaks the output contract, so the design rules were not graded."}</b>${v.conformance.problems.length ? `<ul>${v.conformance.problems.map((p) => `<li>${esc10(p)}</li>`).join("")}</ul>` : ""}${v.conformance.warnings.length ? `<div class="det">Warnings: ${v.conformance.warnings.map(esc10).join(" \xB7 ")}</div>` : ""}</div>
52133
52644
  <div class="sum"><div><b>${s.measuredPass}</b><span>measured, pass</span></div><div><b>${s.measuredFail}</b><span>measured, fail</span></div><div><b>${s.proxyPass}</b><span>proxy, pass</span></div><div><b>${s.proxyFail}</b><span>proxy, fail</span></div><div><b>${s.asserted}</b><span>judgment calls</span></div><div><b>${s.na + s.unchecked}</b><span>not applicable / not checked</span></div></div>
52134
- ${v.brandMatch ? `<p class="note"><b>Brand:</b> ${esc9(v.brandMatch.detail)}</p>` : ""}
52645
+ ${v.brandMatch ? `<p class="note"><b>Brand:</b> ${esc10(v.brandMatch.detail)}</p>` : ""}
52135
52646
  ${groups.filter((g) => g.rows.length).map((g) => `<h2>${g.title}</h2><p class="note">${g.note}</p><table><thead><tr><th>Rule</th><th>What it asks</th><th>State</th><th>Measured</th><th>Evidence</th></tr></thead><tbody>${g.rows.map(row).join("")}</tbody></table>`).join("")}
52136
- ${v.screenshots.length ? `<h2>Screenshots</h2><div class="shots">${v.screenshots.map((f) => `<a href="${esc9(f)}">${esc9(f)}</a>`).join("")}</div>` : ""}
52137
- <footer>Ruleset ${esc9(v.ruleset)} \xB7 viewports ${esc9(v.viewports.join(", ") || "\u2014")} \xB7 verification@${v.schemaVersion}. Measured lines are computed by software; proxy lines approximate a judgment; judgment calls are listed so they are never forgotten. Nothing here is hidden.</footer>
52647
+ ${v.screenshots.length ? `<h2>Screenshots</h2><div class="shots">${v.screenshots.map((f) => `<a href="${esc10(f)}">${esc10(f)}</a>`).join("")}</div>` : ""}
52648
+ <footer>Ruleset ${esc10(v.ruleset)} \xB7 viewports ${esc10(v.viewports.join(", ") || "\u2014")} \xB7 verification@${v.schemaVersion}. Measured lines are computed by software; proxy lines approximate a judgment; judgment calls are listed so they are never forgotten. Nothing here is hidden.</footer>
52138
52649
  </div></body></html>`;
52139
52650
  }
52140
- var esc9, STATE_LABEL, STATE_CLASS;
52651
+ var esc10, STATE_LABEL, STATE_CLASS;
52141
52652
  var init_report2 = __esm({
52142
52653
  "src/command-os/design/report.ts"() {
52143
52654
  "use strict";
52144
52655
  init_branding();
52145
52656
  init_svg_sanitize();
52146
- esc9 = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
52657
+ esc10 = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
52147
52658
  STATE_LABEL = {
52148
52659
  "MEASURED-PASS": "Pass",
52149
52660
  "MEASURED-FAIL": "Fail",
@@ -55948,6 +56459,7 @@ function registerSecretShopperTools(server2, client, registry2) {
55948
56459
  let planNote = { source: expect?.length ? "given" : "none" };
55949
56460
  let workflowNames;
55950
56461
  let draftWorkflows;
56462
+ let draftWorkflowIds;
55951
56463
  if (!expect?.length && entry.kind === "workflows" && registry2) {
55952
56464
  try {
55953
56465
  const plan = await planFromWorkflowIds(workflowReadConnection(registry2, client), entry.workflowIds);
@@ -55955,12 +56467,13 @@ function registerSecretShopperTools(server2, client, registry2) {
55955
56467
  planNote = { source: "workflows", conditional: plan.leftOut.conditional, noWording: plan.leftOut.noWording, unread: plan.unread };
55956
56468
  workflowNames = plan.names;
55957
56469
  draftWorkflows = plan.drafts;
56470
+ draftWorkflowIds = plan.draftIds;
55958
56471
  } catch {
55959
56472
  }
55960
56473
  }
55961
56474
  const others = (registry2?.listLocations() ?? []).filter((l) => l.locationId !== locationId2).map((l) => l.name);
55962
56475
  const run = await startShopper(
55963
- { locationId: locationId2, ownerEmail, testPhone: args.testPhone, firstName: args.firstName, entry, watchMinutes: args.watchMinutes, expect, planNote, workflowNames, draftWorkflows, keepLead: args.keepLead },
56476
+ { locationId: locationId2, ownerEmail, testPhone: args.testPhone, firstName: args.firstName, entry, watchMinutes: args.watchMinutes, expect, planNote, workflowNames, draftWorkflows, draftWorkflowIds, keepLead: args.keepLead },
55964
56477
  { client, otherBusinessNames: others }
55965
56478
  );
55966
56479
  return { ok: run.status !== "failed", ...report2(run), next: run.status === "watching" ? `Say "finish the secret shopper" after ${new Date(run.closesAt).toLocaleTimeString()} to read what was sent. Finishing earlier gives a partial look.` : void 0 };