@elitedcs/ghl-mcp 3.89.0-beta.5 → 3.89.0-beta.6

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
@@ -27616,6 +27616,23 @@ function calendarSlotMismatch(planCal, existing) {
27616
27616
  if (want === void 0 || have === void 0 || want === have) return void 0;
27617
27617
  return `Calendar "${planCal.name}" is ${have} min in the account, the plan says ${want} \u2014 change it in Calendars \u2192 Settings (slot duration) or tell Blueprint to update it (update_calendar ${existing.id}). Blueprint never modifies an existing calendar on its own.`;
27618
27618
  }
27619
+ function openHoursKey(blocks) {
27620
+ if (!Array.isArray(blocks) || blocks.length === 0) return void 0;
27621
+ const byDay = /* @__PURE__ */ new Map();
27622
+ const t = (h, m) => `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
27623
+ for (const b of blocks) for (const d of b.daysOfTheWeek ?? []) {
27624
+ const list4 = byDay.get(d) ?? [];
27625
+ for (const w of b.hours ?? []) list4.push(`${t(w.openHour, w.openMinute)}-${t(w.closeHour, w.closeMinute)}`);
27626
+ byDay.set(d, list4);
27627
+ }
27628
+ return [...byDay.entries()].sort((a, b) => a[0] - b[0]).map(([d, ws]) => `${d}:${ws.sort().join(",")}`).join(" ");
27629
+ }
27630
+ function calendarHoursMismatch(planCal, existing) {
27631
+ const want = openHoursKey(planCal.openHours);
27632
+ const have = openHoursKey(existing.openHours);
27633
+ if (want === void 0 || have === void 0 || want === have) return void 0;
27634
+ return `Calendar "${planCal.name}" was already in the account, so its open hours were left as they are, and they differ from the hours in the plan (the intake's business hours). Set its hours by hand if the client's hours are the right ones. Build never changes a calendar that is already there.`;
27635
+ }
27619
27636
  function calendarNeedsStaff(cal) {
27620
27637
  if (!calendarHoldsTeam(cal)) return false;
27621
27638
  return cal.requiresStaff === true || STAFF_CALENDAR_TYPES.has(cal.calendarType);
@@ -28611,7 +28628,7 @@ function resolvePlan(plan, existing, opts = {}) {
28611
28628
  for (const cal of plan.calendars ?? []) {
28612
28629
  const existingCal = (existing.calendars ?? []).find((c) => norm2(c.name) === norm2(cal.name));
28613
28630
  if (existingCal) {
28614
- const reason3 = calendarSlotMismatch(cal, existingCal);
28631
+ const reason3 = [calendarSlotMismatch(cal, existingCal), calendarHoursMismatch(cal, existingCal)].filter(Boolean).join(" ") || void 0;
28615
28632
  if (reason3) calendarMismatch.push({ ref: cal.ref, name: cal.name, existingId: existingCal.id, reason: reason3 });
28616
28633
  const named = calendarTeamMemberRefs(cal).map((r) => ({ id: idMap.get(r) ?? "", name: idx.userName.get(r) ?? r }));
28617
28634
  const claimable = named.filter((m) => m.id && m.id !== PENDING_USER_REF);
@@ -29839,6 +29856,26 @@ var init_claude_cli = __esm({
29839
29856
  }
29840
29857
  });
29841
29858
 
29859
+ // src/brief-issues.ts
29860
+ function isCleanCheckReport(text2) {
29861
+ const t = text2.toLowerCase().replace(/[`*_]/g, " ").replace(/\s+/g, " ").trim().replace(/^(?:warning|warn|note|info)\s*:\s*/, "");
29862
+ if (!t || /^(?:none|no issues|no gaps|no warnings|nothing to report|n\/a)\.?$/.test(t)) return true;
29863
+ if (/^(?:the )?(?:brief|validate brief|validation|check)(?: check)? (?:is valid|was valid|passed|passes|found (?:no|nothing)(?: gaps| issues| problems)?)(?: validation)?\.?$/.test(t)) return true;
29864
+ if (/\b(?:but|however|except|although|missing|lacks?|needs?|should|invalid|failed|empty|blank|unknown|not (?:set|given|provided))\b|valid\s*[:=]?\s*false/.test(t)) return false;
29865
+ const counts = [...t.matchAll(/\b(\d+|no|zero)\s+(?:errors?|warnings?|gaps?|issues?|problems?)\b/g)];
29866
+ const saysClean = /\bvalid\s*[:=]?\s*true\b|\bis valid\b|\bpassed\b|\bclean\b/.test(t) || counts.length > 0;
29867
+ return saysClean && counts.length > 0 && counts.every((c) => c[1] === "0" || c[1] === "no" || c[1] === "zero");
29868
+ }
29869
+ function briefIssuesShown(issues) {
29870
+ if (!issues) return issues;
29871
+ return issues.map((x) => String(x).replace(/^\s*(?:warning|warn)\s*:\s*/i, "").trim()).filter((x) => x && !isCleanCheckReport(x)).map((x) => x.charAt(0).toUpperCase() + x.slice(1));
29872
+ }
29873
+ var init_brief_issues = __esm({
29874
+ "src/brief-issues.ts"() {
29875
+ "use strict";
29876
+ }
29877
+ });
29878
+
29842
29879
  // src/client-record.ts
29843
29880
  function clientRecordKey(locationId2, clientKey) {
29844
29881
  return clientKey ? `${locationId2}${CLIENT_RECORD_SEPARATOR}${clientKey}` : locationId2;
@@ -29900,7 +29937,11 @@ function buildPermission(locationId2, opts = {}) {
29900
29937
  reason: `${opts.name || locationId2} is not approved for building yet. Approve it on the card first \u2014 that is the deliberate step that lets automation write into a real client account.`
29901
29938
  };
29902
29939
  }
29903
- function buildStageReusePrompt(locationId2, locationName, saved) {
29940
+ function buildFactsRule(ctx) {
29941
+ const staff = (ctx?.verifiedStaff ?? []).filter((n) => n.trim());
29942
+ return `Say who a person is only from get_users or the build reply's staff list, and never judge anyone by their name: a name such as "Test Provider" or "Front Desk" is a real person when the account lists them. Never call a staff member a placeholder, a test user or not a real person. ` + (staff.length ? `The cockpit's staff check matched these people in THIS account by email before the build: ${staff.join(", ")}. They are real staff members. ` : "") + `Say a custom value is empty only when the build reply's customValues.empty lists it, and name exactly those; a hand-off's instruction lists every value it covers, filled or not, so it is never evidence that one is empty. Say a calendar keeps template hours only when the build reply's composed warnings say so.`;
29943
+ }
29944
+ function buildStageReusePrompt(locationId2, locationName, saved, ctx) {
29904
29945
  return `You are running ONE unattended step of a GHL Command Blueprint build. Use ONLY the ghl MCP tools.
29905
29946
  AN APPROVED BUILD PLAN IS ALREADY ON FILE for this account (plan ${saved.planId ?? "(no id)"}, saved ${saved.savedAt}). This run REUSES it. Do NOT write, rewrite, improve or re-derive a plan, and do NOT rebuild the Brief.
29906
29947
  What the saved plan says should exist:
@@ -29911,6 +29952,7 @@ ${saved.summary}
29911
29952
  4. Read back what landed (get_pipelines, list_workflows_full, get_calendars) and compare it, name for name, with the saved plan above. ${BUILD_REPLY_RULE}
29912
29953
  5. ${STAFF_NOTIFICATION_RULE} A notification step whose recipient is empty is listed in issues as waiting for a staff member, never left unmentioned.
29913
29954
  6. ${MISSING_STEP_RULE} ${STAFF_PAUSE_RULE}
29955
+ 7. ${buildFactsRule(ctx)}
29914
29956
  If the tool reports that no saved plan exists, STOP and finish with RESULT: {"ok":false,"error":"no saved plan on this machine \u2014 run the build stage fresh"}.
29915
29957
  If apply_build_plan answers phase:"scope" (nothing is selected for this client, or the selection changed after the plan was saved), STOP: build nothing else and finish with RESULT: {"ok":false,"error":"<its error, word for word>"}.
29916
29958
  ${NO_INVENTED_CAUSE_RULE}
@@ -30103,6 +30145,7 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
30103
30145
  } catch {
30104
30146
  }
30105
30147
  }
30148
+ if (stage === 1 && outcome.issues) outcome.issues = briefIssuesShown(outcome.issues);
30106
30149
  const detail = outcome.error || (outcome.issues?.length ? outcome.issues.join(" \xB7 ") : "") || outcome.summary || "no detail reported";
30107
30150
  const success = outcome.formId ? `Done: form "${outcome.formName}" (${outcome.formId})` : `Done: ${outcome.summary ?? "stage complete"}`;
30108
30151
  onEvent({ kind: "result", line: outcome.ok ? success : `Not passed: ${detail}` });
@@ -30138,13 +30181,14 @@ var init_stage_runner = __esm({
30138
30181
  init_claude_cli();
30139
30182
  init_claude_locate();
30140
30183
  init_result_text();
30184
+ init_brief_issues();
30141
30185
  init_account_rules();
30142
30186
  init_account_rules();
30143
30187
  init_client_record();
30144
30188
  init_workflow_builder_client();
30145
30189
  STAFF_NOTIFICATION_RULE = `Staff notifications ALWAYS stay in the workflow. Call get_users on THIS account and use only a user id it returns as the recipient. If get_users returns no users, leave the recipient EMPTY ("") \u2014 never use a user id from another account, from the agency, from an earlier conversation or from memory (a foreign id looks like it worked and notifies nobody). An empty recipient can make publish fail: then keep the workflow as a draft with the notification step still in it, and list that step in the RESULT issues as "waiting for a staff member: add one in ${GHL_ADD_STAFF_CLICK_PATH}, then pick them on the notification step and publish".`;
30146
30190
  STAFF_PAUSE_RULE = `If apply_build_plan answers with staffPause, copy its reason into issues word for word and do not call those calendars built.`;
30147
- BUILD_REPLY_RULE = `A long apply_build_plan reply comes back shortened, with replyTrimmed on it; it still holds everything your report needs (summary, missingSteps, workflowsNotBuilt, staffPause, calendars, countsByType, notClean, templates, nextManualSteps and, on a dry run, composed.copySlots), so report from it. If you need a section it lists under sectionsNotShownInFull, read it with apply_build_plan detail:{of:"dry_run" or "execute",section:"<name>"}; that call builds nothing.`;
30191
+ BUILD_REPLY_RULE = `A long apply_build_plan reply comes back shortened, with replyTrimmed on it; it still holds everything your report needs (summary, missingSteps, workflowsNotBuilt, staffPause, calendars, countsByType, notClean, templates, nextManualSteps, customValues and, on a dry run, composed.copySlots), so report from it. If you need a section it lists under sectionsNotShownInFull, read it with apply_build_plan detail:{of:"dry_run" or "execute",section:"<name>"}; that call builds nothing.`;
30148
30192
  MISSING_STEP_RULE = `A step the plan asks for that is not in the account is NEVER dropped from your report. If apply_build_plan answers with a non-empty missingSteps, put EVERY one of those steps in issues, naming the workflow and the step, and say the build is not finished. A later run reporting "refreshed in place" does not close a gap an earlier run reported \u2014 only the step actually being in the workflow does. Never call a build complete while missingSteps has anything in it. If apply_build_plan answers with a non-empty workflowsNotBuilt, put every one of those workflows in issues with its reason, and never call the build complete while workflowsNotBuilt has anything in it.`;
30149
30193
  NO_INVENTED_CAUSE_RULE = `NEVER invent a reason a tool failed, and never state an HTTP status code a tool did not actually return. When a tool answers with an explanation of its own \u2014 an "errorKind", a "next", or a plain-English reason \u2014 quote THAT, verbatim, as the cause, and stop there. Do not add your own diagnosis on top of it, and do not guess at credentials, scopes or permissions: those guesses read as fact to the person acting on them and send them days in the wrong direction. If a tool failed and told you nothing useful, say exactly that \u2014 "it failed and did not say why" \u2014 rather than filling the gap.`;
30150
30194
  STAGE_SPECS = {
@@ -30219,7 +30263,7 @@ If anything fails, finish with: RESULT: {"ok":false,"error":"<short reason>"}`
30219
30263
  2. Find the answers. They arrive one of TWO ways and you must check BOTH:
30220
30264
  (a) a real form submission \u2014 get_forms then get_form_submissions_full; or
30221
30265
  (b) an intake completed in the cockpit \u2014 search_contacts for the tag "blueprint-intake-received" and read that contact's custom fields (get_contact), which hold the answers under "Intake: <question>" fields.
30222
- 3. Turn whichever you found into a Brief with normalize_submission_to_brief (for a form submission), then check it with validate_brief. If the answers came from the tagged contact, build the Brief from those field values directly and call validate_brief on it too. Booking calendars go at the TOP level of the Brief as calendars:[{"name":"...","type":"one_on_one"|"round_robin"|"class","staffNames":["..."],"durationMinutes":30}], one entry per calendar, never inside goal. Report validate_brief gaps in issues as warnings and finish ok:true.
30266
+ 3. Turn whichever you found into a Brief with normalize_submission_to_brief (for a form submission), then check it with validate_brief. If the answers came from the tagged contact, build the Brief from those field values directly and call validate_brief on it too. Booking calendars go at the TOP level of the Brief as calendars:[{"name":"...","type":"one_on_one"|"round_robin"|"class","staffNames":["..."],"durationMinutes":30}], one entry per calendar, never inside goal. Put each real gap validate_brief reports in issues, in plain words, and finish ok:true. When validate_brief finds nothing, issues is [] \u2014 never report the clean result itself as an issue.
30223
30267
  Use these authoritative normalized intake answers when provided: ${JSON.stringify(ctx?.normalizedAnswers ?? {})}.
30224
30268
  Do NOT create, update or delete anything \u2014 this step only reads and normalizes.
30225
30269
  Only if BOTH paths come up empty, finish with RESULT: {"ok":false,"error":"Open the real form, submit it, then refresh."}.
@@ -30266,13 +30310,14 @@ RESULT: {"ok":true,"summary":"<one sentence: who the client is and what they wan
30266
30310
  "mcp__ghl__get_contact",
30267
30311
  "mcp__ghl__get_custom_fields"
30268
30312
  ],
30269
- prompt: (locationId2, locationName, ctx) => ctx?.savedPlan ? buildStageReusePrompt(locationId2, locationName, ctx.savedPlan) : `You are running ONE unattended step of a GHL Command Blueprint build. Use ONLY the ghl MCP tools.
30313
+ prompt: (locationId2, locationName, ctx) => ctx?.savedPlan ? buildStageReusePrompt(locationId2, locationName, ctx.savedPlan, ctx) : `You are running ONE unattended step of a GHL Command Blueprint build. Use ONLY the ghl MCP tools.
30270
30314
  THE PLAN IS COMPOSED IN CODE, NOT BY YOU (plan and build were split 2026-08-26 after a run spent 18 of its 25 minutes writing 108 workflow actions by hand). You never author workflows, actions, emails or texts unless step 6's fallback fires.
30271
30315
  1. switch_location to ${locationId2} (${locationName}); verify with get_current_location and stop if it is not that location.
30272
30316
  2. Rebuild the Brief. The answers arrive one of TWO ways and you must check BOTH:
30273
30317
  (a) a real form submission \u2014 get_forms then get_form_submissions_full, then normalize_submission_to_brief; or
30274
30318
  (b) an intake completed in the cockpit \u2014 search_contacts for the tag "blueprint-intake-received", read that contact with get_contact, and assemble the Brief directly from its "Intake: <question>" custom fields (get_custom_fields maps ids to names). apply_build_plan accepts a Brief assembled this way.
30275
30319
  Only if BOTH come up empty, stop and report that there are no intake answers yet \u2014 never tell the operator to fill in the form when path (b) was never checked.
30320
+ Carry the "Business Hours" answer onto the Brief as business.hours, word for word: the build sets each calendar's open hours from it.
30276
30321
  Use these authoritative normalized intake answers when provided: ${JSON.stringify(ctx?.normalizedAnswers ?? {})}.
30277
30322
  3. Call apply_build_plan with fromPreset:true, preset:"${presetForIndustry(ctx?.industry)}", brief:<the Brief from step 2>, mode:"dry_run". The server expands the industry preset with the client's staff, calendars (appointment lengths included), pipeline stages, offer and notification rules in under a second, validates it, and reports what would be built.
30278
30323
  ${BUILD_REPLY_RULE}
@@ -30283,6 +30328,7 @@ Use these authoritative normalized intake answers when provided: ${JSON.stringif
30283
30328
  8. ${STAFF_NOTIFICATION_RULE} In a hand-authored fallback plan, a notification's "to" is that user id, or "" when the account has no users.
30284
30329
  9. ${MISSING_STEP_RULE} ${STAFF_PAUSE_RULE}
30285
30330
  10. ${NO_INVENTED_CAUSE_RULE}
30331
+ 11. ${buildFactsRule(ctx)}
30286
30332
  If apply_build_plan answers phase:"scope" (nothing is confirmed or selected for this client, or the selection changed), STOP: build nothing else and finish with RESULT: {"ok":false,"error":"<its error, word for word>"}.
30287
30333
  Finish with EXACTLY one final line:
30288
30334
  RESULT: {"ok":true,"summary":"<what was created: counts of pipelines/fields/tags/workflows, and whether the plan came from the preset or the fallback>","issues":["<anything skipped or needing a human, including every notification step waiting for a staff member>"]}` + loadPlanGuide(ctx?.industry)
@@ -31589,7 +31635,7 @@ function uniqueVerifyJobs(jobs2, notificationCount, owned = 0) {
31589
31635
  function renderVerifyReport(records, notes) {
31590
31636
  const { rows: rows2, allowPublishing } = verifyReadback(records);
31591
31637
  const normal2 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
31592
- let removed = false;
31638
+ const removedClaims = /* @__PURE__ */ new Set();
31593
31639
  const aliases2 = (r) => [
31594
31640
  r.name,
31595
31641
  r.ref,
@@ -31601,12 +31647,12 @@ function renderVerifyReport(records, notes) {
31601
31647
  ...r.type === "workflow" ? ["all workflows"] : []
31602
31648
  ].map(normal2).filter(Boolean);
31603
31649
  const names = [...new Set(rows2.flatMap(aliases2))].sort((a, b) => b.length - a.length);
31604
- const mentionPattern = names.length ? new RegExp(`\\b(?:${names.join("|")})\\b`, "g") : void 0;
31650
+ const mentionPattern2 = names.length ? new RegExp(`\\b(?:${names.join("|")})\\b`, "g") : void 0;
31605
31651
  const action = /^(?:please )?(?:assign|add|open|connect|register|confirm|review|check|select|choose|set|finish|complete|build|create|publish|get|enter|update|remove|fix|configure|test|retry|run|use|see|inspect|verify|invite|schedule|book|enable|disable|save|send|provide|upload|download|link|map|edit|change|replace|resolve|contact|ask|install|purchase|obtain|reconnect|authorize|grant|authenticate|sign)\b/i;
31606
31652
  const judge = (sentence) => {
31607
31653
  const mentions = [];
31608
31654
  const text2 = normal2(sentence.replace(/\b(?:isn't|wasn't|aren't|weren't)\b/gi, "is not").replace(/\b(?:doesn't|don't)\b/gi, "does not").replace(/[;,]/g, " CLAUSEBREAK "));
31609
- const tokenized = mentionPattern ? text2.replace(mentionPattern, (name) => {
31655
+ const tokenized = mentionPattern2 ? text2.replace(mentionPattern2, (name) => {
31610
31656
  mentions.push(rows2.filter((r) => aliases2(r).includes(name)));
31611
31657
  return `OBJECT${mentions.length - 1}`;
31612
31658
  }) : text2;
@@ -31653,7 +31699,7 @@ function renderVerifyReport(records, notes) {
31653
31699
  const filter = (text2) => sentences(text2).filter((sentence) => {
31654
31700
  if (/could not read:|unchecked:/i.test(sentence)) return false;
31655
31701
  if (judge(sentence).contradicted || !allowPublishing && /\b(everything|all (?:planned )?objects|the account)\b.*\b(ready|complete|working|built)\b/i.test(sentence)) {
31656
- removed = true;
31702
+ removedClaims.add(normal2(sentence));
31657
31703
  return false;
31658
31704
  }
31659
31705
  return allowPublishing || !publishing.test(sentence);
@@ -31715,14 +31761,36 @@ function renderVerifyReport(records, notes) {
31715
31761
  const summary = [
31716
31762
  `${records.buildOk === true ? verifyPassedLine(records.memberJobs ?? rows2.filter((r) => r.status === "pending").length + issues.length) : `${present} of ${rows2.length} planned items are in the account; ${attention} need attention.`}${drafts ? `
31717
31763
  ${drafts}` : ""}${liveLine ? `
31718
- ${liveLine}` : ""}${removed ? "\nRemoved a claim the read-back contradicts." : ""}`,
31764
+ ${liveLine}` : ""}${removedClaims.size ? `
31765
+ ${removedClaimNote(removedClaims.size)}` : ""}`,
31719
31766
  ["What is not right", ...diagnosis.length ? diagnosis : ["No planned items need attention."]].join("\n"),
31720
31767
  ["Every planned item", ...[...grouped].flatMap(([type, items]) => [typeLabels[type], ...items.map((r) => ` ${r.line}`)])].join("\n"),
31721
31768
  ...summaryNotes.length ? [["Notes from the check", ...summaryNotes].join("\n")] : []
31722
31769
  ].join("\n\n");
31723
- return { summary, issues };
31770
+ const cvNames = customValueDisplayNames(rows2, records.deps);
31771
+ return { summary: plainCustomValueNames(summary, cvNames), issues: issues.map((i) => plainCustomValueNames(i, cvNames)) };
31772
+ }
31773
+ function removedClaimNote(count2) {
31774
+ return `${count2 === 1 ? "One thing the check reported was" : `${count2} things the check reported were`} left out, because this client's account shows otherwise; the lists below are what the account really holds.`;
31775
+ }
31776
+ function customValueDisplayName(key) {
31777
+ const words2 = key.trim().replace(/^cv\./, "").replace(/[_\s]+/g, " ").trim();
31778
+ return words2 ? words2.charAt(0).toUpperCase() + words2.slice(1) : key;
31779
+ }
31780
+ function customValueDisplayNames(rows2, deps = []) {
31781
+ const keys = /* @__PURE__ */ new Set();
31782
+ for (const r of [...rows2, ...deps]) {
31783
+ if (typeof r.ref === "string" && r.ref.startsWith("cv.")) keys.add(r.ref.slice(3));
31784
+ if (r.type === "custom_value" && typeof r.name === "string") keys.add(r.name.trim());
31785
+ }
31786
+ return new Map([...keys].filter((k) => /^[a-z0-9]+(?:_[a-z0-9]+)+$/.test(k)).map((k) => [k, customValueDisplayName(k)]));
31787
+ }
31788
+ function plainCustomValueNames(text2, names) {
31789
+ if (!names.size) return text2;
31790
+ const keys = [...names.keys()].sort((a, b) => b.length - a.length);
31791
+ return text2.replace(new RegExp(`(?<![\\w.{(])(?:${keys.join("|")})(?![\\w})])`, "g"), (k) => names.has(k) ? `${names.get(k)} (${k})` : k);
31724
31792
  }
31725
- var readLabels, readLabel, BUILDER_SIGNIN_BODY, BUILDER_SIGNIN_SPANS, BUILDER_SIGNIN_TOKENS, BUSY_SIGNIN_LINE, BUSY_MESSAGE, cleanVerifyName, cleanVerifyText, sentences, publishing, detailClauses, oneSentence, oneSentenceOf, expectedDraft, typeLabels, firstFive, verifyTypeOrder, verifyGroup, jobText, countWord, normalJob;
31793
+ var readLabels, readLabel, BUILDER_SIGNIN_BODY, BUILDER_SIGNIN_SPANS, BUILDER_SIGNIN_TOKENS, BUSY_SIGNIN_LINE, BUSY_MESSAGE, cleanVerifyName, cleanVerifyText, sentences, publishing, detailClauses, oneSentence, oneSentenceOf, expectedDraft, typeLabels, firstFive, verifyTypeOrder, verifyGroup, jobText, countWord, normalJob, REMOVED_CLAIM_LINE;
31726
31794
  var init_member_progress = __esm({
31727
31795
  "src/member-progress.ts"() {
31728
31796
  "use strict";
@@ -31765,6 +31833,7 @@ var init_member_progress = __esm({
31765
31833
  jobText = (text2) => text2.trim().replace(/\s+/g, " ").replace(/([^.!?])$/, "$1.");
31766
31834
  countWord = (text2) => /^\d+$/.test(text2) ? Number(text2) : ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve"].indexOf(text2.toLowerCase());
31767
31835
  normalJob = (text2) => text2.toLowerCase().replace(/^manual action:\s*/, "").replace(/[^a-z0-9]+/g, " ").trim();
31836
+ REMOVED_CLAIM_LINE = /^(?:One thing the check reported was|\d+ things the check reported were) left out\b|^Removed a claim\b/i;
31768
31837
  }
31769
31838
  });
31770
31839
 
@@ -34015,7 +34084,7 @@ var require_package = __commonJS({
34015
34084
  "package.json"(exports2, module2) {
34016
34085
  module2.exports = {
34017
34086
  name: "@elitedcs/ghl-mcp",
34018
- version: "3.89.0-beta.5",
34087
+ version: "3.89.0-beta.6",
34019
34088
  mcpName: "io.github.drjerryrelth/ghl-command",
34020
34089
  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.",
34021
34090
  main: "dist/index.js",
@@ -36440,7 +36509,7 @@ function reportDocumentHtml(run, card, opts) {
36440
36509
  const failed = stepProblems.filter((p) => p.kind === "failed_holding" || p.kind === "failed_gave_up").length;
36441
36510
  const stepsKnown = !!st?.read && allSteps.length > 0;
36442
36511
  const period = `${localTime2(run.startedAt)}${run.finishedAt ? ` to ${localTime2(run.finishedAt)}` : ""}`;
36443
- const clock2 = (ms) => new Date(ms).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
36512
+ const clock3 = (ms) => new Date(ms).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
36444
36513
  const tiles = [
36445
36514
  stepsKnown ? statTile2(judged, "Steps checked") : statTile2("\u2013", "Steps checked", "warn"),
36446
36515
  statTile2(f.sent, "Messages sent"),
@@ -36452,7 +36521,7 @@ function reportDocumentHtml(run, card, opts) {
36452
36521
  ].join("");
36453
36522
  const worst = f.problems[0];
36454
36523
  const sentAt = worst ? f.timeline.find((t) => t.messageId === worst.messageId) : void 0;
36455
- const quote = worst ? callout2(`A new lead got "${marked2(worst.quote, "")}"`, { quote: true, tone: "bad", cite: `${worst.sentBy ?? worst.title}${sentAt ? `, sent ${clock2(run.startedAt + sentAt.minute * 6e4)}` : ""}${worst.kind === "blank_field" && !run.leadHasFirstName ? " to a lead with no first name on file" : ""}` }) : stepProblems[0] ? callout2(`"${esc4(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 ? callout2(`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(", ") }) : callout2(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" });
36524
+ const quote = worst ? callout2(`A new lead got "${marked2(worst.quote, "")}"`, { quote: true, tone: "bad", cite: `${worst.sentBy ?? worst.title}${sentAt ? `, sent ${clock3(run.startedAt + sentAt.minute * 6e4)}` : ""}${worst.kind === "blank_field" && !run.leadHasFirstName ? " to a lead with no first name on file" : ""}` }) : stepProblems[0] ? callout2(`"${esc4(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 ? callout2(`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(", ") }) : callout2(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" });
36456
36525
  const stepTitle = (p) => POINTS[p.kind].what;
36457
36526
  const stepCard = (p, controls) => fixCard(stepTitle(p), POINTS[p.kind].points, p.detail, esc4(`"${p.stepName}" in ${p.workflowName}`), p.fix, controls);
36458
36527
  const DRAFT_WHY = "Publishing starts sending to every real lead, so publish it yourself in GoHighLevel when it is ready.";
@@ -37425,9 +37494,9 @@ async function previewUndo(deps, run, key) {
37425
37494
  return view(p);
37426
37495
  }
37427
37496
  async function approveFix(deps, run, hash, confirmedWording) {
37428
- const clock2 = deps.now ?? Date.now;
37497
+ const clock3 = deps.now ?? Date.now;
37429
37498
  const p = previews.get(hash);
37430
- if (!p || p.expires < clock2()) {
37499
+ if (!p || p.expires < clock3()) {
37431
37500
  previews.delete(hash);
37432
37501
  return { ok: false, written: false, error: "This preview has expired or was already used. Nothing was changed. Show the change again." };
37433
37502
  }
@@ -37459,7 +37528,7 @@ async function approveFix(deps, run, hash, confirmedWording) {
37459
37528
  workflowName: p.workflowName,
37460
37529
  stepName: p.stepName,
37461
37530
  previewHash: p.hash,
37462
- at: clock2(),
37531
+ at: clock3(),
37463
37532
  state: "writing",
37464
37533
  prior: current,
37465
37534
  written: p.after,
@@ -37515,7 +37584,7 @@ async function approveFix(deps, run, hash, confirmedWording) {
37515
37584
  const verdict = verifyReadBack(p.after, back, p.added, p.checks);
37516
37585
  const detail = verdict.held ? p.undo ? "Read back from GoHighLevel: the step is back the way it was." : "Read back from GoHighLevel: the change held." : `${p.undo ? "The undo was written" : "Written"}, but reading it back did not match: ${verdict.problems.join("; ")}. ${NO_AUTO_ROLLBACK}`;
37517
37586
  putRecord(run.locationId, run.runId, { ...rec2, state: "applied", held: verdict.held, detail, afterHash: templatesHash(back) }, deps.base);
37518
- if (undone && verdict.held) putRecord(run.locationId, run.runId, { ...undone, undoneAt: clock2(), undoneBy: rec2.id }, deps.base);
37587
+ if (undone && verdict.held) putRecord(run.locationId, run.runId, { ...undone, undoneAt: clock3(), undoneBy: rec2.id }, deps.base);
37519
37588
  return { ok: true, held: verdict.held, detail, undo: p.undo };
37520
37589
  } finally {
37521
37590
  writing.delete(p.workflowId);
@@ -38072,7 +38141,7 @@ function createBriefRedactor(secrets) {
38072
38141
  const matcher = new RegExp(`(?=(${pattern}))`, "g");
38073
38142
  return (text2) => {
38074
38143
  matcher.lastIndex = 0;
38075
- const pieces = [];
38144
+ const pieces2 = [];
38076
38145
  let copiedUntil = 0;
38077
38146
  let start = -1;
38078
38147
  let end = 0;
@@ -38086,15 +38155,15 @@ function createBriefRedactor(secrets) {
38086
38155
  end = nextEnd;
38087
38156
  } else if (nextStart <= end) end = Math.max(end, nextEnd);
38088
38157
  else {
38089
- pieces.push(text2.slice(copiedUntil, start), marker);
38158
+ pieces2.push(text2.slice(copiedUntil, start), marker);
38090
38159
  copiedUntil = end;
38091
38160
  start = nextStart;
38092
38161
  end = nextEnd;
38093
38162
  }
38094
38163
  }
38095
38164
  if (start < 0) return text2;
38096
- pieces.push(text2.slice(copiedUntil, start), marker, text2.slice(end));
38097
- return pieces.join("");
38165
+ pieces2.push(text2.slice(copiedUntil, start), marker, text2.slice(end));
38166
+ return pieces2.join("");
38098
38167
  };
38099
38168
  }
38100
38169
  var init_brief_redaction = __esm({
@@ -38847,7 +38916,7 @@ function clientTitle(name) {
38847
38916
  return s.charAt(0).toUpperCase() + s.slice(1);
38848
38917
  }
38849
38918
  function clientCheckHeadline(summary) {
38850
- return summary.split(/\n\s*\n/)[0].split("\n").filter((line3) => line3.trim() && !/^Removed a claim/i.test(line3.trim())).join(" ").trim();
38919
+ return summary.split(/\n\s*\n/)[0].split("\n").filter((line3) => line3.trim() && !REMOVED_CLAIM_LINE.test(line3.trim())).join(" ").trim();
38851
38920
  }
38852
38921
  function scrubLatestCheck(raw) {
38853
38922
  if (!raw) return void 0;
@@ -39176,6 +39245,7 @@ var init_client_status = __esm({
39176
39245
  init_handoff_pack();
39177
39246
  init_branding();
39178
39247
  init_svg_sanitize();
39248
+ init_member_progress();
39179
39249
  CLIENT_STATE_LABEL = {
39180
39250
  "in-place": "Live in your account",
39181
39251
  "not-yet": "Not confirmed yet",
@@ -40967,12 +41037,12 @@ function splitList(text2, opts = {}) {
40967
41037
  }
40968
41038
  function parsePricePoints(text2) {
40969
41039
  const out = [];
40970
- const segments = text2.split(/\r?\n|;|\.\s+/);
41040
+ const segments2 = text2.split(/\r?\n|;|\.\s+/);
40971
41041
  const ENTRY = /^(.*?)\s*[-–—:|]?\s*\$?\s*([\d,]+(?:\.\d+)?)\s*(?:[-–—]\s*\$?\s*([\d,]+(?:\.\d+)?))?\s*(\/\s*[a-z]+|(?:per|an?|each)\s+[a-z]+)?\s*$/i;
40972
41042
  const clean4 = (s) => s.replace(/[\s.,:;|–—-]+$/, "").trim();
40973
41043
  const num3 = (s) => s.replace(/,/g, "");
40974
41044
  const recur = (s) => s ? "/" + s.replace(/^\/|^(?:per|an?|each)\b/i, "").trim().toLowerCase() : "";
40975
- for (const segment of segments) {
41045
+ for (const segment of segments2) {
40976
41046
  const t = segment.trim().replace(/\.$/, "");
40977
41047
  if (!t) continue;
40978
41048
  const m = t.match(ENTRY);
@@ -41006,8 +41076,8 @@ function staffFromLooseLine(line3) {
41006
41076
  const after = line3.slice(m.index + m[0].length).replace(/^\s*[)>\]]+/, "").replace(LIGHT_EDGE, "");
41007
41077
  let mobile;
41008
41078
  const roleParts = [];
41009
- const pieces = after.split(/\s+[-–—|]\s+|[,;|]/).map((x) => x.replace(LIGHT_EDGE, "").replace(/^[(\[<](.*)[)\]>]$/, "$1").trim()).filter(Boolean);
41010
- for (const piece of pieces) {
41079
+ const pieces2 = after.split(/\s+[-–—|]\s+|[,;|]/).map((x) => x.replace(LIGHT_EDGE, "").replace(/^[(\[<](.*)[)\]>]$/, "$1").trim()).filter(Boolean);
41080
+ for (const piece of pieces2) {
41011
41081
  const bare = piece.replace(/^(mobile|phone|cell|role)\s*[:=]\s*/i, "");
41012
41082
  if (!mobile && PHONE_RE.test(bare) && /\d{7,}/.test(bare.replace(/\D/g, ""))) mobile = bare;
41013
41083
  else if (!new RegExp(EMAIL_ANYWHERE.source).test(bare)) roleParts.push(bare);
@@ -41021,8 +41091,8 @@ function parseStaffLines(text2) {
41021
41091
  const emailsIn = (t) => (t.match(EMAIL_ANYWHERE) ?? []).length;
41022
41092
  const lines = cleanLines(text2).flatMap((line3) => {
41023
41093
  if (!line3.includes(";") || emailsIn(line3) < 2) return [line3];
41024
- const pieces = line3.split(";").map((p) => p.trim()).filter(Boolean);
41025
- return pieces.every((p) => emailsIn(p) === 1) ? pieces : [line3];
41094
+ const pieces2 = line3.split(";").map((p) => p.trim()).filter(Boolean);
41095
+ return pieces2.every((p) => emailsIn(p) === 1) ? pieces2 : [line3];
41026
41096
  });
41027
41097
  lines.forEach((line3, i) => {
41028
41098
  if (emailsIn(line3) > 1) {
@@ -43816,7 +43886,7 @@ async function clientPreflight(registry2, locationId2, deps = {}) {
43816
43886
  }
43817
43887
  lines.push({
43818
43888
  key: "authentication",
43819
- ok: authenticated,
43889
+ ok: authenticated || !key,
43820
43890
  text: authenticated ? "Your Anthropic key authenticates, so powered runs will bill it." : key ? "The saved Anthropic key did not authenticate, so powered runs will fall back to your Claude sign-in on this computer. Replace it in Your agency, or remove it." : "No Anthropic key saved. Powered runs use your Claude sign-in on this computer and cost you nothing extra."
43821
43891
  });
43822
43892
  const blocking = lines.filter((l) => l.key !== "authentication" && !l.advisory);
@@ -44321,7 +44391,7 @@ var init_customization = __esm({
44321
44391
  q({ key: "capacity_limited", label: "Do trial spots really run out most weeks?", section: "offer", sectionLabel: "Offer and pricing", dataType: "SINGLE_OPTIONS", options: ["Yes", "No"], briefPath: "offer.capacityLimited", briefKind: "boolean" }),
44322
44392
  q({ key: "programs", label: "Programs you sell after the trial, with prices", section: "offer", sectionLabel: "Offer and pricing", dataType: "LARGE_TEXT", placeholder: "One per line, e.g. Signature 12-session program $2,997", briefPath: "offer.programs", briefKind: "pricePoints" }),
44323
44393
  q({ key: "contraindications", label: "Who should not book (your screening list)", section: "offer", sectionLabel: "Offer and pricing", dataType: "LARGE_TEXT", placeholder: "e.g. pregnant or breastfeeding, cancer in the last 12 months, pacemaker", briefPath: "offer.contraindications" }),
44324
- q({ key: "prep_instructions", label: "How should clients prepare for the visit?", section: "offer", sectionLabel: "Offer and pricing", dataType: "LARGE_TEXT", placeholder: "e.g. Please arrive 10 minutes early and wear loose clothing. One or two sentences. This replaces the day-before reminder's usual 'come 10 minutes early' line, so include arriving early if you want it said.", briefPath: "offer.prepInstructions" }),
44394
+ q({ key: "prep_instructions", label: "How should clients prepare for the visit?", section: "offer", sectionLabel: "Offer and pricing", dataType: "LARGE_TEXT", placeholder: "e.g. Please arrive 10 minutes early and wear loose clothing. One or two sentences. They go in the day-before reminder in place of its usual line, 'Please come about 10 minutes early', so include arriving early if you want the reminder to say it. The booking confirmation already asks them to come 10 minutes early.", briefPath: "offer.prepInstructions" }),
44325
44395
  q({ key: "anchor_price", label: "What a comparable treatment usually costs elsewhere (optional)", section: "offer", sectionLabel: "Offer and pricing", dataType: "TEXT", placeholder: "e.g. $300 to $500. Leave blank if you are not sure.", briefPath: "offer.anchorPrice" }),
44326
44396
  q({ key: "visit_only_price", label: "Do you offer a program price that is only good for 72 hours after the trial visit?", section: "offer", sectionLabel: "Offer and pricing", dataType: "SINGLE_OPTIONS", options: ["Yes", "No"], briefPath: "offer.visitOnlyPrice", briefKind: "boolean" }),
44327
44397
  q({ key: "testimonial_quote", label: "One short client quote you may use in texts (optional, no name, no health or weight claims)", section: "assets", sectionLabel: "Assets on hand", dataType: "LARGE_TEXT", placeholder: "Leave blank if you have none", briefPath: "proof.testimonialQuote" }),
@@ -44701,6 +44771,159 @@ var init_testimonial_guard = __esm({
44701
44771
  }
44702
44772
  });
44703
44773
 
44774
+ // src/intake-to-build/business-hours.ts
44775
+ function dayIndex(word) {
44776
+ const w = word.toLowerCase().replace(/\.$/, "");
44777
+ for (const [re, n] of DAY_WORDS) if (re.test(w)) return n;
44778
+ return null;
44779
+ }
44780
+ function parseDays(spec) {
44781
+ const s = spec.toLowerCase().trim();
44782
+ if (GROUP_WORDS[s]) return [...GROUP_WORDS[s]];
44783
+ const days = [];
44784
+ for (const part of s.split(/\s*(?:,|&|\/|\band\b)\s*/).filter(Boolean)) {
44785
+ const range = part.split(/\s*(?:-|–|—|\bto\b|\bthrough\b|\bthru\b)\s*/);
44786
+ if (range.length === 1) {
44787
+ const d = dayIndex(range[0]);
44788
+ if (d === null) return null;
44789
+ days.push(d);
44790
+ } else if (range.length === 2) {
44791
+ const a = dayIndex(range[0]), b = dayIndex(range[1]);
44792
+ if (a === null || b === null) return null;
44793
+ for (let d = a; ; d = (d + 1) % 7) {
44794
+ days.push(d);
44795
+ if (d === b) break;
44796
+ }
44797
+ } else return null;
44798
+ }
44799
+ return days.length ? days : null;
44800
+ }
44801
+ function parseClock(text2) {
44802
+ const t = text2.toLowerCase().replace(/\s+/g, "").replace(/\./g, "");
44803
+ if (t === "noon") return { h: 12, m: 0, meridiem: "pm" };
44804
+ const m = /^(\d{1,2})(?::(\d{2}))?(am|pm|a|p)?$/.exec(t);
44805
+ if (!m) return null;
44806
+ const h = Number(m[1]), min = m[2] ? Number(m[2]) : 0;
44807
+ if (h > 23 || min > 59) return null;
44808
+ const mer = m[3] ? m[3].startsWith("a") ? "am" : "pm" : void 0;
44809
+ if (mer && (h < 1 || h > 12)) return null;
44810
+ return { h, m: min, meridiem: mer };
44811
+ }
44812
+ function to24(c) {
44813
+ if (c.meridiem === "am") return c.h === 12 ? 0 : c.h;
44814
+ if (c.meridiem === "pm") return c.h === 12 ? 12 : c.h + 12;
44815
+ return c.h;
44816
+ }
44817
+ function parseWindow(text2) {
44818
+ const parts = text2.split(/\s*(?:-|–|—|\bto\b|\buntil\b|\btill\b)\s*/i).filter((p) => p.trim());
44819
+ if (parts.length !== 2) return null;
44820
+ const open2 = parseClock(parts[0]), close = parseClock(parts[1]);
44821
+ if (!open2 || !close) return null;
44822
+ let openH;
44823
+ if (open2.meridiem) openH = to24(open2);
44824
+ else if (open2.h >= 13) openH = open2.h;
44825
+ else if (!close.meridiem && close.h >= 13) openH = open2.h;
44826
+ else if (!close.meridiem && (open2.h < 7 || open2.h > 12)) return null;
44827
+ else if (open2.h >= 5 && open2.h <= 12) openH = open2.h;
44828
+ else return null;
44829
+ let closeH;
44830
+ if (close.meridiem) closeH = to24(close);
44831
+ else if (close.h >= 13 || close.h === 0) closeH = close.h;
44832
+ else closeH = close.h > openH || close.h === openH && close.m > open2.m ? close.h : close.h + 12;
44833
+ if (!open2.meridiem && open2.h <= 12 && close.meridiem === "pm") {
44834
+ const openPm = open2.h === 12 ? 12 : open2.h + 12;
44835
+ if (openPm !== openH && openPm * 60 + open2.m < closeH * 60 + close.m) return null;
44836
+ }
44837
+ if (!open2.meridiem && !close.meridiem && open2.h <= 12 && closeH * 60 + close.m < 12 * 60) return null;
44838
+ const openMin = openH * 60 + open2.m, closeMin = closeH * 60 + close.m;
44839
+ if (closeH > 23 || closeMin <= openMin) return null;
44840
+ return { openHour: openH, openMinute: open2.m, closeHour: closeH, closeMinute: close.m };
44841
+ }
44842
+ function segments(text2) {
44843
+ const out = [];
44844
+ for (const piece of text2.split(/\s*(?:;|\n|\|)\s*/)) {
44845
+ let current = "";
44846
+ for (const part of piece.split(/\s*,\s*/)) {
44847
+ if (current && /\d|\b(?:closed|off|shut|noon)\b/i.test(current) && /^(?:sun|mon|tue|wed|thu|fri|sat|weekdays|weekends?|daily)/i.test(part)) {
44848
+ out.push(current);
44849
+ current = part;
44850
+ } else current = current ? `${current}, ${part}` : part;
44851
+ }
44852
+ if (current.trim()) out.push(current);
44853
+ }
44854
+ return out.filter((s) => s.trim());
44855
+ }
44856
+ function parseBusinessHours(answer2) {
44857
+ const text2 = String(answer2 ?? "").trim();
44858
+ if (!text2) return null;
44859
+ const byDay = /* @__PURE__ */ new Map();
44860
+ for (const raw of segments(text2)) {
44861
+ const seg = raw.trim().replace(/[.]$/, "");
44862
+ const m = DAYS_SPEC.exec(seg);
44863
+ if (!m) return null;
44864
+ const days = parseDays(m[1]);
44865
+ if (!days) return null;
44866
+ const rest = m[2].trim();
44867
+ const value = /^(?:closed|off|shut)$/i.test(rest) ? "closed" : parseWindow(rest);
44868
+ if (!value) return null;
44869
+ for (const d of days) {
44870
+ if (byDay.has(d)) return null;
44871
+ byDay.set(d, value);
44872
+ }
44873
+ }
44874
+ const open2 = [...byDay.entries()].filter((e) => e[1] !== "closed");
44875
+ if (!open2.length) return null;
44876
+ const order = [1, 2, 3, 4, 5, 6, 0];
44877
+ const blocks = [];
44878
+ for (const d of order) {
44879
+ const w = byDay.get(d);
44880
+ if (!w || w === "closed") continue;
44881
+ const same2 = blocks.find((b) => JSON.stringify(b.hours[0]) === JSON.stringify(w));
44882
+ if (same2) same2.daysOfTheWeek.push(d);
44883
+ else blocks.push({ daysOfTheWeek: [d], hours: [{ ...w }] });
44884
+ }
44885
+ return blocks;
44886
+ }
44887
+ function clock(h, m) {
44888
+ const suffix = h < 12 ? "am" : "pm";
44889
+ const h12 = h % 12 === 0 ? 12 : h % 12;
44890
+ return `${h12}${m ? `:${String(m).padStart(2, "0")}` : ""}${suffix}`;
44891
+ }
44892
+ function describeOpenHours(blocks) {
44893
+ return blocks.map((b) => {
44894
+ const days = [...b.daysOfTheWeek];
44895
+ const consecutive = days.every((d, i) => i === 0 || d === (days[i - 1] + 1) % 7);
44896
+ const dayText = days.length > 2 && consecutive ? `${DAY_NAMES2[days[0]]} to ${DAY_NAMES2[days[days.length - 1]]}` : days.map((d) => DAY_NAMES2[d]).join(days.length === 2 ? " and " : ", ");
44897
+ return `${dayText} ${b.hours.map((w) => `${clock(w.openHour, w.openMinute)} to ${clock(w.closeHour, w.closeMinute)}`).join(" and ")}`;
44898
+ }).join("; ");
44899
+ }
44900
+ var DAY_WORDS, GROUP_WORDS, DAY_TOKEN, DAYS_SPEC, DAY_NAMES2;
44901
+ var init_business_hours = __esm({
44902
+ "src/intake-to-build/business-hours.ts"() {
44903
+ "use strict";
44904
+ DAY_WORDS = [
44905
+ [/^sun(?:day)?s?$/, 0],
44906
+ [/^mon(?:day)?s?$/, 1],
44907
+ [/^tue(?:s(?:day)?)?s?$/, 2],
44908
+ [/^wed(?:s|nesday)?s?$/, 3],
44909
+ [/^thu(?:r(?:s(?:day)?)?)?s?$/, 4],
44910
+ [/^fri(?:day)?s?$/, 5],
44911
+ [/^sat(?:urday)?s?$/, 6]
44912
+ ];
44913
+ GROUP_WORDS = {
44914
+ weekdays: [1, 2, 3, 4, 5],
44915
+ weekends: [0, 6],
44916
+ weekend: [0, 6],
44917
+ daily: [0, 1, 2, 3, 4, 5, 6],
44918
+ "every day": [0, 1, 2, 3, 4, 5, 6],
44919
+ "7 days": [0, 1, 2, 3, 4, 5, 6]
44920
+ };
44921
+ DAY_TOKEN = String.raw`(?:sun|mon|tue|wed|thu|fri|sat)[a-z.]*`;
44922
+ DAYS_SPEC = new RegExp(String.raw`^((?:weekdays|weekends?|daily|every day|7 days)|${DAY_TOKEN}(?:\s*(?:-|–|—|,|&|/|\band\b|\bto\b|\bthrough\b|\bthru\b)\s*${DAY_TOKEN})*)\s*:?\s*(.*)$`, "i");
44923
+ DAY_NAMES2 = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
44924
+ }
44925
+ });
44926
+
44704
44927
  // src/intake-to-build/plan-from-preset.ts
44705
44928
  function toGsm7(s) {
44706
44929
  return GSM7_REPLACEMENTS.reduce((out, [re, to]) => out.replace(re, to), s);
@@ -45212,7 +45435,7 @@ function composeCalendars(skeletonCalendars, ctx) {
45212
45435
  if (kept.length > 0 && ctx.bookingWanted) {
45213
45436
  ctx.warnings.push(`The intake did not describe a booking calendar, so the preset's "${String(kept[0].name)}" calendar is built as-is \u2014 say how many calendars and who is on each to change it.`);
45214
45437
  }
45215
- return kept;
45438
+ return applyIntakeHours(kept, ctx);
45216
45439
  }
45217
45440
  const out = [];
45218
45441
  const usedRefs = /* @__PURE__ */ new Set();
@@ -45253,7 +45476,26 @@ function composeCalendars(skeletonCalendars, ctx) {
45253
45476
  resolveOwnerTeam(kept[i]);
45254
45477
  out.push(kept[i]);
45255
45478
  }
45256
- return out;
45479
+ return applyIntakeHours(out, ctx);
45480
+ }
45481
+ function applyIntakeHours(cals, ctx) {
45482
+ const answer2 = (ctx.brief.business?.hours ?? "").trim();
45483
+ if (!answer2 || cals.length === 0) return cals;
45484
+ const hours = parseBusinessHours(answer2);
45485
+ if (!hours) {
45486
+ const template = cals.find((c) => Array.isArray(c.openHours) && c.openHours.length)?.openHours;
45487
+ ctx.warnings.push(`The intake's business hours ("${answer2}") could not be read one way, so the calendars keep the template hours${template ? ` (${describeOpenHours(template)})` : ""}. Set each calendar's hours in GoHighLevel to match the client's.`);
45488
+ return cals;
45489
+ }
45490
+ for (const cal of cals) {
45491
+ cal.openHours = hours.map((b) => ({ daysOfTheWeek: [...b.daysOfTheWeek], hours: b.hours.map((w) => ({ ...w })) }));
45492
+ if (cal.availabilityType === void 0) cal.availabilityType = 0;
45493
+ }
45494
+ const shared = cals.filter((c) => c.calendarType === "round_robin" || c.calendarType === "collective").map((c) => `"${String(c.name)}"`);
45495
+ if (shared.length) {
45496
+ ctx.warnings.push(`Calendar hours were set from the intake (${describeOpenHours(hours)}). On ${shared.join(" and ")}, GoHighLevel offers a time only when the person assigned also has that time in their own availability, so check each person's availability covers these hours.`);
45497
+ }
45498
+ return cals;
45257
45499
  }
45258
45500
  function resolveActionUsers(action, ctx) {
45259
45501
  if (action.userRef === "user.owner") {
@@ -46043,6 +46285,7 @@ var init_plan_from_preset = __esm({
46043
46285
  init_offer_normalize();
46044
46286
  init_testimonial_guard();
46045
46287
  init_sms_rules();
46288
+ init_business_hours();
46046
46289
  init_plan_guide();
46047
46290
  init_claims_guard();
46048
46291
  PRESET_TOKEN_ROOTS = /* @__PURE__ */ new Set(["business", "offer", "audience", "voice", "goal", "proof"]);
@@ -46916,6 +47159,199 @@ var init_client_intake = __esm({
46916
47159
  }
46917
47160
  });
46918
47161
 
47162
+ // src/build-facts.ts
47163
+ function customValueKeyOf(name) {
47164
+ return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
47165
+ }
47166
+ function customValueStatusOf(planValues, accountValues, now = () => /* @__PURE__ */ new Date()) {
47167
+ const readAt = now().toISOString();
47168
+ if (!accountValues || planValues.length > 0 && accountValues.length === 0) return { filled: [], empty: [], unreadable: true, readAt };
47169
+ const byKey = new Map(accountValues.map((v) => [customValueKeyOf(v.name), v]));
47170
+ const filled = [];
47171
+ const empty = [];
47172
+ const unknown = [];
47173
+ for (const cv of planValues) {
47174
+ const live = byKey.get(customValueKeyOf(cv.name));
47175
+ if (!live) empty.push(cv.name);
47176
+ else if (typeof live.value !== "string") unknown.push(cv.name);
47177
+ else if (live.value.trim()) filled.push(cv.name);
47178
+ else empty.push(cv.name);
47179
+ }
47180
+ return { filled, empty, ...unknown.length ? { unknown } : {}, readAt };
47181
+ }
47182
+ function handoffCustomValueNote(handoffRef, planValues, status) {
47183
+ if (!status || status.unreadable) return "";
47184
+ const covered = planValues.filter((cv) => cv.filledBy === handoffRef).map((cv) => cv.name);
47185
+ if (!covered.length) return "";
47186
+ const filled = covered.filter((n) => status.filled.includes(n));
47187
+ const empty = covered.filter((n) => status.empty.includes(n));
47188
+ const list4 = (xs) => xs.length <= 1 ? xs.join("") : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
47189
+ const parts = [
47190
+ filled.length ? `${list4(filled)} ${filled.length === 1 ? "is" : "are"} filled` : "",
47191
+ empty.length ? `${list4(empty)} ${empty.length === 1 ? "is" : "are"} empty` : ""
47192
+ ].filter(Boolean);
47193
+ return parts.length ? ` Read back from the account after this build: ${parts.join("; ")}.` : "";
47194
+ }
47195
+ function pieces(text2) {
47196
+ const guarded2 = text2.replace(/\bDr\./g, "Dr\0");
47197
+ const out = [];
47198
+ const re = /([\s\S]*?(?:[.!?](?=\s|$)|(?=\n)|$))(\s*)/g;
47199
+ let m;
47200
+ while ((m = re.exec(guarded2)) && m[0].length) out.push({ sentence: m[1].replace(/Dr\u0000/g, "Dr."), sep: m[2] });
47201
+ return out;
47202
+ }
47203
+ function rejoin(parts) {
47204
+ const kept = parts.filter((p) => p !== null && p.sentence.trim() !== "");
47205
+ return kept.map((p, i) => p.sentence + (i < kept.length - 1 ? p.sep : "")).join("");
47206
+ }
47207
+ function claimsVerifiedPersonNotReal(sentence, verifiedStaff) {
47208
+ for (const raw of verifiedStaff) {
47209
+ const name = raw.trim();
47210
+ if (!name) continue;
47211
+ const n = esc11(name);
47212
+ const after = new RegExp(String.raw`\b${n}\b(?:,\s*|\s+(?:is|was|looks like|appears to be|seems to be|may be|might be)\s+|\s+(?:isn'?t|wasn'?t)\s+)(?:only\s+|just\s+|still\s+|probably\s+)?(?:(?:a|an|the)\s+)?${NOT_REAL_PHRASE}`, "i");
47213
+ const before = new RegExp(String.raw`\b(?:placeholder|fake|dummy|test|sample|demo)(?: (?:user|staff member|person|provider|name))?,?\s+(?:named\s+|called\s+)?${n}\b`, "i");
47214
+ const isnt = new RegExp(String.raw`\b${n}\b\s+(?:isn'?t|is not|wasn'?t|was not)\s+(?:(?:a|an)\s+)?(?:real|actual|genuine)\b`, "i");
47215
+ if (after.test(sentence) || isnt.test(sentence) || before.test(sentence)) return true;
47216
+ }
47217
+ return false;
47218
+ }
47219
+ function mentionPattern(key) {
47220
+ const words2 = customValueKeyOf(key).split("_").map(esc11);
47221
+ return new RegExp(`\\b${words2.join("[\\s_]+")}\\b`, "gi");
47222
+ }
47223
+ function correctClause(clause, status) {
47224
+ const pred = new RegExp(EMPTY_PRED, "i").exec(clause);
47225
+ if (!pred) return { text: clause, changed: false };
47226
+ const head4 = clause.slice(0, pred.index);
47227
+ const keys = [...status.filled, ...status.empty, ...status.unknown ?? []];
47228
+ const found = [];
47229
+ for (const key of keys) for (const m of head4.matchAll(mentionPattern(key))) found.push({ key, start: m.index, end: m.index + m[0].length });
47230
+ if (!found.length) return { text: clause, changed: false };
47231
+ found.sort((a, b) => a.start - b.start);
47232
+ const last = found[found.length - 1];
47233
+ const tail = head4.slice(last.end);
47234
+ if (!/^\s*(?:custom values?\s*)?(?:\([^)]*\)\s*)?$/i.test(tail)) return { text: clause, changed: false };
47235
+ for (let i = 1; i < found.length; i++) if (!/^\s*(?:,|and|,\s*and|&)\s*$/i.test(head4.slice(found[i - 1].end, found[i].start))) return { text: clause, changed: false };
47236
+ const filledNamed = found.filter((f) => status.filled.includes(f.key));
47237
+ if (!filledNamed.length) return { text: clause, changed: false };
47238
+ const stillEmpty = found.filter((f) => status.empty.includes(f.key));
47239
+ if (!stillEmpty.length) return { text: "", changed: true };
47240
+ const listed = listOf(stillEmpty.map((f) => head4.slice(f.start, f.end)));
47241
+ let out = clause.slice(0, found[0].start) + listed + clause.slice(last.end);
47242
+ if (stillEmpty.length === 1 && found.length > 1) {
47243
+ return { text: out.replace(/\bcustom values\b/i, "custom value").replace(/\b(are|were)(?=\s+(?:still\s+)?(?:left\s+)?(?:empty|blank|unfilled|not|missing)\b)/i, (v) => v.toLowerCase() === "are" ? "is" : "was"), changed: true };
47244
+ }
47245
+ return { text: out, changed: true };
47246
+ }
47247
+ function maskAndNames(clause, keys) {
47248
+ const masked = [];
47249
+ let text2 = clause;
47250
+ for (const key of keys.filter((k) => /(?:^|_)and(?:_|$)/.test(customValueKeyOf(k)))) {
47251
+ text2 = text2.replace(mentionPattern(key), (m) => {
47252
+ masked.push(m);
47253
+ return `${masked.length - 1}`;
47254
+ });
47255
+ }
47256
+ return { text: text2, unmask: (t) => t.replace(/\u0001(\d+)\u0001/g, (_m, i) => masked[Number(i)]) };
47257
+ }
47258
+ function splitAndClaims(clause, keys = []) {
47259
+ const { text: text2, unmask } = maskAndNames(clause, keys);
47260
+ return splitMasked(text2).map(unmask);
47261
+ }
47262
+ function claimsWithin(clause, keys) {
47263
+ const { text: text2, unmask } = maskAndNames(clause, keys);
47264
+ const claims = (t) => new RegExp(EMPTY_PRED, "i").test(t);
47265
+ const bits = text2.split(/(,?\s+and\s+)/i);
47266
+ let from = claims(bits[0]) ? 0 : -1;
47267
+ for (let i = 2; from < 0 && i < bits.length; i += 2) if (claims(bits.slice(i).join(""))) from = i;
47268
+ if (from < 0) return [];
47269
+ return splitMasked(bits.slice(from).join("")).map(unmask).filter((_, i) => i % 2 === 0).filter(claims);
47270
+ }
47271
+ function splitMasked(clause) {
47272
+ const bits = clause.split(/(,?\s+and\s+)/i);
47273
+ const out = [bits[0]];
47274
+ const claims = (t) => new RegExp(EMPTY_PRED, "i").test(t);
47275
+ for (let i = 1; i < bits.length; i += 2) {
47276
+ const joiner = bits[i], next = bits[i + 1] ?? "";
47277
+ if (claims(out[out.length - 1]) && claims(next)) out.push(joiner, next);
47278
+ else out[out.length - 1] += joiner + next;
47279
+ }
47280
+ return out;
47281
+ }
47282
+ function correctEmptinessClaim(sentence, status) {
47283
+ if (!status || status.unreadable) return { text: sentence, changed: false };
47284
+ const keys = [...status.filled, ...status.empty, ...status.unknown ?? []];
47285
+ const parts = [];
47286
+ const outer = sentence.split(CLAUSE_SPLIT);
47287
+ for (let i = 0; i < outer.length; i += 2) {
47288
+ parts.push(...splitAndClaims(outer[i], keys));
47289
+ if (i + 1 < outer.length) parts.push(outer[i + 1]);
47290
+ }
47291
+ let changed = false;
47292
+ const kept = [];
47293
+ let dropNextConsequence = false;
47294
+ for (let i = 0; i < parts.length; i += 2) {
47295
+ const joiner = parts[i - 1] ?? "";
47296
+ if (dropNextConsequence && /^\s*,?\s*(?:and so|so)\s+$/i.test(joiner)) {
47297
+ changed = true;
47298
+ for (const piece of claimsWithin(parts[i], keys)) {
47299
+ const c2 = correctClause(piece, status);
47300
+ if (c2.text.trim()) kept.push({ text: c2.text, joiner: ", and " });
47301
+ }
47302
+ continue;
47303
+ }
47304
+ dropNextConsequence = false;
47305
+ const c = correctClause(parts[i], status);
47306
+ if (c.changed) changed = true;
47307
+ if (c.text.trim()) kept.push({ text: c.text, joiner });
47308
+ else dropNextConsequence = true;
47309
+ }
47310
+ if (!changed) return { text: sentence, changed: false };
47311
+ let text2 = kept.map((k, i) => (i ? k.joiner : "") + k.text).join("").trim();
47312
+ if (text2 && !/[.!?]$/.test(text2)) text2 += ".";
47313
+ text2 = text2.charAt(0).toUpperCase() + text2.slice(1);
47314
+ return { text: /^[.!?]?$/.test(text2) ? "" : text2, changed: true };
47315
+ }
47316
+ function guardBuildReport(report3, facts) {
47317
+ const staff = (facts.verifiedStaff ?? []).filter((n) => n.trim());
47318
+ const others = (facts.unverifiedStaff ?? []).filter((n) => n.trim());
47319
+ const fix = (text2) => {
47320
+ let staffClaim = false, changed = false;
47321
+ const parts = pieces(text2).map((p) => {
47322
+ if (staff.length && claimsVerifiedPersonNotReal(p.sentence, staff)) {
47323
+ staffClaim = true;
47324
+ changed = true;
47325
+ return null;
47326
+ }
47327
+ const bullet3 = /^(\s*(?:[-*•]|\d+[.)])\s+)/.exec(p.sentence)?.[1] ?? "";
47328
+ const c = correctEmptinessClaim(p.sentence.slice(bullet3.length), facts.customValues);
47329
+ if (!c.changed) return p;
47330
+ changed = true;
47331
+ return c.text ? { sentence: bullet3 + c.text, sep: p.sep } : null;
47332
+ });
47333
+ return { text: changed ? rejoin(parts) : text2, staffClaim };
47334
+ };
47335
+ const namesOther = (text2) => others.some((n) => new RegExp(`\\b${esc11(n)}\\b`, "i").test(text2) || new RegExp(`\\b${esc11(n.split(/\s+/)[0])}\\b`).test(text2));
47336
+ const out = { ...report3 };
47337
+ if (typeof report3.summary === "string") out.summary = fix(report3.summary).text;
47338
+ if (Array.isArray(report3.issues)) {
47339
+ out.issues = report3.issues.map((x) => ({ original: x, ...fix(x) })).filter((r) => r.text.trim() && !(r.staffClaim && !namesOther(r.original))).map((r) => r.text);
47340
+ }
47341
+ return out;
47342
+ }
47343
+ var esc11, listOf, NOT_REAL_PHRASE, EMPTY_PRED, CLAUSE_SPLIT;
47344
+ var init_build_facts = __esm({
47345
+ "src/build-facts.ts"() {
47346
+ "use strict";
47347
+ esc11 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
47348
+ listOf = (xs) => xs.length <= 1 ? xs.join("") : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
47349
+ NOT_REAL_PHRASE = String.raw`(?:placeholders?|not (?:a |an )?(?:real|actual|genuine)\b|fake|dummy|made[- ]up|stand-?ins?|(?:test|sample|example|demo) (?:users?|accounts?|staff(?: members?)?|persons?|people|providers?|members?|names?|entr(?:y|ies)))`;
47350
+ EMPTY_PRED = String.raw`\b(?:is|are|was|were|remains?|stays?)\s+(?:still\s+)?(?:left\s+)?(?:empty|blank|unfilled|not (?:yet )?(?:been )?(?:filled(?: in)?|set|entered)|missing)\b(?!\s+(?:its|their|the|a|an|from|in the)\b)`;
47351
+ CLAUSE_SPLIT = /(;\s*|,\s*(?:but|while|although|though|so|and so|because|whereas)\s+|\s+but\s+)/i;
47352
+ }
47353
+ });
47354
+
46919
47355
  // src/agency-profile-page.ts
46920
47356
  function agencyProfilePageHtml(p, clients = [], seatToken = "") {
46921
47357
  const offers = (p.offers ?? []).map((o) => `${o.name}${o.priceFloor ? ` - ${o.priceFloor}${o.cadence === "monthly" ? "/mo" : ""}` : ""}`).join("\n");
@@ -46927,8 +47363,8 @@ function agencyProfilePageHtml(p, clients = [], seatToken = "") {
46927
47363
  const staff = (p.staff ?? []).map((m) => [m.name, m.role, m.email].filter(Boolean).join(" \xB7 ")).join("\n");
46928
47364
  const hintOf2 = (id, hint) => hint ? `<div class="hint" id="${id}-hint">${hint}</div>` : "";
46929
47365
  const described = (id, hint) => hint ? ` aria-describedby="${id}-hint"` : "";
46930
- const input = (id, label2, value, o = {}) => `<div class="cds-field"><label for="${id}">${label2}</label>${hintOf2(id, o.hint)}<input class="cds-input" id="${id}" name="${id}"${o.numeric ? ' inputmode="numeric"' : ""} value="${esc11(value)}"${o.placeholder ? ` placeholder="${esc11(o.placeholder)}"` : ""}${described(id, o.hint)}></div>`;
46931
- const area = (id, label2, value, o = {}) => `<div class="cds-field"><label for="${id}">${label2}</label>${hintOf2(id, o.hint)}<textarea class="cds-textarea" id="${id}" name="${id}"${o.rows ? ` rows="${o.rows}"` : ""}${o.placeholder ? ` placeholder="${o.placeholder}"` : ""}${described(id, o.hint)}>${esc11(value)}</textarea></div>`;
47366
+ const input = (id, label2, value, o = {}) => `<div class="cds-field"><label for="${id}">${label2}</label>${hintOf2(id, o.hint)}<input class="cds-input" id="${id}" name="${id}"${o.numeric ? ' inputmode="numeric"' : ""} value="${esc12(value)}"${o.placeholder ? ` placeholder="${esc12(o.placeholder)}"` : ""}${described(id, o.hint)}></div>`;
47367
+ const area = (id, label2, value, o = {}) => `<div class="cds-field"><label for="${id}">${label2}</label>${hintOf2(id, o.hint)}<textarea class="cds-textarea" id="${id}" name="${id}"${o.rows ? ` rows="${o.rows}"` : ""}${o.placeholder ? ` placeholder="${o.placeholder}"` : ""}${described(id, o.hint)}>${esc12(value)}</textarea></div>`;
46932
47368
  const filled = (xs) => xs.filter((x) => Array.isArray(x) ? x.length > 0 : x !== void 0 && x !== null && String(x).trim() !== "").length;
46933
47369
  const progress = (xs) => {
46934
47370
  const n = filled(xs);
@@ -46946,20 +47382,20 @@ function agencyProfilePageHtml(p, clients = [], seatToken = "") {
46946
47382
  const cur = e.currency || p.pricing?.currency || "";
46947
47383
  const recorded = Object.keys(e).filter((k) => k !== "updated_at").length > 0;
46948
47384
  const sel = (k, opts, current) => `<select class="cds-select f" data-k="${k}">${opts.map(([val, text2]) => `<option value="${val}"${(current ?? "") === val ? " selected" : ""}>${text2}</option>`).join("")}</select>`;
46949
- return ` <li><details class="cl cds-details" data-id="${esc11(c.locationId)}"${recorded ? "" : ' data-empty="1"'}>
46950
- <summary class="ap-sum"><span class="ttl cn">${esc11(c.name)}<span class="sub cs">${recorded ? esc11([e.offer, e.price !== void 0 ? `${fmt(e.price, cur)}${e.cadence === "monthly" ? "/mo" : ""}` : "", e.status && e.status !== "active" ? e.status.charAt(0).toUpperCase() + e.status.slice(1) : ""].filter(Boolean).join(" \xB7 ") || "Recorded") : "Nothing recorded yet"}</span></span><span class="ap-end">${under && floor !== void 0 ? `<span class="cds-chip warn">Under your floor of ${esc11(fmt(floor, cur))}</span>` : ""}<span class="cds-link open">Open</span></span></summary>
47385
+ return ` <li><details class="cl cds-details" data-id="${esc12(c.locationId)}"${recorded ? "" : ' data-empty="1"'}>
47386
+ <summary class="ap-sum"><span class="ttl cn">${esc12(c.name)}<span class="sub cs">${recorded ? esc12([e.offer, e.price !== void 0 ? `${fmt(e.price, cur)}${e.cadence === "monthly" ? "/mo" : ""}` : "", e.status && e.status !== "active" ? e.status.charAt(0).toUpperCase() + e.status.slice(1) : ""].filter(Boolean).join(" \xB7 ") || "Recorded") : "Nothing recorded yet"}</span></span><span class="ap-end">${under && floor !== void 0 ? `<span class="cds-chip warn">Under your floor of ${esc12(fmt(floor, cur))}</span>` : ""}<span class="cds-link open">Open</span></span></summary>
46951
47387
  <div class="ap-body">
46952
47388
  <div class="cds-grid">
46953
47389
  <div class="cds-field"><label>What this is</label>${sel("kind", [["client", "A client whose GoHighLevel account you run"], ["software", "Software they bought from you"], ["other", "Other income: event fee, revenue share, training"]], e.kind ?? "client")}</div>
46954
- <div class="cds-field"><label>Offer</label><input class="cds-input f" data-k="offer" value="${esc11(e.offer)}" placeholder="${esc11(p.offers?.[0]?.name || "Speed to Lead")}"></div>
46955
- <div class="cds-field"><label>They pay</label><input class="cds-input f" data-k="price" inputmode="numeric" value="${esc11(e.price)}"></div>
47390
+ <div class="cds-field"><label>Offer</label><input class="cds-input f" data-k="offer" value="${esc12(e.offer)}" placeholder="${esc12(p.offers?.[0]?.name || "Speed to Lead")}"></div>
47391
+ <div class="cds-field"><label>They pay</label><input class="cds-input f" data-k="price" inputmode="numeric" value="${esc12(e.price)}"></div>
46956
47392
  <div class="cds-field"><label>How often</label>${sel("cadence", [["", "Not set"], ["monthly", "Monthly"], ["one-time", "One-time"]], e.cadence)}</div>
46957
- <div class="cds-field"><label>Started</label><input class="cds-input f" data-k="startedOn" placeholder="2026-01-15" value="${esc11(e.startedOn)}"></div>
46958
- <div class="cds-field"><label>Renews</label><input class="cds-input f" data-k="renewsOn" placeholder="2027-01-15" value="${esc11(e.renewsOn)}"></div>
47393
+ <div class="cds-field"><label>Started</label><input class="cds-input f" data-k="startedOn" placeholder="2026-01-15" value="${esc12(e.startedOn)}"></div>
47394
+ <div class="cds-field"><label>Renews</label><input class="cds-input f" data-k="renewsOn" placeholder="2027-01-15" value="${esc12(e.renewsOn)}"></div>
46959
47395
  <div class="cds-field"><label>Status</label>${sel("status", [["", "Not set"], ["active", "Active"], ["paused", "Paused"], ["ended", "Ended"]], e.status)}</div>
46960
- <div class="cds-field"><label>Who runs it</label><input class="cds-input f" data-k="owner" value="${esc11(e.owner)}" placeholder="${esc11(p.staff?.[0]?.name || "")}"></div>
47396
+ <div class="cds-field"><label>Who runs it</label><input class="cds-input f" data-k="owner" value="${esc12(e.owner)}" placeholder="${esc12(p.staff?.[0]?.name || "")}"></div>
46961
47397
  </div>
46962
- <div class="cds-field"><label>Anything specific to this client: one per line</label><textarea class="cds-textarea f" data-k="sops" placeholder="Call the owner before any send">${esc11((e.sops ?? []).join("\n"))}</textarea></div>
47398
+ <div class="cds-field"><label>Anything specific to this client: one per line</label><textarea class="cds-textarea f" data-k="sops" placeholder="Call the owner before any send">${esc12((e.sops ?? []).join("\n"))}</textarea></div>
46963
47399
  <div class="cds-row"><button type="button" class="cds-btn ghost save-cl">Save this client</button><span class="cmsg" role="status"></span></div>
46964
47400
  </div>
46965
47401
  </details></li>`;
@@ -47015,7 +47451,7 @@ ${p.agencyName ? "" : ` <div class="cds-callout warn"><span class="ttl">Add an a
47015
47451
  ${input("legalName", "Registered entity, if different", p.legalName, { placeholder: "Your Agency LLC, used on contracts and A2P" })}
47016
47452
  <div class="cds-grid">
47017
47453
  ${input("website", "Website", p.website, { placeholder: "https://youragency.com" })}
47018
- <div class="cds-field"><label for="accent">Brand colour</label><input type="color" class="cds-input" id="accent" name="accent" value="${esc11(p.accent || "#b8934a")}"></div>
47454
+ <div class="cds-field"><label for="accent">Brand colour</label><input type="color" class="cds-input" id="accent" name="accent" value="${esc12(p.accent || "#b8934a")}"></div>
47019
47455
  </div>
47020
47456
  ${input("logoUrl", "Logo URL", p.logoUrl, { placeholder: "https://youragency.com/logo.svg", hint: "Must start with https. A logo link that isn't https is refused, because this one goes on pages your prospects open." })}
47021
47457
  ${input("logoFile", "Logo file on this computer (for client reports)", p.logoFile, { placeholder: "/Users/you/Documents/logo.png", hint: "Optional. The full path to a PNG, JPEG, WebP, GIF or SVG under 256 KB. Reports you hand clients carry it inside the page, so it prints and travels with a saved copy." })}
@@ -47060,7 +47496,7 @@ ${p.agencyName ? "" : ` <div class="cds-callout warn"><span class="ttl">Add an a
47060
47496
  <div class="cds-panel-head"><div class="txt"><div class="cds-panel-label">What you deliver</div><h2 class="cds-panel-title">Services you sell</h2></div></div>
47061
47497
  <p>Tick what you actually deliver. A report you run on a prospect's website offers a fix only under a service ticked here; anything else shows as a plain finding, so a prospect is never offered something you do not do.</p>
47062
47498
  <div class="cds-grid two">
47063
- ${SERVICE_IDS.map((id) => ` <label class="cds-check"><input type="checkbox" name="svc" value="${id}"${sold.offered.includes(id) ? " checked" : ""}><span>${esc11(SERVICES[id].label)}</span></label>`).join("\n")}
47499
+ ${SERVICE_IDS.map((id) => ` <label class="cds-check"><input type="checkbox" name="svc" value="${id}"${sold.offered.includes(id) ? " checked" : ""}><span>${esc12(SERVICES[id].label)}</span></label>`).join("\n")}
47064
47500
  </div>
47065
47501
  ${area("svcCustom", "Your own services: one per line", sold.custom.map((c) => c.name).join("\n"), { rows: Math.max(3, sold.custom.length + 1), placeholder: "Local Lift package", hint: "Saved as yours. To have one pitched on a report, tell Claude which service above it covers, for example: my Local Lift package covers local search." })}
47066
47502
  </section>
@@ -47143,8 +47579,8 @@ ${SERVICE_IDS.map((id) => ` <label class="cds-check"><input type="checkbox" n
47143
47579
  <details class="cds-details"><summary>Details</summary>
47144
47580
  <div class="cds-stack">
47145
47581
  <p>Install or update Claude Code on this computer with the Claude Code setup instructions. It needs version 2.1.0 or newer.</p>
47146
- <p>${esc11(claudeCodeAddHelp())}</p>
47147
- <p>On GoHighLevel's Agency Pro plan, connect your agency Private Integration key in GHL Command so it can create sub-accounts and staff users. Create that key in GoHighLevel's agency view (not inside a sub-account): ${esc11(AGENCY_PIT_CLICK_PATH)}. GoHighLevel allows both only on Agency Pro; on other plans, create the sub-account and add its staff in GoHighLevel yourself.</p>
47582
+ <p>${esc12(claudeCodeAddHelp())}</p>
47583
+ <p>On GoHighLevel's Agency Pro plan, connect your agency Private Integration key in GHL Command so it can create sub-accounts and staff users. Create that key in GoHighLevel's agency view (not inside a sub-account): ${esc12(AGENCY_PIT_CLICK_PATH)}. GoHighLevel allows both only on Agency Pro; on other plans, create the sub-account and add its staff in GoHighLevel yourself.</p>
47148
47584
  <p>If a client\u2019s workflow builder connection fails, say to Claude: \u201CUnlock the Workflow Builder.\u201D Log into GoHighLevel in the Chrome window that opens, then quit Claude fully and open it again.</p>
47149
47585
  <p>Enable the intake and workflow tools in your GHL Command settings.</p>
47150
47586
  <p>Use Check and save this key, in the Claude key panel, to verify Claude authentication.</p>
@@ -47285,7 +47721,7 @@ document.querySelectorAll(".save-cl").forEach(btn=>btn.addEventListener("click",
47285
47721
  </script>
47286
47722
  <script>${COCKPIT_TYPE_JS}</script></body></html>`;
47287
47723
  }
47288
- var READ_NUMBER_JS, fmt, esc11;
47724
+ var READ_NUMBER_JS, fmt, esc12;
47289
47725
  var init_agency_profile_page = __esm({
47290
47726
  "src/agency-profile-page.ts"() {
47291
47727
  "use strict";
@@ -47305,7 +47741,7 @@ var init_agency_profile_page = __esm({
47305
47741
  return Number.isFinite(n)?{value:n}:{bad:true};
47306
47742
  }`;
47307
47743
  fmt = (n, currency) => `${currency ? currency + " " : ""}${n.toLocaleString("en-US")}`;
47308
- esc11 = (x) => String(x ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
47744
+ esc12 = (x) => String(x ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
47309
47745
  }
47310
47746
  });
47311
47747
 
@@ -49671,24 +50107,24 @@ function storedDay(iso2, now = /* @__PURE__ */ new Date()) {
49671
50107
  function storedCard(r) {
49672
50108
  const st = brandState(r);
49673
50109
  if (r.problem) {
49674
- return `<article class="cds-card bp-card"><div class="cds-row"><h3 class="cds-panel-title">${esc12(r.slug)}</h3><span class="cds-chip ${st.tone}">${st.word}</span></div>
50110
+ return `<article class="cds-card bp-card"><div class="cds-row"><h3 class="cds-panel-title">${esc13(r.slug)}</h3><span class="cds-chip ${st.tone}">${st.word}</span></div>
49675
50111
  <p>This brand's file could not be opened. Pull it again from the website, or ask for help with the detail below.</p>
49676
- <details class="cds-details"><summary>Details</summary><p>${esc12(r.problem)}</p></details></article>`;
50112
+ <details class="cds-details"><summary>Details</summary><p>${esc13(r.problem)}</p></details></article>`;
49677
50113
  }
49678
50114
  const href = `/brand/${encodeURIComponent(r.slug)}`;
49679
50115
  const f = fontWords(r.display, r.body);
49680
50116
  return `<article class="cds-card bp-card">
49681
- <div class="cds-row"><h3 class="cds-panel-title">${esc12(r.name)}</h3><span class="cds-chip ${st.tone}">${st.word}</span>${f.broken ? `<span class="cds-chip warn">Check the fonts</span>` : ""}</div>
50117
+ <div class="cds-row"><h3 class="cds-panel-title">${esc13(r.name)}</h3><span class="cds-chip ${st.tone}">${st.word}</span>${f.broken ? `<span class="cds-chip warn">Check the fonts</span>` : ""}</div>
49682
50118
  ${r.accent ? `<div class="bp-sw">${swatch(r.accent, "Accent")}</div>` : ""}
49683
- <p>${esc12(f.text)}</p>
49684
- <div class="cds-src">${esc12(brandSource(r.extractedVia))}</div>
50119
+ <p>${esc13(f.text)}</p>
50120
+ <div class="cds-src">${esc13(brandSource(r.extractedVia))}</div>
49685
50121
  <div class="cds-row"><a class="cds-btn ghost" href="${href}">Open the brand</a></div>
49686
- <details class="cds-details"><summary>Details</summary><p>Confidence ${Math.round(r.confidence * 100)}%, read by ${esc12(r.extractedVia)}, ${esc12(r.scheme)} background${r.extractedAt ? `, stored ${esc12(r.extractedAt.slice(0, 10))}` : ""}.</p>${f.broken ? brokenFontsNote(r.display, r.body) : ""}</details>
50122
+ <details class="cds-details"><summary>Details</summary><p>Confidence ${Math.round(r.confidence * 100)}%, read by ${esc13(r.extractedVia)}, ${esc13(r.scheme)} background${r.extractedAt ? `, stored ${esc13(r.extractedAt.slice(0, 10))}` : ""}.</p>${f.broken ? brokenFontsNote(r.display, r.body) : ""}</details>
49687
50123
  </article>`;
49688
50124
  }
49689
50125
  function houseRow(d) {
49690
50126
  const sub = [d.voice.tone, fontWords(d.typography.display.family, d.typography.body.family).text].filter(Boolean).join(". ");
49691
- return `<li><span class="ttl">${esc12(d.name)}<span class="sub">${esc12(sub)}</span></span><span class="end"><span class="bp-sw">${swatch(d.colors.bg.hex, "Background")}${swatch(d.colors.fg.hex, "Text")}${swatch(d.colors.accent.hex, "Accent")}${swatch(d.colors.surface.hex, "Surface")}</span><button type="button" class="cds-btn ghost" data-house="${esc12(d.slug)}">Use this look</button></span></li>`;
50127
+ return `<li><span class="ttl">${esc13(d.name)}<span class="sub">${esc13(sub)}</span></span><span class="end"><span class="bp-sw">${swatch(d.colors.bg.hex, "Background")}${swatch(d.colors.fg.hex, "Text")}${swatch(d.colors.accent.hex, "Accent")}${swatch(d.colors.surface.hex, "Surface")}</span><button type="button" class="cds-btn ghost" data-house="${esc13(d.slug)}">Use this look</button></span></li>`;
49692
50128
  }
49693
50129
  function brandsPageHtml(rows2, house, refs2, seatToken = "") {
49694
50130
  const first2 = rows2.slice(0, SHOW_FIRST), rest = rows2.slice(SHOW_FIRST);
@@ -49723,7 +50159,7 @@ ${refs2.length ? `<details id="refs-more" class="cds-more"><summary>More looks f
49723
50159
  <div class="cds-stack">
49724
50160
  <p>Colours, type and shape only, fetched when you pick one. No logo comes with it, and the pages you make never name the brand it came from.</p>
49725
50161
  <div class="cds-field bp-form"><label for="ref-filter">Find a look</label><input class="cds-input" id="ref-filter" type="search" autocomplete="off" placeholder="Type a name"><div class="hint" id="ref-count" aria-live="polite"></div></div>
49726
- <div class="bp-refs" id="refs">${refs2.map((r) => `<button type="button" class="cds-chip" data-look="${esc12(r.slug)}" title="${esc12(r.website)}">${esc12(r.name)}</button>`).join("")}</div>
50162
+ <div class="bp-refs" id="refs">${refs2.map((r) => `<button type="button" class="cds-chip" data-look="${esc13(r.slug)}" title="${esc13(r.website)}">${esc13(r.name)}</button>`).join("")}</div>
49727
50163
  </div>
49728
50164
  </details>` : ""}
49729
50165
  <div class="cds-field"><p class="hint" id="look-status" role="status" aria-live="polite"></p></div>
@@ -49762,20 +50198,20 @@ function brandCardHtml(d, opts = {}) {
49762
50198
  };
49763
50199
  const fw = composeFonts(faceWords(disp), faceWords(bod), !disp.family?.trim() && !bod.family?.trim());
49764
50200
  const v = brandVerdict(brandState({ confidence: d.provenance.confidence }), fw.broken);
49765
- const named = (hex, label2) => `<div class="bp-swatch"><span data-sw="${esc12(hex)}" role="img" aria-label="${esc12(label2)} colour"></span><span class="cds-src">${esc12(label2)}</span></div>`;
49766
- const sw = (label2, t) => `<tr><td><span class="bp-sw">${swatch(t.hex, label2)}</span></td><th scope="row">${esc12(label2)}</th><td class="cds-num">${esc12(t.hex)}</td><td><small>${esc12(t.source)}</small></td></tr>`;
49767
- const face = (label2, f) => `<tr><th scope="row">${esc12(label2)}</th><td><b>${esc12(f.family)}</b></td><td>${f.renderIntent === "google" ? `<span class="cds-chip ok">Google Fonts</span>` : `<span class="cds-chip warn">${f.renderIntent === "licensed-webfont" ? "licensed" : "system"}</span> renders as <b>${esc12(renderedFamily(f))}</b>`}</td><td><small>${esc12(f.source)}</small></td></tr>`;
49768
- const pairs = d.contrast.pairs.map((p) => `<tr><td class="cds-num">${esc12(p.fg)} on ${esc12(p.bg)}</td><td class="cds-num">${p.ratio}:1 ${p.ratio >= 4.5 ? `<span class="cds-chip ok">AA</span>` : `<span class="cds-chip warn">under 4.5</span>`}</td><td>${p.correctedFg ? `<small>text shade corrected to ${esc12(p.correctedFg)} (${p.correctedRatio}:1)</small>` : ""}</td></tr>`).join("");
49769
- const samples = d.voice.samples.map((s) => `<li><span class="ttl">${esc12(s)}</span></li>`);
50201
+ const named = (hex, label2) => `<div class="bp-swatch"><span data-sw="${esc13(hex)}" role="img" aria-label="${esc13(label2)} colour"></span><span class="cds-src">${esc13(label2)}</span></div>`;
50202
+ const sw = (label2, t) => `<tr><td><span class="bp-sw">${swatch(t.hex, label2)}</span></td><th scope="row">${esc13(label2)}</th><td class="cds-num">${esc13(t.hex)}</td><td><small>${esc13(t.source)}</small></td></tr>`;
50203
+ const face = (label2, f) => `<tr><th scope="row">${esc13(label2)}</th><td><b>${esc13(f.family)}</b></td><td>${f.renderIntent === "google" ? `<span class="cds-chip ok">Google Fonts</span>` : `<span class="cds-chip warn">${f.renderIntent === "licensed-webfont" ? "licensed" : "system"}</span> renders as <b>${esc13(renderedFamily(f))}</b>`}</td><td><small>${esc13(f.source)}</small></td></tr>`;
50204
+ const pairs = d.contrast.pairs.map((p) => `<tr><td class="cds-num">${esc13(p.fg)} on ${esc13(p.bg)}</td><td class="cds-num">${p.ratio}:1 ${p.ratio >= 4.5 ? `<span class="cds-chip ok">AA</span>` : `<span class="cds-chip warn">under 4.5</span>`}</td><td>${p.correctedFg ? `<small>text shade corrected to ${esc13(p.correctedFg)} (${p.correctedRatio}:1)</small>` : ""}</td></tr>`).join("");
50205
+ const samples = d.voice.samples.map((s) => `<li><span class="ttl">${esc13(s)}</span></li>`);
49770
50206
  const headlines = samples.length ? `<ul class="cds-list">${samples.slice(0, SHOW_FIRST).join("")}</ul>${samples.length > SHOW_FIRST ? `<details class="cds-more"><summary>Show all ${samples.length} headlines</summary><ul class="cds-list">${samples.slice(SHOW_FIRST).join("")}</ul></details>` : ""}` : "";
49771
50207
  const cleanSvg = d.logo?.inlineSvg ? sanitizeSvgOrDrop(d.logo.inlineSvg) : void 0;
49772
- const logo = cleanSvg ? `<img src="data:image/svg+xml;base64,${Buffer.from(cleanSvg.replace(/currentColor/g, c.fg.hex)).toString("base64")}" alt="${esc12(d.name)} logo">` : d.logo?.inlineSvg ? `<span>The stored logo is not a plain drawing, so it is not shown</span>` : isRasterDataUri(d.logo?.dataUri) ? `<img src="${esc12(d.logo.dataUri)}" alt="${esc12(d.name)} logo">` : d.logo?.dataUri ? `<span>The stored logo image is not a plain picture, so it is not shown</span>` : `<span>No logo yet. Add one under Change something.</span>`;
50208
+ const logo = cleanSvg ? `<img src="data:image/svg+xml;base64,${Buffer.from(cleanSvg.replace(/currentColor/g, c.fg.hex)).toString("base64")}" alt="${esc13(d.name)} logo">` : d.logo?.inlineSvg ? `<span>The stored logo is not a plain drawing, so it is not shown</span>` : isRasterDataUri(d.logo?.dataUri) ? `<img src="${esc13(d.logo.dataUri)}" alt="${esc13(d.name)} logo">` : d.logo?.dataUri ? `<span>The stored logo image is not a plain picture, so it is not shown</span>` : `<span>No logo yet. Add one under Change something.</span>`;
49773
50209
  const day = storedDay(d.provenance.extractedAt);
49774
- const sub = [d.website ? esc12(d.website) : "", esc12(brandSource(d.provenance.extractedVia)), day ? `stored ${esc12(day)}` : ""].filter(Boolean).join(" \xB7 ");
50210
+ const sub = [d.website ? esc13(d.website) : "", esc13(brandSource(d.provenance.extractedVia)), day ? `stored ${esc13(day)}` : ""].filter(Boolean).join(" \xB7 ");
49775
50211
  const table = (cols, body) => `<div class="cds-table-wrap"><table class="cds-table"><thead><tr>${cols.map((h) => `<th scope="col">${h}</th>`).join("")}</tr></thead><tbody>${body}</tbody></table></div>`;
49776
50212
  const details = `<details class="cds-details" id="brand-details"><summary>Details</summary><div class="cds-stack">
49777
- <p>Confidence ${Math.round(d.provenance.confidence * 100)}%, read by ${esc12(d.provenance.extractedVia)}, ${esc12(d.scheme)} scheme, ${esc12(d.provenance.extractedAt.slice(0, 10))}.</p>
49778
- ${d.pageStudioTheme.warnings.length ? `<h3>Before this reaches a client</h3><ul>${d.pageStudioTheme.warnings.map((w) => `<li>${esc12(w)}</li>`).join("")}</ul>` : ""}
50213
+ <p>Confidence ${Math.round(d.provenance.confidence * 100)}%, read by ${esc13(d.provenance.extractedVia)}, ${esc13(d.scheme)} scheme, ${esc13(d.provenance.extractedAt.slice(0, 10))}.</p>
50214
+ ${d.pageStudioTheme.warnings.length ? `<h3>Before this reaches a client</h3><ul>${d.pageStudioTheme.warnings.map((w) => `<li>${esc13(w)}</li>`).join("")}</ul>` : ""}
49779
50215
  ${fw.broken ? brokenFontsNote(disp.family, bod.family) : ""}
49780
50216
  <h3>Colours</h3>
49781
50217
  ${table(['<span class="cds-sr">Swatch</span>', "Role", "Hex", "Where it came from"], `${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)}`)}
@@ -49784,34 +50220,34 @@ ${table(["Pair", "Ratio", '<span class="cds-sr">Correction</span>'], pairs)}
49784
50220
  <h3>Fonts</h3>
49785
50221
  ${table(["Use", "Face", "Renders as", "Source"], `${face("Display", disp)}${face("Body", bod)}`)}
49786
50222
  <h3>Logo</h3>
49787
- <p>${esc12(d.provenance.evidence["logo"] ?? "No note on where the logo came from.")}${d.logo?.whiteFillFixed ? " \xB7 white fills replaced with currentColor" : ""}</p>
50223
+ <p>${esc13(d.provenance.evidence["logo"] ?? "No note on where the logo came from.")}${d.logo?.whiteFillFixed ? " \xB7 white fills replaced with currentColor" : ""}</p>
49788
50224
  <h3>Shape and voice</h3>
49789
- <p>Radius ${d.shape.radius.sm} / ${d.shape.radius.md} / ${d.shape.radius.lg} px \xB7 shadows ${esc12(d.shape.shadows)} \xB7 ${d.shape.gridBase}-point grid</p>
49790
- ${d.voice.tone ? `<p>Tone: ${esc12(d.voice.tone)}${d.voice.audience ? ` \xB7 Audience: ${esc12(d.voice.audience)}` : ""}</p>` : ""}
50225
+ <p>Radius ${d.shape.radius.sm} / ${d.shape.radius.md} / ${d.shape.radius.lg} px \xB7 shadows ${esc13(d.shape.shadows)} \xB7 ${d.shape.gridBase}-point grid</p>
50226
+ ${d.voice.tone ? `<p>Tone: ${esc13(d.voice.tone)}${d.voice.audience ? ` \xB7 Audience: ${esc13(d.voice.audience)}` : ""}</p>` : ""}
49791
50227
  ${headlines}
49792
50228
  <h3>For Page Studio</h3>
49793
50229
  <p>The six-slot theme compose_page and compose_website accept. A re-skin of the donor template, not a port of the design system.</p>
49794
- <pre class="bp-pre">${esc12(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>
50230
+ <pre class="bp-pre">${esc13(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>
49795
50231
  <h3>Where every value came from</h3>
49796
- ${table(["Value", "Source"], Object.entries(d.provenance.evidence).map(([k, val]) => `<tr><th scope="row">${esc12(k)}</th><td><small>${esc12(val)}</small></td></tr>`).join(""))}
49797
- ${d.provenance.source ? `<p>Source: ${esc12(d.provenance.source.url)}${d.provenance.source.commit ? ` @ ${esc12(d.provenance.source.commit.slice(0, 7))}` : ""}${d.provenance.source.license ? ` \xB7 ${esc12(d.provenance.source.license)}` : ""}</p>` : ""}
49798
- ${opts.dir ? `<p>Stored at ${esc12(opts.dir)}</p>` : ""}
50232
+ ${table(["Value", "Source"], Object.entries(d.provenance.evidence).map(([k, val]) => `<tr><th scope="row">${esc13(k)}</th><td><small>${esc13(val)}</small></td></tr>`).join(""))}
50233
+ ${d.provenance.source ? `<p>Source: ${esc13(d.provenance.source.url)}${d.provenance.source.commit ? ` @ ${esc13(d.provenance.source.commit.slice(0, 7))}` : ""}${d.provenance.source.license ? ` \xB7 ${esc13(d.provenance.source.license)}` : ""}</p>` : ""}
50234
+ ${opts.dir ? `<p>Stored at ${esc13(opts.dir)}</p>` : ""}
49799
50235
  </div></details>`;
49800
50236
  return `${head2(`${d.name} \xB7 brand`)}
49801
- <div class="cds-page-head"><div class="txt"><div class="cds-kicker">Brand</div><h1 class="cds-page-title">Check the ${esc12(d.name)} brand, then make something in it</h1><p class="cds-page-sub">${sub}</p></div><a class="cds-btn ghost" href="/brands">All brands</a></div>
49802
- <section class="cds-panel bp-glance" id="glance" aria-labelledby="glance-title"><div class="cds-panel-head"><div class="txt"><div class="cds-panel-label">At a glance</div><h2 class="cds-panel-title" id="glance-title">How ${esc12(d.name)} looks</h2></div></div>
49803
- <div class="cds-row" id="verdict"><span class="cds-chip ${v.tone}">${v.word}</span><p>${esc12(v.say)}</p></div>
50237
+ <div class="cds-page-head"><div class="txt"><div class="cds-kicker">Brand</div><h1 class="cds-page-title">Check the ${esc13(d.name)} brand, then make something in it</h1><p class="cds-page-sub">${sub}</p></div><a class="cds-btn ghost" href="/brands">All brands</a></div>
50238
+ <section class="cds-panel bp-glance" id="glance" aria-labelledby="glance-title"><div class="cds-panel-head"><div class="txt"><div class="cds-panel-label">At a glance</div><h2 class="cds-panel-title" id="glance-title">How ${esc13(d.name)} looks</h2></div></div>
50239
+ <div class="cds-row" id="verdict"><span class="cds-chip ${v.tone}">${v.word}</span><p>${esc13(v.say)}</p></div>
49804
50240
  <dl class="cds-kv">
49805
50241
  <dt>Colours</dt><dd><div class="bp-swatches">${named(c.accent.hex, "Accent")}${named(c.fg.hex, "Text")}${named(c.bg.hex, "Background")}${named(c.surface.hex, "Cards")}</div></dd>
49806
- <dt>Fonts</dt><dd>${esc12(fw.text)}</dd>
49807
- <dt>Logo</dt><dd><div class="bp-logo" data-ink="${esc12(c.fg.hex)}" data-sw="${esc12(c.bg.hex)}">${logo}</div></dd>
50242
+ <dt>Fonts</dt><dd>${esc13(fw.text)}</dd>
50243
+ <dt>Logo</dt><dd><div class="bp-logo" data-ink="${esc13(c.fg.hex)}" data-sw="${esc13(c.bg.hex)}">${logo}</div></dd>
49808
50244
  </dl>
49809
- ${opts.screenshot ? `<details class="cds-more" id="their-site"><summary>Show their website as we saw it</summary><img class="bp-shot" src="/brand/${encodeURIComponent(d.slug)}/screenshot.png" alt="Screenshot of ${esc12(d.name)}'s website"></details>` : ""}
50245
+ ${opts.screenshot ? `<details class="cds-more" id="their-site"><summary>Show their website as we saw it</summary><img class="bp-shot" src="/brand/${encodeURIComponent(d.slug)}/screenshot.png" alt="Screenshot of ${esc13(d.name)}'s website"></details>` : ""}
49810
50246
  ${details}
49811
50247
  </section>
49812
50248
  <div class="cds-grid two">
49813
50249
  ${panel2("Make", "Make something in this brand", `<p>Runs on <b>your own Claude</b>, on this computer, never our key.</p>
49814
- <details class="cds-details" id="make-details"><summary>Details</summary><p>${esc12(RUN_EXPECTATION)} The run keeps going if you close this tab. Write the headline and the points you want made, not instructions.</p></details>
50250
+ <details class="cds-details" id="make-details"><summary>Details</summary><p>${esc13(RUN_EXPECTATION)} The run keeps going if you close this tab. Write the headline and the points you want made, not instructions.</p></details>
49815
50251
  <form id="make" class="cds-stack" novalidate><div class="cds-grid">
49816
50252
  <div class="cds-field"><label for="mk-medium">What</label><select class="cds-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>
49817
50253
  <div class="cds-field"><label for="mk-slides">Slides (a deck only)</label><input class="cds-input" id="mk-slides" type="number" min="${DESIGN_FORM_LIMITS.slidesMin}" max="${DESIGN_FORM_LIMITS.slidesMax}" step="1" value="6"></div>
@@ -49827,10 +50263,10 @@ ${panel2("Make", "Make something in this brand", `<p>Runs on <b>your own Claude<
49827
50263
  <div class="cds-field"><p class="hint" id="mk-status" role="status" aria-live="polite"></p></div><pre id="mk-log" class="bp-pre" hidden></pre></form>`, "make-panel")}
49828
50264
  ${panel2("Change", "Change something", `<p>Fill in only what you want to change. An empty field keeps what is there.</p>
49829
50265
  <form id="override" class="cds-stack">
49830
- <div class="cds-field"><label for="accent">Accent colour</label><div class="cds-row bp-accent"><input class="cds-input" id="accent" name="accent" type="text" placeholder="${esc12(c.accent.hex)}" autocomplete="off"><input class="cds-input bp-pick" id="accent-pick" type="color" value="${esc12(pickerHex(c.accent.hex))}" aria-label="Pick the accent colour"></div></div>
49831
- <div class="cds-field"><label for="displayFont">Heading font</label><input class="cds-input" id="displayFont" type="text" placeholder="${esc12(plausibleFontName(disp.family) ? disp.family : "Type the font name")}" autocomplete="off"></div>
49832
- <div class="cds-field"><label for="bodyFont">Text font</label><input class="cds-input" id="bodyFont" type="text" placeholder="${esc12(plausibleFontName(bod.family) ? bod.family : "Type the font name")}" autocomplete="off"></div>
49833
- <div class="cds-field"><label for="brandName">Brand name</label><input class="cds-input" id="brandName" type="text" placeholder="${esc12(d.name)}" autocomplete="off"></div>
50266
+ <div class="cds-field"><label for="accent">Accent colour</label><div class="cds-row bp-accent"><input class="cds-input" id="accent" name="accent" type="text" placeholder="${esc13(c.accent.hex)}" autocomplete="off"><input class="cds-input bp-pick" id="accent-pick" type="color" value="${esc13(pickerHex(c.accent.hex))}" aria-label="Pick the accent colour"></div></div>
50267
+ <div class="cds-field"><label for="displayFont">Heading font</label><input class="cds-input" id="displayFont" type="text" placeholder="${esc13(plausibleFontName(disp.family) ? disp.family : "Type the font name")}" autocomplete="off"></div>
50268
+ <div class="cds-field"><label for="bodyFont">Text font</label><input class="cds-input" id="bodyFont" type="text" placeholder="${esc13(plausibleFontName(bod.family) ? bod.family : "Type the font name")}" autocomplete="off"></div>
50269
+ <div class="cds-field"><label for="brandName">Brand name</label><input class="cds-input" id="brandName" type="text" placeholder="${esc13(d.name)}" autocomplete="off"></div>
49834
50270
  <div class="cds-field"><label for="logoSvg">Logo</label><div class="hint" id="logoSvg-hint">Paste the client's logo as SVG code. Their designer can send it.</div><input class="cds-input" id="logoSvg" type="text" placeholder="&lt;svg \u2026&gt;\u2026&lt;/svg&gt;" autocomplete="off" aria-describedby="logoSvg-hint"></div>
49835
50271
  <div class="cds-row"><button type="submit" class="cds-btn ghost" id="save">Save changes</button></div><div class="cds-field"><p class="hint" id="save-status" role="status" aria-live="polite"></p></div></form>`, "change-panel")}
49836
50272
  </div>
@@ -49897,7 +50333,7 @@ t.addEventListener("input",()=>{const v=t.value.trim();if(/^#[0-9a-f]{6}$/i.test
49897
50333
  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}});
49898
50334
  `)}`;
49899
50335
  }
49900
- var esc12, STYLE, SHELL_JS, foot, SENTENCE_WORDS, pickerHex, swatch, brokenFontsNote, SHOW_FIRST, panel2;
50336
+ var esc13, STYLE, SHELL_JS, foot, SENTENCE_WORDS, pickerHex, swatch, brokenFontsNote, SHOW_FIRST, panel2;
49901
50337
  var init_brand_page = __esm({
49902
50338
  "src/command-os/design/brand-page.ts"() {
49903
50339
  "use strict";
@@ -49907,7 +50343,7 @@ var init_brand_page = __esm({
49907
50343
  init_cockpit_shell();
49908
50344
  init_script_json();
49909
50345
  init_svg_sanitize();
49910
- esc12 = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
50346
+ esc13 = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
49911
50347
  STYLE = `
49912
50348
  [hidden]{display:none!important}
49913
50349
  .bp-cards{display:grid;gap:var(--space-4);grid-template-columns:repeat(auto-fill,minmax(240px,1fr))}
@@ -49949,8 +50385,8 @@ for(const s of document.querySelectorAll("[data-ink]")){const v=s.dataset.ink;if
49949
50385
  if (/^#[0-9a-f]{8}$/i.test(h)) return h.slice(0, 7).toLowerCase();
49950
50386
  return "#000000";
49951
50387
  };
49952
- swatch = (hex, label2) => `<span data-sw="${esc12(hex)}" title="${esc12(label2)} ${esc12(hex)}" role="img" aria-label="${esc12(label2)} ${esc12(hex)}"></span>`;
49953
- brokenFontsNote = (display, body) => `<p>Fonts as read from the site: headings "${esc12(display)}", text "${esc12(body)}".</p>`;
50388
+ swatch = (hex, label2) => `<span data-sw="${esc13(hex)}" title="${esc13(label2)} ${esc13(hex)}" role="img" aria-label="${esc13(label2)} ${esc13(hex)}"></span>`;
50389
+ brokenFontsNote = (display, body) => `<p>Fonts as read from the site: headings "${esc13(display)}", text "${esc13(body)}".</p>`;
49954
50390
  SHOW_FIRST = 8;
49955
50391
  panel2 = (label2, title2, body, id = "") => `<section class="cds-panel"${id ? ` id="${id}" aria-labelledby="${id}-title"` : ""}><div class="cds-panel-head"><div class="txt"><div class="cds-panel-label">${label2}</div><h2 class="cds-panel-title"${id ? ` id="${id}-title"` : ""}>${title2}</h2></div></div>${body}</section>`;
49956
50392
  }
@@ -50160,9 +50596,9 @@ async function generateReview(spawnClaude, input) {
50160
50596
  function reviewDocumentHtml(review, opts) {
50161
50597
  const accent = opts.accent && /^#[0-9a-fA-F]{6}$/.test(opts.accent) ? opts.accent : "#B8934A";
50162
50598
  const agency = opts.agencyName?.trim() || "";
50163
- const esc16 = (v) => String(v ?? "").replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
50599
+ const esc17 = (v) => String(v ?? "").replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
50164
50600
  const date = (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, { year: "numeric", month: "long", day: "numeric" });
50165
- const gap = (g) => `<div class="gap ${g.priority}"><div class="gt">${esc16(g.title)}<span class="pri">${g.priority === "now" ? "Fix first" : g.priority === "soon" ? "Next" : "Later"}</span></div>${g.cost ? `<div class="gc">${esc16(g.cost)}</div>` : ""}${g.fix ? `<div class="gf">What we do: ${esc16(g.fix)}</div>` : ""}</div>`;
50601
+ const gap = (g) => `<div class="gap ${g.priority}"><div class="gt">${esc17(g.title)}<span class="pri">${g.priority === "now" ? "Fix first" : g.priority === "soon" ? "Next" : "Later"}</span></div>${g.cost ? `<div class="gc">${esc17(g.cost)}</div>` : ""}${g.fix ? `<div class="gf">What we do: ${esc17(g.fix)}</div>` : ""}</div>`;
50166
50602
  const o = review.offer;
50167
50603
  const confirmed = opts.scope && !opts.scope.wholePreset && !opts.scope.awaitingConfirmation ? opts.scope : void 0;
50168
50604
  const purchased = confirmed || opts.purchasedFeatureIds ? [.../* @__PURE__ */ new Set([
@@ -50172,8 +50608,8 @@ function reviewDocumentHtml(review, opts) {
50172
50608
  const unbuilt = opts.scope?.notBuilt ?? [];
50173
50609
  const represented = [...opts.scope?.built ?? [], ...unbuilt].map((f) => BUILD_ASSESSMENT_FEATURE[f.id]).concat(opts.scope && !opts.scope.awaitingConfirmation ? ["pipeline"] : []);
50174
50610
  const assessmentNext = opts.assessment && purchased ? assessmentFeatureLines(opts.assessment, [...purchased, ...represented]) : [];
50175
- const available = unbuilt.map((f) => `<div class="build"><b>${esc16(f.name)}</b><span>${esc16(f.what)}${f.figure ? ` <b>${esc16(f.figure)}</b>` : ""}</span></div>`).join("") + assessmentNext.map((line3) => `<p>${esc16(line3)}</p>`).join("");
50176
- return `<!doctype html><html><head><meta charset="utf-8"><title>${esc16(opts.clientName)} \u2014 Growth Blueprint</title><style>
50611
+ const available = unbuilt.map((f) => `<div class="build"><b>${esc17(f.name)}</b><span>${esc17(f.what)}${f.figure ? ` <b>${esc17(f.figure)}</b>` : ""}</span></div>`).join("") + assessmentNext.map((line3) => `<p>${esc17(line3)}</p>`).join("");
50612
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${esc17(opts.clientName)} \u2014 Growth Blueprint</title><style>
50177
50613
  @page{margin:0.6in}
50178
50614
  *{box-sizing:border-box}
50179
50615
  body{font-family:-apple-system,Segoe UI,Helvetica,sans-serif;color:#1f2430;margin:0;line-height:1.55}
@@ -50208,40 +50644,40 @@ function reviewDocumentHtml(review, opts) {
50208
50644
  <div class="wrap">
50209
50645
  <div class="cover">
50210
50646
  <p class="eyebrow">Growth Blueprint</p>
50211
- <h1>${esc16(opts.clientName)}</h1>
50212
- <p class="sub">${agency ? `Prepared by ${esc16(agency)} \xB7 ` : ""}${date}</p>
50647
+ <h1>${esc17(opts.clientName)}</h1>
50648
+ <p class="sub">${agency ? `Prepared by ${esc17(agency)} \xB7 ` : ""}${date}</p>
50213
50649
  </div>
50214
- ${opts.planLines?.length ? `<div class="head">Executable plan for approval</div>${opts.planLines.map((line3) => `<p style="white-space:pre-wrap">${esc16(line3)}</p>`).join("")}` : ""}
50215
- ${review.headline ? `<div class="lead">${esc16(review.headline)}</div>` : ""}
50650
+ ${opts.planLines?.length ? `<div class="head">Executable plan for approval</div>${opts.planLines.map((line3) => `<p style="white-space:pre-wrap">${esc17(line3)}</p>`).join("")}` : ""}
50651
+ ${review.headline ? `<div class="lead">${esc17(review.headline)}</div>` : ""}
50216
50652
  ${review.ica ? `<div class="head">Who you're really selling to</div>
50217
- <p>${esc16(review.ica.who)}</p>
50218
- ${review.ica.painPoints?.length ? `<p class="kv"><b>What keeps them up:</b> ${esc16(review.ica.painPoints.join(" \xB7 "))}</p>` : ""}
50219
- ${review.ica.objections?.length ? `<p class="kv"><b>Why they hesitate:</b> ${esc16(review.ica.objections.join(" \xB7 "))}</p>` : ""}
50220
- ${review.ica.buyingTrigger ? `<p class="kv"><b>What makes them finally buy:</b> ${esc16(review.ica.buyingTrigger)}</p>` : ""}
50221
- ${review.ica.whereTheyAre ? `<p class="kv"><b>Where they already are:</b> ${esc16(review.ica.whereTheyAre)}</p>` : ""}` : ""}
50653
+ <p>${esc17(review.ica.who)}</p>
50654
+ ${review.ica.painPoints?.length ? `<p class="kv"><b>What keeps them up:</b> ${esc17(review.ica.painPoints.join(" \xB7 "))}</p>` : ""}
50655
+ ${review.ica.objections?.length ? `<p class="kv"><b>Why they hesitate:</b> ${esc17(review.ica.objections.join(" \xB7 "))}</p>` : ""}
50656
+ ${review.ica.buyingTrigger ? `<p class="kv"><b>What makes them finally buy:</b> ${esc17(review.ica.buyingTrigger)}</p>` : ""}
50657
+ ${review.ica.whereTheyAre ? `<p class="kv"><b>Where they already are:</b> ${esc17(review.ica.whereTheyAre)}</p>` : ""}` : ""}
50222
50658
  ${o ? `<div class="head">Your offer, graded</div>
50223
50659
  <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>
50224
- ${o.dreamOutcome ? `<p class="kv"><b>Worth wanting:</b> ${esc16(o.dreamOutcome)}</p>` : ""}
50225
- ${o.believability ? `<p class="kv"><b>Believable:</b> ${esc16(o.believability)}</p>` : ""}
50226
- ${o.speed ? `<p class="kv"><b>Speed to result:</b> ${esc16(o.speed)}</p>` : ""}
50227
- ${o.effort ? `<p class="kv"><b>Effort asked of them:</b> ${esc16(o.effort)}</p>` : ""}
50228
- ${o.biggestLever ? `<p class="kv"><b>Biggest lever:</b> ${esc16(o.biggestLever)}</p>` : ""}
50229
- ${o.recommendedOffer ? `<div class="lead"><b>Run this instead:</b> ${esc16(o.recommendedOffer)}</div>` : ""}` : ""}
50660
+ ${o.dreamOutcome ? `<p class="kv"><b>Worth wanting:</b> ${esc17(o.dreamOutcome)}</p>` : ""}
50661
+ ${o.believability ? `<p class="kv"><b>Believable:</b> ${esc17(o.believability)}</p>` : ""}
50662
+ ${o.speed ? `<p class="kv"><b>Speed to result:</b> ${esc17(o.speed)}</p>` : ""}
50663
+ ${o.effort ? `<p class="kv"><b>Effort asked of them:</b> ${esc17(o.effort)}</p>` : ""}
50664
+ ${o.biggestLever ? `<p class="kv"><b>Biggest lever:</b> ${esc17(o.biggestLever)}</p>` : ""}
50665
+ ${o.recommendedOffer ? `<div class="lead"><b>Run this instead:</b> ${esc17(o.recommendedOffer)}</div>` : ""}` : ""}
50230
50666
  ${review.gaps?.length ? `<div class="head">What's leaking today (${review.gaps.length})</div>${review.gaps.map(gap).join("")}` : ""}
50231
- ${review.build?.length ? `<div class="head">What we're building</div>${review.build.map((b) => `<div class="build"><b>${esc16(b.item)}</b><span>${esc16(b.because)}</span></div>`).join("")}` : ""}
50667
+ ${review.build?.length ? `<div class="head">What we're building</div>${review.build.map((b) => `<div class="build"><b>${esc17(b.item)}</b><span>${esc17(b.because)}</span></div>`).join("")}` : ""}
50232
50668
  ${opts.scope?.awaitingConfirmation ? `<div class="head">What this build includes</div>
50233
50669
  <p>Not confirmed yet. The build waits until the features to build are confirmed.</p>
50234
50670
  ` : opts.scope ? `<div class="head">What this build includes</div>
50235
- <p>${opts.scope.package ? `Built to your <b>${esc16(opts.scope.package)}</b> package: ` : ""}${opts.scope.built.length ? esc16(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>
50671
+ <p>${opts.scope.package ? `Built to your <b>${esc17(opts.scope.package)}</b> package: ` : ""}${opts.scope.built.length ? esc17(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>
50236
50672
  ` : ""}
50237
50673
  ${opts.bought ? `<div class="head">What you bought</div>
50238
- <p>${esc16(opts.bought.assessment)}</p>
50239
- ${opts.bought.confirmed ? opts.bought.bought.map((b) => `<div class="build"><b>${esc16(b.name)}</b><span>${esc16(b.means)}. ${esc16(b.delivery)}.</span></div>`).join("") : "<p>Not confirmed yet. The build waits until what you bought is saved.</p>"}
50240
- ${opts.bought.confirmed && opts.bought.stillToDo.length ? `<div class="head">Still to do</div><div class="ask"><ul>${opts.bought.stillToDo.map((line3) => `<li>${esc16(line3)}</li>`).join("")}</ul></div>` : ""}` : ""}
50241
- ${review.unknowns?.length ? `<div class="head">What we still need from you</div><div class="ask"><ul>${review.unknowns.map((u) => `<li>${esc16(u)}</li>`).join("")}</ul></div>` : ""}
50242
- ${opts.assessment ? `<div class="head">Assessment</div><p>Snapshot attached on ${esc16(opts.assessment.attachedAt.slice(0, 10))}.</p>${assessmentFeatureLines(opts.assessment, [], false).filter((line3) => line3.includes("by your own numbers")).map((line3) => `<p>${esc16(line3)}</p>`).join("")}` : ""}
50674
+ <p>${esc17(opts.bought.assessment)}</p>
50675
+ ${opts.bought.confirmed ? opts.bought.bought.map((b) => `<div class="build"><b>${esc17(b.name)}</b><span>${esc17(b.means)}. ${esc17(b.delivery)}.</span></div>`).join("") : "<p>Not confirmed yet. The build waits until what you bought is saved.</p>"}
50676
+ ${opts.bought.confirmed && opts.bought.stillToDo.length ? `<div class="head">Still to do</div><div class="ask"><ul>${opts.bought.stillToDo.map((line3) => `<li>${esc17(line3)}</li>`).join("")}</ul></div>` : ""}` : ""}
50677
+ ${review.unknowns?.length ? `<div class="head">What we still need from you</div><div class="ask"><ul>${review.unknowns.map((u) => `<li>${esc17(u)}</li>`).join("")}</ul></div>` : ""}
50678
+ ${opts.assessment ? `<div class="head">Assessment</div><p>Snapshot attached on ${esc17(opts.assessment.attachedAt.slice(0, 10))}.</p>${assessmentFeatureLines(opts.assessment, [], false).filter((line3) => line3.includes("by your own numbers")).map((line3) => `<p>${esc17(line3)}</p>`).join("")}` : ""}
50243
50679
  ${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>` : ""}
50244
- <footer><span>${agency ? esc16(agency) : "Growth Blueprint"}</span><span>${esc16(opts.clientName)} \xB7 ${date}</span></footer>
50680
+ <footer><span>${agency ? esc17(agency) : "Growth Blueprint"}</span><span>${esc17(opts.clientName)} \xB7 ${date}</span></footer>
50245
50681
  </div></body></html>`;
50246
50682
  }
50247
50683
  var GAP_CHECKLIST;
@@ -50361,7 +50797,7 @@ var init_client_fields = __esm({
50361
50797
  });
50362
50798
 
50363
50799
  // src/command-os/assessment/report.js
50364
- function esc13(s) {
50800
+ function esc14(s) {
50365
50801
  return String(s === void 0 || s === null ? "" : s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
50366
50802
  }
50367
50803
  function isFigure(v) {
@@ -50381,7 +50817,7 @@ function renderReport2(opts) {
50381
50817
  var assumptionLabels = opts.assumptionLabels || {};
50382
50818
  var date = opts.date || "";
50383
50819
  var accent = safeColour(agency.accent);
50384
- var biz = esc13(state.biz || "your business");
50820
+ var biz = esc14(state.biz || "your business");
50385
50821
  var F = Array.isArray(r.F) ? r.F : [];
50386
50822
  var O = Array.isArray(r.O) ? r.O : [];
50387
50823
  var hrs = Array.isArray(r.hrs) ? r.hrs : [];
@@ -50392,23 +50828,23 @@ function renderReport2(opts) {
50392
50828
  out.push("<title>Where " + biz + " is leaking</title>");
50393
50829
  out.push("<style>" + css(accent) + "</style></head><body><div class=wrap>");
50394
50830
  out.push("<header>");
50395
- if (agency.name) out.push('<p class="by">Prepared by ' + esc13(agency.name) + "</p>");
50831
+ if (agency.name) out.push('<p class="by">Prepared by ' + esc14(agency.name) + "</p>");
50396
50832
  out.push("<h1>Where " + biz + " is leaking</h1>");
50397
- var who = state.client && state.client.name ? esc13(state.client.name) + (state.client.role ? ", " + esc13(state.client.role) : "") : "";
50833
+ var who = state.client && state.client.name ? esc14(state.client.name) + (state.client.role ? ", " + esc14(state.client.role) : "") : "";
50398
50834
  if (who) out.push('<p class="for">Prepared for ' + who + "</p>");
50399
- if (date) out.push('<p class="when">' + esc13(date) + "</p>");
50835
+ if (date) out.push('<p class="when">' + esc14(date) + "</p>");
50400
50836
  if (agency.method || agency.promise) {
50401
- out.push('<p class="about">' + [agency.method, agency.promise].filter(Boolean).map(esc13).join(" ") + "</p>");
50837
+ out.push('<p class="about">' + [agency.method, agency.promise].filter(Boolean).map(esc14).join(" ") + "</p>");
50402
50838
  }
50403
50839
  out.push("</header>");
50404
50840
  if (r.verdict === "insufficient") {
50405
- out.push('<div class="verdict stop"><b>We could not put a number on this yet</b>' + esc13(r.line || "") + "</div>");
50841
+ out.push('<div class="verdict stop"><b>We could not put a number on this yet</b>' + esc14(r.line || "") + "</div>");
50406
50842
  out.push(measureFirst(O));
50407
50843
  out.push(footer(agency));
50408
50844
  return out.join("\n") + "</div></body></html>";
50409
50845
  }
50410
50846
  if (r.verdict === "thin") {
50411
- out.push('<div class="verdict warn"><b>Honestly, there is not much here</b>' + esc13(r.line || "") + "</div>");
50847
+ out.push('<div class="verdict warn"><b>Honestly, there is not much here</b>' + esc14(r.line || "") + "</div>");
50412
50848
  }
50413
50849
  out.push('<div class="hgrid">');
50414
50850
  if (r.hrsWk > 0) out.push(head3(r.hrsWk.toFixed(1), "hours a week going into work a system should do"));
@@ -50420,7 +50856,7 @@ function renderReport2(opts) {
50420
50856
  for (var a = 0; a < opts.answered.length; a++) {
50421
50857
  var q3 = opts.answered[a];
50422
50858
  var numeric = q3.unit !== void 0 ? q3.unit !== "text" : isFigure(q3.value);
50423
- out.push("<tr><td>" + esc13(q3.ask) + '</td><td class="' + (numeric ? "num" : "said") + '">' + (q3.unknown ? '<span class="unk">you did not know</span>' : esc13(q3.value)) + "</td></tr>");
50859
+ out.push("<tr><td>" + esc14(q3.ask) + '</td><td class="' + (numeric ? "num" : "said") + '">' + (q3.unknown ? '<span class="unk">you did not know</span>' : esc14(q3.value)) + "</td></tr>");
50424
50860
  }
50425
50861
  out.push("</table>");
50426
50862
  }
@@ -50429,15 +50865,15 @@ function renderReport2(opts) {
50429
50865
  out.push('<tr><th>What is happening</th><th class="num">A month</th></tr>');
50430
50866
  for (var i = 0; i < F.length; i++) {
50431
50867
  var f = F[i];
50432
- var note = f.assumption ? '<span class="assume">' + esc13(assumptionLabels[f.assumption] || "This is your assumption, not a measurement.") + "</span>" : "";
50433
- out.push("<tr><td><strong>" + esc13(f.title) + "</strong>" + note + '</td><td class="num">' + money6(f.monthly) + "</td></tr>");
50868
+ var note = f.assumption ? '<span class="assume">' + esc14(assumptionLabels[f.assumption] || "This is your assumption, not a measurement.") + "</span>" : "";
50869
+ out.push("<tr><td><strong>" + esc14(f.title) + "</strong>" + note + '</td><td class="num">' + money6(f.monthly) + "</td></tr>");
50434
50870
  }
50435
50871
  out.push('<tr class="tot"><td><strong>Leaking, every month</strong></td><td class="num"><strong>' + money6(r.monthly) + "</strong></td></tr>");
50436
50872
  if (r.stop > 0) {
50437
- out.push('<tr><td><strong>What we expect to actually stop</strong><span class="assume">' + esc13(assumptionLabels.recovery || "No system catches everything. This is your figure.") + '</span></td><td class="num"><strong>' + money6(r.stop) + "</strong></td></tr>");
50873
+ out.push('<tr><td><strong>What we expect to actually stop</strong><span class="assume">' + esc14(assumptionLabels.recovery || "No system catches everything. This is your figure.") + '</span></td><td class="num"><strong>' + money6(r.stop) + "</strong></td></tr>");
50438
50874
  }
50439
50875
  if (r.oneoff !== null && r.oneoff !== void 0 && r.oneoff > 0) {
50440
- out.push('<tr><td>Your dormant list, worked once<span class="assume">' + esc13(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">' + money6(r.oneoff) + "</td></tr>");
50876
+ out.push('<tr><td>Your dormant list, worked once<span class="assume">' + esc14(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">' + money6(r.oneoff) + "</td></tr>");
50441
50877
  }
50442
50878
  out.push("</table>");
50443
50879
  }
@@ -50448,14 +50884,14 @@ function renderReport2(opts) {
50448
50884
  out.push("<h2>Named, and deliberately not priced</h2><ul>");
50449
50885
  for (var j = 0; j < named.length; j++) {
50450
50886
  var o = named[j];
50451
- out.push("<li><strong>" + esc13(o.title) + "</strong>: " + esc13(o.declined ? "priced only on an assumption you chose not to make." : o.reason) + "</li>");
50887
+ out.push("<li><strong>" + esc14(o.title) + "</strong>: " + esc14(o.declined ? "priced only on an assumption you chose not to make." : o.reason) + "</li>");
50452
50888
  }
50453
50889
  out.push("</ul><p class=small>We can see these matter and we are not going to invent a number for them.</p>");
50454
50890
  }
50455
50891
  if (hrs.length) {
50456
50892
  out.push("<h2>Where the time goes</h2><table>");
50457
50893
  for (var h = 0; h < hrs.length; h++) {
50458
- out.push("<tr><td>" + esc13(hrs[h].t) + '</td><td class="num">' + Number(hrs[h].h).toFixed(1) + " hrs</td></tr>");
50894
+ out.push("<tr><td>" + esc14(hrs[h].t) + '</td><td class="num">' + Number(hrs[h].h).toFixed(1) + " hrs</td></tr>");
50459
50895
  }
50460
50896
  out.push('<tr class="tot"><td><strong>A week</strong></td><td class="num"><strong>' + Number(r.hrsWk).toFixed(1) + "</strong></td></tr></table>");
50461
50897
  }
@@ -50467,7 +50903,7 @@ function renderReport2(opts) {
50467
50903
  var svc = services.find(function(s) {
50468
50904
  return Array.isArray(s.covers) && s.covers.indexOf(fx.id) !== -1;
50469
50905
  });
50470
- out.push("<tr><td>" + esc13(fx.title) + "</td><td>" + esc13(svc ? svc.name : fx.fix || "") + "</td></tr>");
50906
+ out.push("<tr><td>" + esc14(fx.title) + "</td><td>" + esc14(svc ? svc.name : fx.fix || "") + "</td></tr>");
50471
50907
  }
50472
50908
  out.push("</table>");
50473
50909
  if (!services.length) {
@@ -50483,7 +50919,7 @@ function renderReport2(opts) {
50483
50919
  }, 0);
50484
50920
  out.push("<h2>The investment</h2><table>");
50485
50921
  for (var p = 0; p < priced.length; p++) {
50486
- out.push("<tr><td>" + esc13(priced[p].name) + '</td><td class="num">' + money6(priced[p].price) + "</td></tr>");
50922
+ out.push("<tr><td>" + esc14(priced[p].name) + '</td><td class="num">' + money6(priced[p].price) + "</td></tr>");
50487
50923
  }
50488
50924
  out.push('<tr class="tot"><td><strong>Total</strong></td><td class="num"><strong>' + money6(total) + "</strong></td></tr>");
50489
50925
  out.push('<tr><td>Against what we expect to stop, first year</td><td class="num">' + money6(r.annualStop) + "</td></tr>");
@@ -50498,7 +50934,7 @@ function renderReport2(opts) {
50498
50934
  return out.join("\n") + "</div></body></html>";
50499
50935
  }
50500
50936
  function head3(n, label2) {
50501
- return '<div><span class="hn">' + esc13(n) + '</span><span class="hl">' + esc13(label2) + "</span></div>";
50937
+ return '<div><span class="hn">' + esc14(n) + '</span><span class="hl">' + esc14(label2) + "</span></div>";
50502
50938
  }
50503
50939
  function measureFirst(O) {
50504
50940
  var seen = [];
@@ -50507,18 +50943,18 @@ function measureFirst(O) {
50507
50943
  for (var j = 0; j < m.length; j++) if (seen.indexOf(m[j]) === -1) seen.push(m[j]);
50508
50944
  }
50509
50945
  if (!seen.length) return "";
50510
- 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. A week of writing them down is enough.</p><ul><li>" + seen.map(esc13).join("</li><li>") + "</li></ul>";
50946
+ 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. A week of writing them down is enough.</p><ul><li>" + seen.map(esc14).join("</li><li>") + "</li></ul>";
50511
50947
  }
50512
50948
  function sourceLine(state, r, labels) {
50513
50949
  var used = [];
50514
50950
  var F = r.F || [];
50515
50951
  for (var i = 0; i < F.length; i++) if (F[i].assumption && used.indexOf(F[i].assumption) === -1) used.push(F[i].assumption);
50516
50952
  var n = used.length + (r.stop > 0 ? 1 : 0);
50517
- return '<p class="src">Every figure here came from what you told us' + (state.trade ? ", or from your own " + esc13(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>";
50953
+ return '<p class="src">Every figure here came from what you told us' + (state.trade ? ", or from your own " + esc14(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>";
50518
50954
  }
50519
50955
  function footer(agency) {
50520
50956
  if (!agency.name) return "";
50521
- return "<footer>" + esc13(agency.name) + (agency.contact ? " &middot; " + esc13(agency.contact) : "") + (agency.guarantee ? '<br><span class="guarantee">' + esc13(agency.guarantee) + "</span>" : "") + "</footer>";
50957
+ return "<footer>" + esc14(agency.name) + (agency.contact ? " &middot; " + esc14(agency.contact) : "") + (agency.guarantee ? '<br><span class="guarantee">' + esc14(agency.guarantee) + "</span>" : "") + "</footer>";
50522
50958
  }
50523
50959
  function css(accent) {
50524
50960
  return [
@@ -51887,8 +52323,11 @@ __export(dashboard_exports, {
51887
52323
  setClientScope: () => setClientScope,
51888
52324
  setPending: () => setPending,
51889
52325
  setStage: () => setStage,
52326
+ statusFromThisRun: () => statusFromThisRun,
51890
52327
  uniqueDocName: () => uniqueDocName,
51891
52328
  unknownCapabilityHtml: () => unknownCapabilityHtml,
52329
+ unverifiedStaffNames: () => unverifiedStaffNames,
52330
+ verifiedStaffNames: () => verifiedStaffNames,
51892
52331
  verifyPanelOutcome: () => verifyPanelOutcome,
51893
52332
  withBoughtJobs: () => withBoughtJobs,
51894
52333
  withCockpitShell: () => withCockpitShell,
@@ -52110,6 +52549,16 @@ async function resolvePlan2(creds) {
52110
52549
  }
52111
52550
  return { error: "Could not verify the license (offline and no valid cached attestation)." };
52112
52551
  }
52552
+ function unverifiedStaffNames(check) {
52553
+ return (check?.missing ?? []).map((m) => String(m.name ?? "").trim()).filter(Boolean);
52554
+ }
52555
+ function statusFromThisRun(status, runStartedAt) {
52556
+ if (!status?.readAt || !runStartedAt) return void 0;
52557
+ return Date.parse(status.readAt) >= Date.parse(runStartedAt) ? status : void 0;
52558
+ }
52559
+ function verifiedStaffNames(check) {
52560
+ return (check?.matched ?? []).map((m) => String(m.name ?? "").trim()).filter(Boolean);
52561
+ }
52113
52562
  function verifyPanelOutcome(value) {
52114
52563
  return value.plain?.summary?.trim() ? value.plain : { summary: value.summary, issues: value.issues };
52115
52564
  }
@@ -52145,6 +52594,8 @@ function startRun(locationId2, stage, onDone, meta2 = {}) {
52145
52594
  // A build re-run reuses the approved plan already saved for this account (finding 10).
52146
52595
  // ...and only while it still builds the scope the member has confirmed now: a
52147
52596
  // changed selection composes the plan fresh from the preset (delta round 3, T-M4).
52597
+ // Rehearsal 5 (BUG-2026-09-30-10): Build is told who the staff check matched in this account.
52598
+ ...stage === 3 ? { verifiedStaff: verifiedStaffNames(state.staffChecks?.[locationId2]) } : {},
52148
52599
  savedPlan: (() => {
52149
52600
  const sp = stage === 3 ? readSavedPlan(rk) : null;
52150
52601
  return sp && sp.inputHash === meta2.inputHash && savedPlanMatchesScope(sp, state.scope?.[rk]) ? { planId: sp.planId, savedAt: sp.savedAt, summary: planSummary(sp) } : void 0;
@@ -52227,6 +52678,8 @@ function startRun(locationId2, stage, onDone, meta2 = {}) {
52227
52678
  } catch {
52228
52679
  }
52229
52680
  }
52681
+ const buildFacts = stage === 3 ? { verifiedStaff: context2.verifiedStaff ?? [], unverifiedStaff: unverifiedStaffNames(state.staffChecks?.[locationId2]), customValues: statusFromThisRun(readSavedPlan(rk)?.customValueStatus, run.startedAt) } : void 0;
52682
+ if (buildFacts) Object.assign(outcome, guardBuildReport({ summary: outcome.summary, issues: outcome.issues }, buildFacts));
52230
52683
  if (stage === 4 || outcome.summary || outcome.issues?.length) {
52231
52684
  const at = (/* @__PURE__ */ new Date()).toISOString();
52232
52685
  const save = (plain) => {
@@ -52236,10 +52689,11 @@ function startRun(locationId2, stage, onDone, meta2 = {}) {
52236
52689
  save(scopeRefusal ? outcome.plain : void 0);
52237
52690
  if (PLAIN_REPORT_STAGES.has(stage) && !scopeRefusal) {
52238
52691
  run.lines.push("Writing the plain-English version of what came back\u2026");
52239
- const plain = await rewriteOutcomePlain(STAGES2[stage], verifyRaw ?? outcome, { onEvent: (l) => run.lines.push(l), onCost: (costUsd) => {
52692
+ const rewritten = await rewriteOutcomePlain(STAGES2[stage], verifyRaw ?? outcome, { onEvent: (l) => run.lines.push(l), onCost: (costUsd) => {
52240
52693
  recordRunCost({ at, locationId: locationId2, stage, kind: "plain rewrite", ok: true, costUsd });
52241
52694
  run.lines.push(`Plain rewrite: ${costUsd === void 0 ? "cost not reported" : formatUsd(costUsd)}.`);
52242
52695
  } });
52696
+ const plain = buildFacts ? guardBuildReport(rewritten, buildFacts) : rewritten;
52243
52697
  save(plain);
52244
52698
  }
52245
52699
  }
@@ -52794,6 +53248,10 @@ function initials(name){const p=String(name||"").trim().split(/\\s+/);return ((p
52794
53248
  /* Command Design System pieces, built as text (never markup from data). */
52795
53249
  function mk(tag,cls,text){const e=document.createElement(tag);if(cls)e.className=cls;if(text!=null)e.textContent=text;return e}
52796
53250
  function chip(tone,text){return mk("span","cds-chip"+(tone?" "+tone:""),text)}
53251
+ /* Sentences joined for display (rehearsal 5, BUG-2026-09-30-17): a part that already ends a sentence
53252
+ gets no second full stop, so "cannot send." + "Buy a local..." never reads "cannot send.. Buy". */
53253
+ function endSentence(s){const t=String(s==null?"":s).trim();return !t||/[.!?]["')\\]]?$/.test(t)?t:t+"."}
53254
+ function joinSentences(...parts){const xs=parts.map(p=>String(p==null?"":p).trim()).filter(Boolean);return xs.map((p,i)=>i<xs.length-1?endSentence(p):p).join(" ")}
52797
53255
  function panel(label,title,meta){const s=mk("section","cds-panel");const h=mk("div","cds-panel-head");const t=mk("div","txt");
52798
53256
  if(label)t.appendChild(mk("div","cds-panel-label",label));if(title)t.appendChild(mk("h2","cds-panel-title",title));h.appendChild(t);
52799
53257
  if(meta){const m=mk("div","cds-panel-meta");m.appendChild(meta);h.appendChild(m)}s.appendChild(h);return s}
@@ -52966,7 +53424,7 @@ function preparedFormBlock(box,id){const p=iPrepared[id];
52966
53424
  "Left blank because GoHighLevel's form will not accept a 555-01xx test number. Type a real number there, or leave it empty":"left blank, because GoHighLevel's form will not take a 555-01xx test number. Type a real number there, or leave it empty"};
52967
53425
  for(const l of left){const li=document.createElement("li");li.textContent=l.label+": ";
52968
53426
  l.values.forEach((v,i)=>{if(i)li.appendChild(document.createTextNode(", "));const val=mk("span","copy-val",String(v));li.appendChild(val);li.appendChild(document.createTextNode(" "));li.appendChild(copyButton(String(v),val))});
52969
- li.appendChild(document.createTextNode(". "+(WHY[l.reason]||l.reason)+"."));ul.appendChild(li)}
53427
+ li.appendChild(document.createTextNode(". "+endSentence(WHY[l.reason]||l.reason)));ul.appendChild(li)}
52970
53428
  box.appendChild(ul)}
52971
53429
  const rf=document.createElement("button");rf.className="cds-btn ghost";rf.textContent="Refresh status";
52972
53430
  rf.onclick=()=>iLoad(id);box.appendChild(rf)}
@@ -53379,7 +53837,7 @@ function reviewPanel(id,name,state,run){const r=rData[id];const d=document.creat
53379
53837
  if((r.gaps||[]).length){const s3=document.createElement("div");s3.className="cds-stack";const gh=document.createElement("h4");gh.className="cds-panel-label";gh.textContent="What's missing ("+r.gaps.length+")";s3.appendChild(gh);
53380
53838
  const gl=mk("ul","cds-list gaps");
53381
53839
  for(const g of r.gaps){const gg=mk("li","gap "+(g.priority||"soon"));
53382
- const pri=g.priority||"soon";const gt=mk("span","ttl",String(g.title||""));gt.appendChild(mk("span","sub",moneyText((g.cost?g.cost+". ":"")+(g.fix||""))));gg.appendChild(gt);
53840
+ const pri=g.priority||"soon";const gt=mk("span","ttl",String(g.title||""));gt.appendChild(mk("span","sub",moneyText(joinSentences(g.cost,g.fix))));gg.appendChild(gt);
53383
53841
  const ge=mk("span","end");ge.appendChild(chip(pri==="now"?"warn":"",pri==="now"?"Fix now":pri==="later"?"Later":"Soon"));gg.appendChild(ge);gl.appendChild(gg)}
53384
53842
  s3.appendChild(gl);
53385
53843
  d.appendChild(s3)}
@@ -53495,6 +53953,9 @@ function pasteKeyCard(quiet){const card=document.createElement("section");card.c
53495
53953
  registerNotice=r.ok?r.message:(r.error||"The sub-account could not be registered; check the connection and try again.");
53496
53954
  if(r.ok){planNotice="";registerNextId=String(r.locationId||"");chooseIntakeClient({locationId:r.locationId,name:r.name})}
53497
53955
  else{const shown=document.getElementById("register-client-status")||status,action=document.getElementById("register-client-button")||button;shown.setAttribute("role","alert");shown.textContent=registerNotice;const shownKey=document.getElementById("register-client-key"),shownId=document.getElementById("register-client-id");if(shownKey){shownKey.value="";shownKey.disabled=false}if(shownId)shownId.disabled=false;action.disabled=false;action.textContent="Register"}};
53956
+ /* Enter in either field submits, as a form would (rehearsal 5, BUG-2026-09-30-18): the key was
53957
+ pasted, Register never clicked, and nothing said so. The button's own handler does the work. */
53958
+ for(const input of [id,key])input.addEventListener("keydown",e=>{if(e.key!=="Enter"||e.isComposing)return;e.preventDefault();if(!button.disabled&&!registerBusy)button.click()});
53498
53959
  return card}
53499
53960
  /* The live build panel. Renders run.progress when the cockpit reports it (grouped counts, the
53500
53961
  line being written right now); with no progress object it falls back to today's plain log. */
@@ -53603,7 +54064,11 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
53603
54064
  // and the handler it had in the seven-column layout; the next-step panel draws the SAME
53604
54065
  // buttons again for the current step, so nothing appears there that its row would not offer.
53605
54066
  const staleOf=i=>state.buildViews?.[c.locationId]?.stale?.find(x=>x.stage===i);
53606
- const isDone=i=>(st[i]||"pending")==="done"&&!staleOf(i);
54067
+ /* A step that is running right now is never counted done (rehearsal 5, BUG-2026-09-30-15): Brief is
54068
+ marked done and then writes the Blueprint Review while still running, and the header read
54069
+ "Step 3 of 7: Approve \xB7 2 done" beside a Brief row that said Running. */
54070
+ const liveI=run&&run.running&&run.locationId===c.locationId&&typeof run.stage==="number"?run.stage:-1;
54071
+ const isDone=i=>(st[i]||"pending")==="done"&&!staleOf(i)&&i!==liveI;
53607
54072
  let curI=-1;for(let i=0;i<STAGES.length;i++){if(!isDone(i)){curI=i;break}}
53608
54073
  /* A step still waiting on the owner's approval is not counted as done, so the count matches its "Your call" chip. */
53609
54074
  const nDone=STAGES.filter((_,i)=>isDone(i)&&!(i===2&&!(state.buildApproved||{})[c.locationId])).length;
@@ -55781,9 +56246,10 @@ ${opportunityNoteFailure?.detail ?? ""}`);
55781
56246
  return titleCache.get(rk);
55782
56247
  };
55783
56248
  const named = (o, rk) => ({ ...o, ...o.summary !== void 0 && { summary: nameHandoffRefs(o.summary, titlesFor(rk)) }, ...o.issues && { issues: o.issues.map((x) => nameHandoffRefs(x, titlesFor(rk))) } });
56249
+ const briefShown = (key, o) => key.endsWith(":1") ? { ...o, ...o.issues && { issues: briefIssuesShown(o.issues) }, ...o.plain?.issues && { plain: { ...o.plain, issues: briefIssuesShown(o.plain.issues) } } } : o;
55784
56250
  const outcomes = Object.fromEntries(Object.entries(st.outcomes ?? {}).map(([key, raw]) => {
55785
56251
  const rk = key.slice(0, key.lastIndexOf(":"));
55786
- const value = { ...named(raw, rk), ...raw.plain && { plain: named(raw.plain, rk) } };
56252
+ const value = briefShown(key, { ...named(raw, rk), ...raw.plain && { plain: named(raw.plain, rk) } });
55787
56253
  return [key, { ...value, ...key.endsWith(":4") && value.verification ? { plain: verifyPanelOutcome(value) } : { ...plainFallbackOutcome(value), plain: plainFallbackOutcome(value.plain ?? value) }, error: value.error ? "The step did not finish. Check the account before retrying." : void 0 }];
55788
56254
  }));
55789
56255
  res.end(JSON.stringify({ ...st, assessmentFigures, boughtViews, outcomes, manualTasks: Object.fromEntries(Object.entries(st.manualTasks ?? {}).map(([id, tasks2]) => {
@@ -57133,6 +57599,8 @@ var init_dashboard = __esm({
57133
57599
  init_client_intake();
57134
57600
  init_client_intake();
57135
57601
  init_plan_store();
57602
+ init_brief_issues();
57603
+ init_build_facts();
57136
57604
  init_plain_outcome();
57137
57605
  init_staff_first();
57138
57606
  init_user_provisioning();
@@ -62946,10 +63414,10 @@ function renderVerificationHtml(v, branding = {}, base) {
62946
63414
  ];
62947
63415
  const s = v.summary;
62948
63416
  const verdictClass = v.verdict === "PASS" ? "ok" : v.verdict === "FAIL" ? "fail" : "warn";
62949
- const row = (r) => `<tr class="${STATE_CLASS[r.state]}"><td class="id">${esc14(r.ruleId)}</td><td><b>${esc14(r.name)}</b><div class="thr">${esc14(r.threshold)}</div></td><td><span class="pill ${STATE_CLASS[r.state]}">${STATE_LABEL[r.state]}</span></td><td>${esc14(r.measured ?? "")}${r.detail ? `<div class="det">${esc14(r.detail)}</div>` : ""}</td><td class="ev">${esc14(r.evidence ?? "")}</td></tr>`;
63417
+ const row = (r) => `<tr class="${STATE_CLASS[r.state]}"><td class="id">${esc15(r.ruleId)}</td><td><b>${esc15(r.name)}</b><div class="thr">${esc15(r.threshold)}</div></td><td><span class="pill ${STATE_CLASS[r.state]}">${STATE_LABEL[r.state]}</span></td><td>${esc15(r.measured ?? "")}${r.detail ? `<div class="det">${esc15(r.detail)}</div>` : ""}</td><td class="ev">${esc15(r.evidence ?? "")}</td></tr>`;
62950
63418
  return `<!doctype html>
62951
63419
  <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
62952
- <title>Design check \u2014 ${esc14(v.target.split("/").pop() ?? v.target)}</title>
63420
+ <title>Design check \u2014 ${esc15(v.target.split("/").pop() ?? v.target)}</title>
62953
63421
  <style>
62954
63422
  :root{--accent:${b.accent};--ink:#1F2428;--muted:#5C6570;--line:#E3E6E9;--bg:#FFFFFF;--ok:#2F7A5A;--fail:#A63D3D;--warn:#B9621C;--na:#6E7681}
62955
63423
  body{margin:0;background:var(--bg);color:var(--ink);font:18px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}
@@ -62980,23 +63448,23 @@ td.ev{font-family:ui-monospace,Menlo,monospace;font-size:12.5px;color:var(--mute
62980
63448
  .shots a{font-size:13px;color:var(--accent)}
62981
63449
  footer{margin-top:40px;color:var(--muted);font-size:13px;border-top:1px solid var(--line);padding-top:12px}
62982
63450
  </style></head><body><div class="wrap">
62983
- <header><div>${isRasterDataUri(b.logoDataUri) ? `<img src="${esc14(b.logoDataUri)}" alt="" style="height:28px;margin-bottom:8px">` : ""}<h1>Design check</h1><div class="agency">${b.agencyName ? esc14(b.agencyName) + " \xB7 " : ""}${esc14(v.medium)} \xB7 ${esc14(v.target)} \xB7 ${esc14(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>
63451
+ <header><div>${isRasterDataUri(b.logoDataUri) ? `<img src="${esc15(b.logoDataUri)}" alt="" style="height:28px;margin-bottom:8px">` : ""}<h1>Design check</h1><div class="agency">${b.agencyName ? esc15(b.agencyName) + " \xB7 " : ""}${esc15(v.medium)} \xB7 ${esc15(v.target)} \xB7 ${esc15(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>
62984
63452
  <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>
62985
- <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>${esc14(p)}</li>`).join("")}</ul>` : ""}${v.conformance.warnings.length ? `<div class="det">Warnings: ${v.conformance.warnings.map(esc14).join(" \xB7 ")}</div>` : ""}</div>
63453
+ <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>${esc15(p)}</li>`).join("")}</ul>` : ""}${v.conformance.warnings.length ? `<div class="det">Warnings: ${v.conformance.warnings.map(esc15).join(" \xB7 ")}</div>` : ""}</div>
62986
63454
  <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>
62987
- ${v.brandMatch ? `<p class="note"><b>Brand:</b> ${esc14(v.brandMatch.detail)}</p>` : ""}
63455
+ ${v.brandMatch ? `<p class="note"><b>Brand:</b> ${esc15(v.brandMatch.detail)}</p>` : ""}
62988
63456
  ${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("")}
62989
- ${v.screenshots.length ? `<h2>Screenshots</h2><div class="shots">${v.screenshots.map((f) => `<a href="${esc14(f)}">${esc14(f)}</a>`).join("")}</div>` : ""}
62990
- <footer>Ruleset ${esc14(v.ruleset)} \xB7 viewports ${esc14(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>
63457
+ ${v.screenshots.length ? `<h2>Screenshots</h2><div class="shots">${v.screenshots.map((f) => `<a href="${esc15(f)}">${esc15(f)}</a>`).join("")}</div>` : ""}
63458
+ <footer>Ruleset ${esc15(v.ruleset)} \xB7 viewports ${esc15(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>
62991
63459
  </div></body></html>`;
62992
63460
  }
62993
- var esc14, STATE_LABEL, STATE_CLASS;
63461
+ var esc15, STATE_LABEL, STATE_CLASS;
62994
63462
  var init_report2 = __esm({
62995
63463
  "src/command-os/design/report.ts"() {
62996
63464
  "use strict";
62997
63465
  init_branding();
62998
63466
  init_svg_sanitize();
62999
- esc14 = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
63467
+ esc15 = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
63000
63468
  STATE_LABEL = {
63001
63469
  "MEASURED-PASS": "Pass",
63002
63470
  "MEASURED-FAIL": "Fail",
@@ -67923,10 +68391,10 @@ var init_run3 = __esm({
67923
68391
 
67924
68392
  // src/command-os/prospect/report-page.ts
67925
68393
  function gradeRing2(score2, caption, size2 = 176) {
67926
- if (score2 === void 0) return `<div class="cds-ring" role="img" aria-label="Not graded. ${esc15(caption)}">${chip4("warn", "Not graded")}<div class="cap">${esc15(caption)}</div></div>`;
68394
+ if (score2 === void 0) return `<div class="cds-ring" role="img" aria-label="Not graded. ${esc16(caption)}">${chip4("warn", "Not graded")}<div class="cap">${esc16(caption)}</div></div>`;
67927
68395
  const s = Math.max(0, Math.min(100, Math.round(score2)));
67928
68396
  const r = 52, c = 2 * Math.PI * r, g = gradeOf(s), k = gcls2(s);
67929
- return `<div class="cds-ring" role="img" aria-label="Grade ${g}, ${s} out of 100. ${esc15(caption)}"><svg width="${size2}" height="${size2}" 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="44">${g}</text><text class="out" x="60" y="86" text-anchor="middle">${s}/100</text></svg>${caption ? `<div class="cap">${esc15(caption)}</div>` : ""}</div>`;
68397
+ return `<div class="cds-ring" role="img" aria-label="Grade ${g}, ${s} out of 100. ${esc16(caption)}"><svg width="${size2}" height="${size2}" 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="44">${g}</text><text class="out" x="60" y="86" text-anchor="middle">${s}/100</text></svg>${caption ? `<div class="cap">${esc16(caption)}</div>` : ""}</div>`;
67930
68398
  }
67931
68399
  function withoutPrice(name) {
67932
68400
  return name.replace(/[$\u00a3\u20ac]\s?\d[\d,.]*(\s?[kK])?(\s?(\/|per)\s?(mo|month|yr|year|week|wk))?/g, "").replace(/\b\d[\d,.]*\s?(\/|per)\s?(mo|month|yr|year)\b/gi, "").replace(/\s*[-|:,(]\s*\)?\s*$/, "").replace(/\(\s*\)/g, "").replace(/\s{2,}/g, " ").trim();
@@ -68020,11 +68488,11 @@ function bookButton(b, prefill) {
68020
68488
  const t = walkthroughTarget(b, prefill);
68021
68489
  if (!t) return "";
68022
68490
  const printed = t.via === "booking" ? shortUrl(b.bookingUrl.trim()) : t.via === "phone" ? `Call ${b.phone}` : `Email ${b.email}`;
68023
- return `${link2(t.href, WALKTHROUGH.button, "cds-btn rp-book")}<span class="rp-print-only">${esc15(printed)}</span>`;
68491
+ return `${link2(t.href, WALKTHROUGH.button, "cds-btn rp-book")}<span class="rp-print-only">${esc16(printed)}</span>`;
68024
68492
  }
68025
68493
  function walkthroughBlock(id, b, workflowAudit, prefill) {
68026
68494
  const reach = reachLinks(b);
68027
- return `<section data-section="${id}" id="${id}" class="rp-cta"><h2>${esc15(WALKTHROUGH.h2)}</h2>${workflowAudit ? `<p>${esc15(WALKTHROUGH.line)}</p>` : ""}${bookButton(b, prefill)}${reach.length ? `<p class="rp-reach">${esc15(b.name)} \xB7 ${reach.join(" \xB7 ")}</p>` : ""}</section>`;
68495
+ return `<section data-section="${id}" id="${id}" class="rp-cta"><h2>${esc16(WALKTHROUGH.h2)}</h2>${workflowAudit ? `<p>${esc16(WALKTHROUGH.line)}</p>` : ""}${bookButton(b, prefill)}${reach.length ? `<p class="rp-reach">${esc16(b.name)} \xB7 ${reach.join(" \xB7 ")}</p>` : ""}</section>`;
68028
68496
  }
68029
68497
  function competitorsAhead(r) {
68030
68498
  const mine = r.business.score.score;
@@ -68053,16 +68521,16 @@ function patientHeadline(gaps, a) {
68053
68521
  function checksCard(area, checks) {
68054
68522
  const cs = checks.filter((c) => c.area === area);
68055
68523
  const known = cs.filter((c) => c.verdict !== "unknown").length;
68056
- return `<div class="rp-card"><div class="rp-card-head"><h3>${esc15(AREAS3[area].label)}</h3><span class="rp-tag">${known} of ${cs.length} checked</span></div><ul class="rp-checks">${cs.map((c) => {
68524
+ return `<div class="rp-card"><div class="rp-card-head"><h3>${esc16(AREAS3[area].label)}</h3><span class="rp-tag">${known} of ${cs.length} checked</span></div><ul class="rp-checks">${cs.map((c) => {
68057
68525
  const [tone, word] = VERDICT[c.verdict];
68058
- return `<li><span class="q">${esc15(c.label)}</span>${chip4(tone, word)}<span class="ev">${esc15(c.evidence)}</span></li>`;
68526
+ return `<li><span class="q">${esc16(c.label)}</span>${chip4(tone, word)}<span class="ev">${esc16(c.evidence)}</span></li>`;
68059
68527
  }).join("")}</ul></div>`;
68060
68528
  }
68061
68529
  function brandCard(r) {
68062
68530
  const b = r.site.brand;
68063
68531
  const look = r.site.phone?.ok ? r.site.phone.look : void 0;
68064
68532
  if (!b) return `<div class="rp-card"><p class="rp-meta">The brand was not read.</p></div>`;
68065
- const sw = (label2, c) => c && HEX2.test(c.hex) ? `<span class="rp-swatch"><i style="background:${c.hex.toLowerCase()}"></i>${esc15(label2)} ${esc15(c.hex.toLowerCase())}</span>` : "";
68533
+ const sw = (label2, c) => c && HEX2.test(c.hex) ? `<span class="rp-swatch"><i style="background:${c.hex.toLowerCase()}"></i>${esc16(label2)} ${esc16(c.hex.toLowerCase())}</span>` : "";
68066
68534
  const norm6 = (x) => x.toLowerCase().replace(/[^a-z0-9]/g, "");
68067
68535
  const core = b.names.filter((n) => n.where !== "page title").map((n) => n.name);
68068
68536
  const base = core[0] ?? "";
@@ -68073,12 +68541,12 @@ function brandCard(r) {
68073
68541
  if (!b.shareImage) notes.push("No share image: a link to the site posted on Facebook or in a text shows no picture.");
68074
68542
  if (clash) notes.push("The site does not call itself by one name everywhere (listed on the left).");
68075
68543
  return `<div class="rp-two"><div class="rp-card"><h3>What a visitor sees</h3><dl class="rp-kv">
68076
- ${b.names.map((n) => `<dt>${esc15(n.where.charAt(0).toUpperCase() + n.where.slice(1))}</dt><dd>${esc15(n.name)}</dd>`).join("")}
68077
- <dt>Logo</dt><dd>${b.logo ? esc15(b.logo.url ? `${b.logo.where}: ${b.logo.url}` : b.logo.where) : "Not found"}</dd>
68078
- <dt>Browser-tab icon</dt><dd>${b.favicon ? esc15(b.favicon) : "Not found"}</dd>
68079
- <dt>Share image</dt><dd>${b.shareImage ? esc15(b.shareImage) : "Not found"}</dd>
68080
- ${b.headlines.length ? `<dt>Headlines</dt><dd>${b.headlines.map((h) => `"${esc15(h)}"`).join("<br>")}</dd>` : ""}
68081
- </dl></div><div class="rp-card"><h3>Colours and type</h3>${look && (look.background || look.button) ? `<div class="rp-swatches">${sw("Buttons", look.button ? { hex: look.button, source: "" } : void 0)}${sw("Background", look.background ? { hex: look.background, source: "" } : void 0)}${sw("Text", look.text ? { hex: look.text, source: "" } : void 0)}</div>${look.headingFont || look.bodyFont ? `<dl class="rp-kv"><dt>Headings</dt><dd>${esc15(look.headingFont ?? "Not read")}</dd><dt>Body text</dt><dd>${esc15(look.bodyFont ?? "Not read")}</dd></dl>` : ""}<p class="rp-meta">As the phone view drew the home page.</p>` : b.colors ? `<div class="rp-swatches">${sw("Accent", b.colors.accent)}${sw("Background", b.colors.background)}${sw("Text", b.colors.text)}</div>${b.fonts ? `<dl class="rp-kv"><dt>Headings</dt><dd>${esc15(b.fonts.display)}</dd><dt>Body text</dt><dd>${esc15(b.fonts.body)}</dd></dl>` : ""}<p class="rp-meta">Read from the site's stylesheets only (the phone view did not run), so these are a best reading${b.confidence !== void 0 ? `: ${Math.round(b.confidence * 100)}% were declared by the site, the rest are the most used values` : ""}.</p>` : `<p class="rp-meta">${esc15(b.unread ?? "Colours were not read.")}</p>`}${notes.length ? `<ul class="rp-list">${notes.map((n) => `<li>${esc15(n)}</li>`).join("")}</ul>` : `<p class="rp-meta">Logo, tab icon, share image and name are all in place.</p>`}</div></div>`;
68544
+ ${b.names.map((n) => `<dt>${esc16(n.where.charAt(0).toUpperCase() + n.where.slice(1))}</dt><dd>${esc16(n.name)}</dd>`).join("")}
68545
+ <dt>Logo</dt><dd>${b.logo ? esc16(b.logo.url ? `${b.logo.where}: ${b.logo.url}` : b.logo.where) : "Not found"}</dd>
68546
+ <dt>Browser-tab icon</dt><dd>${b.favicon ? esc16(b.favicon) : "Not found"}</dd>
68547
+ <dt>Share image</dt><dd>${b.shareImage ? esc16(b.shareImage) : "Not found"}</dd>
68548
+ ${b.headlines.length ? `<dt>Headlines</dt><dd>${b.headlines.map((h) => `"${esc16(h)}"`).join("<br>")}</dd>` : ""}
68549
+ </dl></div><div class="rp-card"><h3>Colours and type</h3>${look && (look.background || look.button) ? `<div class="rp-swatches">${sw("Buttons", look.button ? { hex: look.button, source: "" } : void 0)}${sw("Background", look.background ? { hex: look.background, source: "" } : void 0)}${sw("Text", look.text ? { hex: look.text, source: "" } : void 0)}</div>${look.headingFont || look.bodyFont ? `<dl class="rp-kv"><dt>Headings</dt><dd>${esc16(look.headingFont ?? "Not read")}</dd><dt>Body text</dt><dd>${esc16(look.bodyFont ?? "Not read")}</dd></dl>` : ""}<p class="rp-meta">As the phone view drew the home page.</p>` : b.colors ? `<div class="rp-swatches">${sw("Accent", b.colors.accent)}${sw("Background", b.colors.background)}${sw("Text", b.colors.text)}</div>${b.fonts ? `<dl class="rp-kv"><dt>Headings</dt><dd>${esc16(b.fonts.display)}</dd><dt>Body text</dt><dd>${esc16(b.fonts.body)}</dd></dl>` : ""}<p class="rp-meta">Read from the site's stylesheets only (the phone view did not run), so these are a best reading${b.confidence !== void 0 ? `: ${Math.round(b.confidence * 100)}% were declared by the site, the rest are the most used values` : ""}.</p>` : `<p class="rp-meta">${esc16(b.unread ?? "Colours were not read.")}</p>`}${notes.length ? `<ul class="rp-list">${notes.map((n) => `<li>${esc16(n)}</li>`).join("")}</ul>` : `<p class="rp-meta">Logo, tab icon, share image and name are all in place.</p>`}</div></div>`;
68082
68550
  }
68083
68551
  function memberColumn(ids, packages, sold) {
68084
68552
  if (!sold && !packages.length) return void 0;
@@ -68115,17 +68583,17 @@ function compareTable(r, member) {
68115
68583
  const shown = COMPARE.filter(([id]) => sites.some((s) => s.checks.some((c) => c.id === id && c.verdict !== "unknown")));
68116
68584
  const col = member ? memberColumn(shown.map(([id]) => id), member.packages, member.sold) : void 0;
68117
68585
  const covered = col ? [...col.values()].filter(Boolean).length : 0;
68118
- const ours = col && member ? `<th scope="col">${esc15(`With ${member.name}`)}<br><small>${esc15(`${covered} of ${shown.length} included`)}</small></th>` : "";
68586
+ const ours = col && member ? `<th scope="col">${esc16(`With ${member.name}`)}<br><small>${esc16(`${covered} of ${shown.length} included`)}</small></th>` : "";
68119
68587
  const oursCell = (id) => {
68120
68588
  if (!col) return "";
68121
68589
  const n = col.get(id);
68122
- return n ? `<td class="cds-yes">Yes<br><small>${esc15(n)}</small></td>` : `<td class="cds-unk">Not included</td>`;
68590
+ return n ? `<td class="cds-yes">Yes<br><small>${esc16(n)}</small></td>` : `<td class="cds-unk">Not included</td>`;
68123
68591
  };
68124
- const head4 = sites.map((s, i) => `<th scope="col"${hl(i)}>${esc15(s.name)}<br><small>${esc15(new URL(s.site.origin).hostname.replace(/^www\./, ""))}</small></th>`).join("") + ours;
68592
+ const head4 = sites.map((s, i) => `<th scope="col"${hl(i)}>${esc16(s.name)}<br><small>${esc16(new URL(s.site.origin).hostname.replace(/^www\./, ""))}</small></th>`).join("") + ours;
68125
68593
  const gradeRow = `<tr><th scope="row">Grade<small>Only the areas checked for each</small></th>${sites.map((s, i) => td(i, s.score.score !== void 0 ? `<td class="cds-num rp-grade"><span class="${gcls2(s.score.score)}">${gradeOf(s.score.score)}</span> ${s.score.score}</td>` : `<td class="cds-unk">Could not open</td>`)).join("")}${col ? `<td></td>` : ""}</tr>`;
68126
- const rows2 = shown.map(([id, label2]) => `<tr><th scope="row">${esc15(label2)}</th>${sites.map((s, i) => td(i, cell2(s, id))).join("")}${oursCell(id)}</tr>`).join("");
68594
+ const rows2 = shown.map(([id, label2]) => `<tr><th scope="row">${esc16(label2)}</th>${sites.map((s, i) => td(i, cell2(s, id))).join("")}${oursCell(id)}</tr>`).join("");
68127
68595
  const colNote = col && member ? ` The last column is what ${member.name} sets up, named as ${member.name} sells it.` : "";
68128
- return `<div class="rp-card"><div class="cds-matrix-wrap rp-scroll"><table class="cds-matrix"><thead><tr><th scope="col"></th>${head4}</tr></thead><tbody>${gradeRow}${rows2}</tbody></table></div><p class="rp-meta">Every site was read the same way within minutes of each other: the home page and up to ${r.competitors.length ? "two" : "three"} more pages its menu links to, robots.txt, the sitemap and one phone-size view. ${r.competitors.length ? "Competitors are the sites the agency named; none were searched for or added." : ""}${esc15(colNote)}</p>${sites.filter((s) => !s.site.reachable).map((s) => callout3(esc15(`${s.name}: ${s.site.unreachableReason ?? "could not be opened"}. Its column says "Not checked", never a fail.`), { tone: "warn" })).join("")}</div>`;
68596
+ return `<div class="rp-card"><div class="cds-matrix-wrap rp-scroll"><table class="cds-matrix"><thead><tr><th scope="col"></th>${head4}</tr></thead><tbody>${gradeRow}${rows2}</tbody></table></div><p class="rp-meta">Every site was read the same way within minutes of each other: the home page and up to ${r.competitors.length ? "two" : "three"} more pages its menu links to, robots.txt, the sitemap and one phone-size view. ${r.competitors.length ? "Competitors are the sites the agency named; none were searched for or added." : ""}${esc16(colNote)}</p>${sites.filter((s) => !s.site.reachable).map((s) => callout3(esc16(`${s.name}: ${s.site.unreachableReason ?? "could not be opened"}. Its column says "Not checked", never a fail.`), { tone: "warn" })).join("")}</div>`;
68129
68597
  }
68130
68598
  function offerParts(r, packages, sold, audience = "client") {
68131
68599
  const gaps = gapsOf(r.business.checks);
@@ -68151,10 +68619,10 @@ function offerParts(r, packages, sold, audience = "client") {
68151
68619
  const pk = allHave ? packages.filter((p) => fs52.every((f) => p.features.includes(f))).map((p) => withoutPrice(p.name)).filter(Boolean) : [];
68152
68620
  const inPk = pk.length ? `Included in ${list3(pk)}.` : allHave && fs52.length === 1 ? FEATURES[fs52[0]].what : "";
68153
68621
  const more = e.gaps.length > 2 ? ` And ${e.gaps.length - 2} more like ${e.gaps.length - 2 === 1 ? "it" : "them"}.` : "";
68154
- return `<li><span class="svc">${esc15(svc)}</span><span class="why">${esc15(e.gaps.slice(0, 2).join(" ") + more)}</span>${inPk ? `<span class="why">${esc15(inPk)}</span>` : ""}</li>`;
68622
+ return `<li><span class="svc">${esc16(svc)}</span><span class="why">${esc16(e.gaps.slice(0, 2).join(" ") + more)}</span>${inPk ? `<span class="why">${esc16(inPk)}</span>` : ""}</li>`;
68155
68623
  }).join("");
68156
68624
  const pitchCard = items ? `<div class="rp-card"><ul class="rp-offer">${items}</ul></div>` : "";
68157
- const plainCard = plain.length ? `<div class="rp-card"><h3>${items ? "Also found" : "What was found"}</h3><ul class="rp-list">${plain.map((g) => `<li>${esc15(g)}</li>`).join("")}</ul></div>` : "";
68625
+ const plainCard = plain.length ? `<div class="rp-card"><h3>${items ? "Also found" : "What was found"}</h3><ul class="rp-list">${plain.map((g) => `<li>${esc16(g)}</li>`).join("")}</ul></div>` : "";
68158
68626
  return {
68159
68627
  label: items ? "What closes the gaps" : "The gaps",
68160
68628
  // The one gap count (gapCount), the same number as the hero and the tile.
@@ -68170,7 +68638,7 @@ function speedNote(r) {
68170
68638
  const ph = r.business.site.phone;
68171
68639
  const fallback = ph?.ok && ph.lcpMs !== void 0 ? "The speed shown was measured by a phone-size Chrome on a fast office connection, which flatters slow sites." : "No other speed measurement was made, so speed is not checked in this report.";
68172
68640
  const why = sc.google === "no_key" ? "Google PageSpeed did not run for this report: no PageSpeed key is set up." : `Google PageSpeed was asked but gave no score: ${sc.reason.replace(/\.$/, "")}.`;
68173
- return callout3(esc15(`${why} ${fallback}`), { tone: "warn", title: "Speed check" });
68641
+ return callout3(esc16(`${why} ${fallback}`), { tone: "warn", title: "Speed check" });
68174
68642
  }
68175
68643
  function scoringTable() {
68176
68644
  const total = AREA_IDS.reduce((a, id) => a + SCORING.areas[id].weight, 0);
@@ -68178,19 +68646,19 @@ function scoringTable() {
68178
68646
  const rows2 = AREA_IDS.map((a) => {
68179
68647
  const cs = CHECK_IDS.filter((id) => SCORING.checks[id].area === a);
68180
68648
  const w = SCORING.areas[a].weight;
68181
- return `<tr><th scope="row">${esc15(SCORING.areas[a].label)}</th><td class="rp-num">${w} (${Math.round(100 * w / total)}%)</td><td><ul class="rp-list">${cs.map((id) => `<li>${esc15(SCORING.checks[id].label)}: ${pts(SCORING.checks[id].points)}</li>`).join("")}</ul></td></tr>`;
68649
+ return `<tr><th scope="row">${esc16(SCORING.areas[a].label)}</th><td class="rp-num">${w} (${Math.round(100 * w / total)}%)</td><td><ul class="rp-list">${cs.map((id) => `<li>${esc16(SCORING.checks[id].label)}: ${pts(SCORING.checks[id].points)}</li>`).join("")}</ul></td></tr>`;
68182
68650
  }).join("");
68183
- return `<div class="rp-card"><div class="rp-scroll"><table class="rp-score"><thead><tr><th scope="col">Area</th><th scope="col">Weight (share of the grade)</th><th scope="col">What is checked, and its points</th></tr></thead><tbody>${rows2}</tbody></table></div><p class="rp-meta">${esc15(`Each area scores 0 to 100 from its checks: a check earns all its points on a yes, half on a partly (or its measured share, such as a Google score of 62 out of 100), and none on a no. The grade is the average of the areas that were checked, each counted by its weight; an area that was not checked is left out and the rest count for more. With no working way to contact the business at all, the grade is capped at ${SCORING.capNoContact}.`)}</p></div>`;
68651
+ return `<div class="rp-card"><div class="rp-scroll"><table class="rp-score"><thead><tr><th scope="col">Area</th><th scope="col">Weight (share of the grade)</th><th scope="col">What is checked, and its points</th></tr></thead><tbody>${rows2}</tbody></table></div><p class="rp-meta">${esc16(`Each area scores 0 to 100 from its checks: a check earns all its points on a yes, half on a partly (or its measured share, such as a Google score of 62 out of 100), and none on a no. The grade is the average of the areas that were checked, each counted by its weight; an area that was not checked is left out and the rest count for more. With no working way to contact the business at all, the grade is capped at ${SCORING.capNoContact}.`)}</p></div>`;
68184
68652
  }
68185
68653
  function evidenceCard(r) {
68186
68654
  const sites = [r.business, ...r.competitors];
68187
68655
  return sites.map((s) => {
68188
- const rows2 = s.site.log.map((l) => `<tr><td>${esc15(clock(l.at))}</td><td>${esc15(l.what)}</td><td>${esc15(l.url)}${l.finalUrl ? `<br>moved to ${esc15(l.finalUrl)}` : ""}</td><td>${l.status || "none"}</td><td>${l.ms} ms</td><td>${esc15(l.note ?? "")}</td></tr>`).join("");
68656
+ const rows2 = s.site.log.map((l) => `<tr><td>${esc16(clock2(l.at))}</td><td>${esc16(l.what)}</td><td>${esc16(l.url)}${l.finalUrl ? `<br>moved to ${esc16(l.finalUrl)}` : ""}</td><td>${l.status || "none"}</td><td>${l.ms} ms</td><td>${esc16(l.note ?? "")}</td></tr>`).join("");
68189
68657
  const ph = s.site.phone;
68190
- const phoneRow = ph ? `<p class="rp-meta">${ph.ok ? esc15(`Phone view (Chrome at 390 by 844 pixels, a fresh browser with no cookies), ${clock(ph.at)}: ${ph.requests} files loaded from ${ph.hosts.length} addresses; main content ${ph.lcpMs !== void 0 ? `after ${(ph.lcpMs / 1e3).toFixed(1)} seconds` : "time not reported"}; fully loaded ${ph.loadMs !== void 0 ? `after ${(ph.loadMs / 1e3).toFixed(1)} seconds` : "time not reported"}. Measured on an office connection, not a phone network.`) : esc15(`Phone view could not run: ${ph.reason}.`)}</p>` : "";
68658
+ const phoneRow = ph ? `<p class="rp-meta">${ph.ok ? esc16(`Phone view (Chrome at 390 by 844 pixels, a fresh browser with no cookies), ${clock2(ph.at)}: ${ph.requests} files loaded from ${ph.hosts.length} addresses; main content ${ph.lcpMs !== void 0 ? `after ${(ph.lcpMs / 1e3).toFixed(1)} seconds` : "time not reported"}; fully loaded ${ph.loadMs !== void 0 ? `after ${(ph.loadMs / 1e3).toFixed(1)} seconds` : "time not reported"}. Measured on an office connection, not a phone network.`) : esc16(`Phone view could not run: ${ph.reason}.`)}</p>` : "";
68191
68659
  const ps = s.site.pagespeed;
68192
- const psRow = ps ? `<p class="rp-meta">${esc15(ps.ok ? `Google PageSpeed (phone), ${clock(ps.at)}: speed ${ps.performance}/100${ps.seo !== void 0 ? `, search ${ps.seo}/100` : ""}.` : `Google PageSpeed not available: ${ps.reason}.`)}</p>` : "";
68193
- return `<details class="rp-card"><summary>${esc15(s.name)}: ${s.site.log.length} requests</summary><p class="rp-meta">${esc15(s.site.robots.unreadable ? `robots.txt could not be read (${s.site.robots.unreadable}), so the site was treated as closed to visitors.` : `robots.txt ${s.site.robots.found ? "was read and followed" : "was not there (the address answered not found), so nothing is off limits"}${s.site.finalOrigin && s.site.finalOrigin !== s.site.origin ? `, for ${s.site.origin.replace(/^https?:\/\//, "")} and again for ${s.site.finalOrigin.replace(/^https?:\/\//, "")} where it moved` : ""}.${s.site.robots.homeAllowed ? "" : " It asks visitors not to open the home page, so nothing was read."}`)}</p>${phoneRow}${psRow}<div class="rp-scroll"><table class="rp-log"><thead><tr><th>Time</th><th>What</th><th>Address</th><th>Answer</th><th>Took</th><th>Note</th></tr></thead><tbody>${rows2}</tbody></table></div></details>`;
68660
+ const psRow = ps ? `<p class="rp-meta">${esc16(ps.ok ? `Google PageSpeed (phone), ${clock2(ps.at)}: speed ${ps.performance}/100${ps.seo !== void 0 ? `, search ${ps.seo}/100` : ""}.` : `Google PageSpeed not available: ${ps.reason}.`)}</p>` : "";
68661
+ return `<details class="rp-card"><summary>${esc16(s.name)}: ${s.site.log.length} requests</summary><p class="rp-meta">${esc16(s.site.robots.unreadable ? `robots.txt could not be read (${s.site.robots.unreadable}), so the site was treated as closed to visitors.` : `robots.txt ${s.site.robots.found ? "was read and followed" : "was not there (the address answered not found), so nothing is off limits"}${s.site.finalOrigin && s.site.finalOrigin !== s.site.origin ? `, for ${s.site.origin.replace(/^https?:\/\//, "")} and again for ${s.site.finalOrigin.replace(/^https?:\/\//, "")} where it moved` : ""}.${s.site.robots.homeAllowed ? "" : " It asks visitors not to open the home page, so nothing was read."}`)}</p>${phoneRow}${psRow}<div class="rp-scroll"><table class="rp-log"><thead><tr><th>Time</th><th>What</th><th>Address</th><th>Answer</th><th>Took</th><th>Note</th></tr></thead><tbody>${rows2}</tbody></table></div></details>`;
68194
68662
  }).join("");
68195
68663
  }
68196
68664
  function prospectReportHtml(r, opts = {}) {
@@ -68227,21 +68695,21 @@ function prospectReportHtml(r, opts = {}) {
68227
68695
  })()
68228
68696
  ].join("");
68229
68697
  const worst = gaps[0];
68230
- const quote = worst ? callout3(esc15(inAudience(worst.gap, aud)), { quote: true, tone: "bad", cite: worst.evidence }) : callout3(site.reachable ? "Everything that was checked is in place." : esc15(site.unreachableReason ?? "The site could not be opened."), { quote: true, tone: site.reachable ? "info" : "warn" });
68231
- const bars = sc.areas.map((a) => a.score !== void 0 ? `<div class="cds-bar-row"><span class="name">${esc15(a.label)}</span><span class="trk"><i class="fill ${gcls2(a.score)}" style="width:${a.score}%"></i></span><span class="val"><span class="${gcls2(a.score)}">${gradeOf(a.score)}</span> ${a.score}</span></div>` : `<div class="cds-bar-row"><span class="name">${esc15(a.label)}</span><span class="trk"></span><span class="val cds-unk">Not checked</span></div>`).join("");
68698
+ const quote = worst ? callout3(esc16(inAudience(worst.gap, aud)), { quote: true, tone: "bad", cite: worst.evidence }) : callout3(site.reachable ? "Everything that was checked is in place." : esc16(site.unreachableReason ?? "The site could not be opened."), { quote: true, tone: site.reachable ? "info" : "warn" });
68699
+ const bars = sc.areas.map((a) => a.score !== void 0 ? `<div class="cds-bar-row"><span class="name">${esc16(a.label)}</span><span class="trk"><i class="fill ${gcls2(a.score)}" style="width:${a.score}%"></i></span><span class="val"><span class="${gcls2(a.score)}">${gradeOf(a.score)}</span> ${a.score}</span></div>` : `<div class="cds-bar-row"><span class="name">${esc16(a.label)}</span><span class="trk"></span><span class="val cds-unk">Not checked</span></div>`).join("");
68232
68700
  const areasOrder = ["website", "search", "ai", "google", "contact"];
68233
68701
  const cards = areasOrder.map((a) => checksCard(a, b.checks));
68234
68702
  const gbpLinks = site.home ? [...new Set([site.home, ...site.pages].flatMap((p) => p.googleProfileLinks))] : [];
68235
68703
  const mapsEmbed = site.home ? [site.home, ...site.pages].some((p) => p.mapsEmbed) : false;
68236
- const gbpNote = callout3(esc15(`What the website shows about the profile: ${gbpLinks.length ? `${gbpLinks.length === 1 ? "a link" : `${gbpLinks.length} links`} to Google (${list3([...new Set(gbpLinks.map(googleLinkKind))])})` : "no link to a Google profile"}${mapsEmbed ? " and an embedded Google map" : ", no embedded Google map"}. Whether the profile exists, its rating and review count, and whether its hours and phone match the website need a Google Maps key, which is not set up yet, so they are not checked and not scored.`), { tone: "info", title: "Google Business Profile" });
68237
- const aiList = r.aiAnswers.length ? `<div class="rp-card"><h3>What AI said when asked</h3><ul class="rp-list">${r.aiAnswers.map((a) => `<li>${esc15(`"${a.question}" (${a.engine}, ${when2(a.askedAt)}): ${a.namesBusiness ? `named ${name}` : `did not name ${name}`}${a.othersNamed.length ? `; named ${list3([...a.othersNamed.slice(0, 5), ...a.othersNamed.length > 5 ? [`${a.othersNamed.length - 5} more`] : []])}` : ""}.`)}</li>`).join("")}</ul></div>` : "";
68704
+ const gbpNote = callout3(esc16(`What the website shows about the profile: ${gbpLinks.length ? `${gbpLinks.length === 1 ? "a link" : `${gbpLinks.length} links`} to Google (${list3([...new Set(gbpLinks.map(googleLinkKind))])})` : "no link to a Google profile"}${mapsEmbed ? " and an embedded Google map" : ", no embedded Google map"}. Whether the profile exists, its rating and review count, and whether its hours and phone match the website need a Google Maps key, which is not set up yet, so they are not checked and not scored.`), { tone: "info", title: "Google Business Profile" });
68705
+ const aiList = r.aiAnswers.length ? `<div class="rp-card"><h3>What AI said when asked</h3><ul class="rp-list">${r.aiAnswers.map((a) => `<li>${esc16(`"${a.question}" (${a.engine}, ${when2(a.askedAt)}): ${a.namesBusiness ? `named ${name}` : `did not name ${name}`}${a.othersNamed.length ? `; named ${list3([...a.othersNamed.slice(0, 5), ...a.othersNamed.length > 5 ? [`${a.othersNamed.length - 5} more`] : []])}` : ""}.`)}</li>`).join("")}</ul></div>` : "";
68238
68706
  const offer = offerParts(r, opts.packages ?? [], opts.services, aud);
68239
68707
  const fu = r.followUp;
68240
68708
  const sellsFollowUp = !!serviceFor("followup", offeredNames(opts.services));
68241
68709
  const offerLine = fu.form === "none" ? "We'll go through it with you on the walkthrough." : "We'll run it with you on the walkthrough.";
68242
- const followHtml = callout3(`<p class="rp-meta">${esc15(sellsFollowUp ? fu.note.replace(/\s*Not run for this report\.$/, "") : fu.note)}</p>${sellsFollowUp ? `<p><strong>${esc15(offerLine)}</strong></p>${bookButton(brand, opts.prefill)}` : ""}`, { tone: fu.form === "found" ? "info" : "warn", title: "Follow-up test" });
68710
+ const followHtml = callout3(`<p class="rp-meta">${esc16(sellsFollowUp ? fu.note.replace(/\s*Not run for this report\.$/, "") : fu.note)}</p>${sellsFollowUp ? `<p><strong>${esc16(offerLine)}</strong></p>${bookButton(brand, opts.prefill)}` : ""}`, { tone: fu.form === "found" ? "info" : "warn", title: "Follow-up test" });
68243
68711
  const reach = reachLinks(brand);
68244
- const footRight = [reach.length ? `${esc15(brand.name)} \xB7 ${reach.join(" \xB7 ")}` : esc15(brand.name), ...opts.credit ? [esc15(PRODUCT_CREDIT)] : []].join("<br>");
68712
+ const footRight = [reach.length ? `${esc16(brand.name)} \xB7 ${reach.join(" \xB7 ")}` : esc16(brand.name), ...opts.credit ? [esc16(PRODUCT_CREDIT)] : []].join("<br>");
68245
68713
  const notChecked = [
68246
68714
  "the Google Business Profile itself (needs a Google Maps key)",
68247
68715
  "keyword rankings and backlinks (need paid data)",
@@ -68250,33 +68718,33 @@ function prospectReportHtml(r, opts = {}) {
68250
68718
  "how fast they answer a new enquiry (the follow-up add-on)",
68251
68719
  "phone calls, including after hours"
68252
68720
  ];
68253
- return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc15(`Secret Shopper \xB7 ${name}`)}</title>
68721
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc16(`Secret Shopper \xB7 ${name}`)}</title>
68254
68722
  <style>${DS_TOKENS_CSS}</style>
68255
68723
  <style>${DS_BUNDLE_CSS}</style>
68256
68724
  <style>${LAYOUT_CSS2}</style></head><body>
68257
68725
  <div class="rp">
68258
68726
  <div class="rp-tools cds-no-print"><button class="cds-btn" type="button" id="save-pdf">Save as PDF</button></div>
68259
- <header data-section="bar" class="rp-bar"><div class="rp-brand">${esc15(brand.name)} <span>Secret Shopper</span></div>${reach.length ? `<div class="rp-contact">${reach.join(" \xB7 ")}</div>` : ""}</header>
68260
- <section data-section="hero" class="rp-hero"><div class="rp-hero-text"><div class="rp-kicker">${esc15(`We looked at ${name} the way a new ${aud} does`)}</div><h1>${esc15(h1)}</h1><p class="rp-sub">${esc15(sub)}</p><p class="rp-meta">${esc15(meta2)}</p></div><div class="rp-ringbox">${gradeRing2(sc.score, caption)}</div></section>
68727
+ <header data-section="bar" class="rp-bar"><div class="rp-brand">${esc16(brand.name)} <span>Secret Shopper</span></div>${reach.length ? `<div class="rp-contact">${reach.join(" \xB7 ")}</div>` : ""}</header>
68728
+ <section data-section="hero" class="rp-hero"><div class="rp-hero-text"><div class="rp-kicker">${esc16(`We looked at ${name} the way a new ${aud} does`)}</div><h1>${esc16(h1)}</h1><p class="rp-sub">${esc16(sub)}</p><p class="rp-meta">${esc16(meta2)}</p></div><div class="rp-ringbox">${gradeRing2(sc.score, caption)}</div></section>
68261
68729
  <section data-section="tiles" class="rp-band"><div class="rp-tiles">${tiles}</div></section>
68262
68730
  <section data-section="quote" class="rp-band">${quote}</section>
68263
68731
  ${section("brand", "1 \xB7 Branding", "How the brand comes across", "Read from the home page and its stylesheets", brandCard(b))}
68264
- ${section("areas", "2 \xB7 Website check", "Score by area", "Only areas that could be checked are graded", `<div class="rp-card"><div class="cds-bars">${bars}</div><div class="cds-src">${esc15(`From ${r.requests} requests to the public websites and one phone-size view each, ${when2(r.generatedAt)}. Weighted average of the areas checked; "Not checked" is never counted as a fail.`)}</div></div>`)}
68732
+ ${section("areas", "2 \xB7 Website check", "Score by area", "Only areas that could be checked are graded", `<div class="rp-card"><div class="cds-bars">${bars}</div><div class="cds-src">${esc16(`From ${r.requests} requests to the public websites and one phone-size view each, ${when2(r.generatedAt)}. Weighted average of the areas checked; "Not checked" is never counted as a fail.`)}</div></div>`)}
68265
68733
  <section data-section="checks" class="rp-band"><div class="rp-two">${cards.join("")}</div><div class="rp-gap"></div>${speedNote(r) ? `${speedNote(r)}<div class="rp-gap"></div>` : ""}${gbpNote}${aiList ? `<div class="rp-gap"></div>${aiList}` : ""}</section>
68266
68734
  ${section("compare", "3 \xB7 Competitors", r.competitors.length ? `How ${name} compares` : "No competitors were named", r.competitors.length ? `${r.competitors.length} nearby ${r.competitors.length === 1 ? "business" : "businesses"} named by the agency` : "Name up to 3 competitor websites to add this comparison", r.competitors.length ? compareTable(r, { name: brand.name, packages: opts.packages ?? [], sold: opts.services }) : `<div class="rp-card"><p class="rp-meta">This report compares only with sites the agency names. None were named, so no comparison is shown and none is invented.</p></div>`, true)}
68267
68735
  ${section("offer", offer.label, offer.h2, "", offer.inner)}
68268
68736
  ${walkthroughBlock("walkthrough", brand, opts.workflowAudit === true, opts.prefill)}
68269
68737
  ${section("followup", "4 \xB7 Follow-up test", "How fast they answer a new enquiry", "Optional add-on, 1 to 2 days", followHtml)}
68270
68738
  ${section("evidence", "Evidence", "Every request this report made", "Public pages only; nothing was logged into or submitted", evidenceCard(r), true)}
68271
- <section data-section="coverage" class="rp-band">${callout3(esc15(`Not checked in this report: ${list3(notChecked)}.`), { tone: "info", title: "Not checked" })}</section>
68739
+ <section data-section="coverage" class="rp-band">${callout3(esc16(`Not checked in this report: ${list3(notChecked)}.`), { tone: "info", title: "Not checked" })}</section>
68272
68740
  ${section("scoring", "How the score is built", "Every area and check, with its weight", "The same for every business in this report", scoringTable(), true)}
68273
68741
  ${walkthroughBlock("book", brand, opts.workflowAudit === true, opts.prefill)}
68274
- <footer data-section="footer" class="rp-foot"><span>${esc15(`Read from ${name}'s public website${r.competitors.length ? " and the competitors named" : ""} on ${when2(r.generatedAt)}. Nothing was changed, submitted or logged into.`)}</span><span class="rp-who">${footRight}</span></footer>
68742
+ <footer data-section="footer" class="rp-foot"><span>${esc16(`Read from ${name}'s public website${r.competitors.length ? " and the competitors named" : ""} on ${when2(r.generatedAt)}. Nothing was changed, submitted or logged into.`)}</span><span class="rp-who">${footRight}</span></footer>
68275
68743
  </div>
68276
68744
  <script>document.getElementById("save-pdf").addEventListener("click",function(){window.print()});window.addEventListener("beforeprint",function(){document.querySelectorAll("details").forEach(function(d){d.open=true})});</script>
68277
68745
  </body></html>`;
68278
68746
  }
68279
- var esc15, when2, clock, HEX2, gcls2, chip4, VERDICT, statTile3, callout3, section, link2, shortUrl, BOOKING_UTM, isGhlBookingWidget, LAYOUT_CSS2, DEFAULT_PROSPECT_BRANDING, PRODUCT_CREDIT, WALKTHROUGH, COMPARE, featureOf, gapCount, gapWords, AUDIENCES, PATIENT_WORDS, CLIENT_WORDS, inAudience, PATIENT_LINE, gapsOf;
68747
+ var esc16, when2, clock2, HEX2, gcls2, chip4, VERDICT, statTile3, callout3, section, link2, shortUrl, BOOKING_UTM, isGhlBookingWidget, LAYOUT_CSS2, DEFAULT_PROSPECT_BRANDING, PRODUCT_CREDIT, WALKTHROUGH, COMPARE, featureOf, gapCount, gapWords, AUDIENCES, PATIENT_WORDS, CLIENT_WORDS, inAudience, PATIENT_LINE, gapsOf;
68280
68748
  var init_report_page2 = __esm({
68281
68749
  "src/command-os/prospect/report-page.ts"() {
68282
68750
  "use strict";
@@ -68286,19 +68754,19 @@ var init_report_page2 = __esm({
68286
68754
  init_features();
68287
68755
  init_services();
68288
68756
  init_weights();
68289
- esc15 = (s) => s.replace(new RegExp("\\p{Extended_Pictographic}\\uFE0F?", "gu"), "").replace(/\s*\u2014\s*/g, " - ").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
68757
+ esc16 = (s) => s.replace(new RegExp("\\p{Extended_Pictographic}\\uFE0F?", "gu"), "").replace(/\s*\u2014\s*/g, " - ").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
68290
68758
  when2 = (iso2) => new Date(iso2).toLocaleString("en-US", { weekday: "short", month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit", timeZoneName: "short" });
68291
- clock = (iso2) => new Date(iso2).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", second: "2-digit" });
68759
+ clock2 = (iso2) => new Date(iso2).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", second: "2-digit" });
68292
68760
  HEX2 = /^#[0-9a-f]{6}$/i;
68293
68761
  gcls2 = (score2) => `cds-g-${gradeOf(score2).toLowerCase()}`;
68294
- chip4 = (tone, text2) => `<span class="cds-chip ${tone}">${esc15(text2)}</span>`;
68762
+ chip4 = (tone, text2) => `<span class="cds-chip ${tone}">${esc16(text2)}</span>`;
68295
68763
  VERDICT = { pass: ["ok", "Yes"], partial: ["warn", "Partly"], fail: ["bad", "No"], unknown: ["", "Not checked"] };
68296
- statTile3 = (value, label2, tone = "") => `<div class="cds-stat"><div class="big ${tone}">${esc15(String(value))}</div><div class="lbl">${esc15(label2)}</div></div>`;
68297
- callout3 = (inner, o) => `<div class="cds-callout ${o.quote ? "quote" : ""} ${o.tone ?? ""}">${o.title ? `<span class="ttl">${esc15(o.title)}</span>` : ""}${inner}${o.cite ? `<cite>${esc15(o.cite)}</cite>` : ""}</div>`;
68298
- section = (id, label2, h2, right, inner, divider = false) => `<section data-section="${id}" id="${id}" class="rp-section"><div class="rp-head${divider ? " rp-divider" : ""}"><div><div class="rp-label">${esc15(label2)}</div><h2>${esc15(h2)}</h2></div>${right ? `<div class="rp-right">${esc15(right)}</div>` : ""}</div>${inner}</section>`;
68764
+ statTile3 = (value, label2, tone = "") => `<div class="cds-stat"><div class="big ${tone}">${esc16(String(value))}</div><div class="lbl">${esc16(label2)}</div></div>`;
68765
+ callout3 = (inner, o) => `<div class="cds-callout ${o.quote ? "quote" : ""} ${o.tone ?? ""}">${o.title ? `<span class="ttl">${esc16(o.title)}</span>` : ""}${inner}${o.cite ? `<cite>${esc16(o.cite)}</cite>` : ""}</div>`;
68766
+ section = (id, label2, h2, right, inner, divider = false) => `<section data-section="${id}" id="${id}" class="rp-section"><div class="rp-head${divider ? " rp-divider" : ""}"><div><div class="rp-label">${esc16(label2)}</div><h2>${esc16(h2)}</h2></div>${right ? `<div class="rp-right">${esc16(right)}</div>` : ""}</div>${inner}</section>`;
68299
68767
  link2 = (href, text2, cls = "") => {
68300
68768
  const h = safeHref(href);
68301
- return h ? `<a${cls ? ` class="${cls}"` : ""} href="${esc15(h)}">${esc15(text2)}</a>` : esc15(text2);
68769
+ return h ? `<a${cls ? ` class="${cls}"` : ""} href="${esc16(h)}">${esc16(text2)}</a>` : esc16(text2);
68302
68770
  };
68303
68771
  shortUrl = (u) => u.replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
68304
68772
  BOOKING_UTM = "utm_source=secret-shopper";
@@ -69065,6 +69533,8 @@ function compactExecute(full) {
69065
69533
  published: full.published,
69066
69534
  publishFailed: full.publishFailed,
69067
69535
  nextManualSteps: full.nextManualSteps,
69536
+ // The account's custom values after the build (BUG-2026-09-30-11): the only source for "empty".
69537
+ ...full.customValues ? { customValues: full.customValues, customValuesNote: full.customValuesNote } : {},
69068
69538
  ...full.journalWarning ? { journalWarning: full.journalWarning } : {},
69069
69539
  ...composed ? { composed: { presetId: composed.presetId, presetVersion: composed.presetVersion, note: composed.note, warnings: composed.warnings, scope: composed.scope, scopeSource: composed.scopeSource } } : {},
69070
69540
  // A second client in a shared sub-account: what was scoped and who else lives here.
@@ -69284,7 +69754,7 @@ var init_build_reply = __esm({
69284
69754
  { name: "composed.warnings", apply: (r) => shrinkWarnings(r) }
69285
69755
  ];
69286
69756
  CARRIES = {
69287
- execute: "summary, countsByType, notClean, calendars, missingSteps, workflowsNotBuilt, staffPause, recipients, templates, publishFailed and nextManualSteps",
69757
+ execute: "summary, countsByType, notClean, calendars, missingSteps, workflowsNotBuilt, staffPause, recipients, templates, publishFailed, nextManualSteps and customValues",
69288
69758
  dry_run: "summary, validation, countsByType, calendars, workflows, handoffs and composed.copySlots"
69289
69759
  };
69290
69760
  }
@@ -70199,7 +70669,7 @@ async function executeBackbone(plan, deps, opts = {}) {
70199
70669
  }
70200
70670
  if (matches.length === 1) {
70201
70671
  idMap[cal.ref] = matches[0].id;
70202
- const mismatch = calendarSlotMismatch(cal, matches[0]);
70672
+ const mismatch = [calendarSlotMismatch(cal, matches[0]), calendarHoursMismatch(cal, matches[0])].filter(Boolean).join(" ") || void 0;
70203
70673
  const boundExpected = resolveExpectedTeam(cal);
70204
70674
  let boundSeen = matches[0].teamMemberIds;
70205
70675
  let teamReadError2;
@@ -71289,6 +71759,7 @@ function pickCalendars(raw) {
71289
71759
  if (typeof o.slotDuration === "number") rec2.slotDuration = o.slotDuration;
71290
71760
  else if (typeof o.slotDuration === "string" && /^\d+$/.test(o.slotDuration)) rec2.slotDuration = Number(o.slotDuration);
71291
71761
  if (typeof o.slotDurationUnit === "string") rec2.slotDurationUnit = o.slotDurationUnit;
71762
+ if (Array.isArray(o.openHours) && o.openHours.length) rec2.openHours = o.openHours;
71292
71763
  if (Array.isArray(o.teamMembers)) {
71293
71764
  rec2.teamMemberIds = o.teamMembers.map((tm) => tm && typeof tm === "object" ? tm.userId : void 0).filter((v) => typeof v === "string" && v.length > 0);
71294
71765
  }
@@ -72396,10 +72867,19 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
72396
72867
  throw raw;
72397
72868
  }
72398
72869
  progressSink?.finish(exec);
72870
+ let accountCustomValues;
72871
+ try {
72872
+ const raw = await client.get(`/locations/${activeLocation}/customValues`);
72873
+ const hasList = Array.isArray(raw) || !!raw && typeof raw === "object" && Array.isArray(raw.customValues);
72874
+ accountCustomValues = hasList ? pickCustomValues(raw) : void 0;
72875
+ } catch {
72876
+ accountCustomValues = void 0;
72877
+ }
72878
+ const customValueStatus = customValueStatusOf(typedPlan.customValues ?? [], accountCustomValues);
72399
72879
  let savedPlanPath;
72400
72880
  const publishRecord = savedPublishRecord(saved, exec, typedPlan, publishWorkflows ?? false, gatedWorkflowRefs);
72401
72881
  try {
72402
- savedPlanPath = savePlanRecord({ ...saved, locationId: activeLocation, ...clientFields, planId: typedPlan.planId, savedAt: (/* @__PURE__ */ new Date()).toISOString(), plan: typedPlan, inputHash: process.env.GHL_COMMAND_INTAKE_HASH || saved?.inputHash, contentHash: contentHash(typedPlan), dependencies: planDependencies(typedPlan), preview: result, idMap: exec.idMap, built: exec.built, manual: exec.manual, calendarPending: exec.calendarPending, scope: scopeForValidation, heldWorkflows, ...publishRecord });
72882
+ savedPlanPath = savePlanRecord({ ...saved, locationId: activeLocation, ...clientFields, planId: typedPlan.planId, savedAt: (/* @__PURE__ */ new Date()).toISOString(), plan: typedPlan, inputHash: process.env.GHL_COMMAND_INTAKE_HASH || saved?.inputHash, contentHash: contentHash(typedPlan), dependencies: planDependencies(typedPlan), preview: result, idMap: exec.idMap, built: exec.built, manual: exec.manual, calendarPending: exec.calendarPending, scope: scopeForValidation, heldWorkflows, customValueStatus, ...publishRecord });
72403
72883
  } catch {
72404
72884
  }
72405
72885
  let journalPathOut;
@@ -72430,7 +72910,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
72430
72910
  ...w.manual.map((m) => `[${w.name}] ${m.reason}`),
72431
72911
  ...w.needsContent.map((c) => `[${w.name}] ${c.reason}`)
72432
72912
  ]);
72433
- const handoffLines = result.handoffs.filter((h) => !h.met).map((h) => `[${h.owner}] ${h.title}: ${h.instruction}`);
72913
+ const handoffLines = result.handoffs.filter((h) => !h.met).map((h) => `[${h.owner}] ${h.title}: ${h.instruction}${handoffCustomValueNote(h.ref, typedPlan.customValues ?? [], customValueStatus)}`);
72434
72914
  return buildReplyResponse({
72435
72915
  ok: exec.ok,
72436
72916
  mode: "execute",
@@ -72498,6 +72978,8 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
72498
72978
  deferred: exec.deferred,
72499
72979
  deferredNote: "GHL Command builds staff users, CRM objects, calendars, forms, message templates, and draft workflows. Pending staff and calendar setup appear as manual tasks. Review each workflow in GoHighLevel under Automation > Workflows before switching it on.",
72500
72980
  nextManualSteps: [...execManualLines, ...manualLines, ...handoffLines],
72981
+ customValues: customValueStatus,
72982
+ customValuesNote: customValueStatus.unreadable ? "The account's custom values could not be read after the build, so whether any is empty is unknown: say that, and name none as empty." : "Read back from the account after this build. A custom value is empty only when it is listed under empty here; never take it from a hand-off's instruction, which lists every value the hand-off covers.",
72501
72983
  summary: exec.ok ? `Built ${exec.built.filter((b) => b.status === "created").length} new object(s), bound ${exec.built.filter((b) => b.status === "existing" || b.status === "refreshed").length} existing${exec.refreshed.length ? ` (${exec.refreshed.length} workflow(s) refreshed in place \u2014 see refreshed[])` : ""}${exec.built.some((b) => b.status === "uncertain") ? `, ${exec.built.filter((b) => b.status === "uncertain").length} to check (found after a failed create)` : ""}.${exec.staff.length ? ` Staff: ${exec.staff.filter((s) => s.status === "created").length} created, ${exec.staff.filter((s) => s.status === "existing").length} already in the account, ${exec.staff.filter((s) => s.status === "pending").length} pending.${exec.staff.some((s) => s.status === "uncertain") ? ` ${exec.staff.filter((s) => s.status === "uncertain").length} created or found after a failed create; left in place; check it by hand.` : ""}` : ""}${exec.templates.emails.length + exec.templates.sms.length ? ` Templates: ${exec.templates.emails.length} email (${exec.templates.emails.filter((t) => t.status === "created" && !t.warning).length} created with the body verified in the preview${exec.templatesUnverified ? `, ${exec.templatesUnverified} created but NOT verified \u2014 see templates.emails[].warning` : ""}${exec.templates.emails.filter((t) => t.status === "existing").length ? `, ${exec.templates.emails.filter((t) => t.status === "existing").length} already existed` : ""}${exec.templates.emails.filter((t) => t.status === "failed").length ? `, ${exec.templates.emails.filter((t) => t.status === "failed").length} failed` : ""}), ${exec.templates.sms.length} SMS.` : ""}${publishSummaryClause({ published: exec.published, publishFailed: exec.publishFailed, anyWorkflowCreated: exec.built.some((b) => b.type === "workflow" && b.status === "created") })}${exec.manual.some((m) => m.type === "calendar") || exec.calendarPending.length ? ` Calendars: ${exec.manual.filter((m) => m.type === "calendar").length} deferred to manual; ${new Set(exec.calendarPending.filter((c) => c.status === "pending").map((c) => c.workflowRef)).size} workflow(s) built with the calendar unpinned${exec.calendarPending.some((c) => c.status === "calendar_ready") ? `, ${new Set(exec.calendarPending.filter((c) => c.status === "calendar_ready").map((c) => c.workflowRef)).size} ready to pin` : ""}.` : ""}${exec.staffPause ? ` STAFF PAUSE: ${exec.staffPause.reason}` : ""}${exec.manual.some((m) => m.type === "calendar_team") ? ` ${exec.manual.filter((m) => m.type === "calendar_team").length} calendar(s) were built WITHOUT the people the plan named \u2014 see nextManualSteps; do not report those calendars as staffed.` : ""}${exec.missingSteps.length ? ` NOT COMPLETE: ${exec.missingSteps.reduce((n, w) => n + w.steps.length, 0)} step(s) the plan asks for are still missing from ${exec.missingSteps.length} workflow(s) that already existed \u2014 add them by hand (see missingSteps).` : ""}${exec.workflowsNotBuilt.length ? ` NOT COMPLETE: ${exec.workflowsNotBuilt.length} workflow(s) were not finished this run because of a passing GoHighLevel error (${exec.workflowsNotBuilt.map((w) => w.name).join(", ")}); everything else was built. Run Build again to finish them (see workflowsNotBuilt).` : ""}${exec.manual.length ? ` ${exec.manual.length} item(s) need a manual step (see nextManualSteps).` : ""}${exec.deferred.length ? " Deferred: " + exec.deferred.map((d) => `${d.count} ${d.section}`).join(", ") + " (manual)." : ""}` : `HALTED at ${exec.halted?.atRef} (${exec.halted?.reason}). ${exec.built.length} object(s) were created before the halt \u2014 see idMap to resume or clean up. NEVER-CLOBBER means a re-run will bind those, not duplicate them.`
72502
72984
  }, "execute", activeLocation);
72503
72985
  }
@@ -73029,6 +73511,7 @@ var init_intake_to_build = __esm({
73029
73511
  init_features();
73030
73512
  init_executor();
73031
73513
  init_execute();
73514
+ init_build_facts();
73032
73515
  customFieldItemSchema = import_zod93.z.object({
73033
73516
  id: import_zod93.z.string(),
73034
73517
  name: import_zod93.z.string(),