@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/CHANGELOG.md +15 -1
- package/README.md +1 -1
- package/dist/index.js +685 -202
- package/package.json +1 -1
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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}` : ""}${
|
|
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
|
-
|
|
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.
|
|
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
|
|
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 ${
|
|
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
|
|
37497
|
+
const clock3 = deps.now ?? Date.now;
|
|
37429
37498
|
const p = previews.get(hash);
|
|
37430
|
-
if (!p || p.expires <
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
38097
|
-
return
|
|
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() &&
|
|
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
|
|
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
|
|
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
|
|
41010
|
-
for (const piece of
|
|
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
|
|
41025
|
-
return
|
|
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.
|
|
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="${
|
|
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)}>${
|
|
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="${
|
|
46950
|
-
<summary class="ap-sum"><span class="ttl cn">${
|
|
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="${
|
|
46955
|
-
<div class="cds-field"><label>They pay</label><input class="cds-input f" data-k="price" inputmode="numeric" value="${
|
|
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="${
|
|
46958
|
-
<div class="cds-field"><label>Renews</label><input class="cds-input f" data-k="renewsOn" placeholder="2027-01-15" value="${
|
|
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="${
|
|
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">${
|
|
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="${
|
|
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>${
|
|
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>${
|
|
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): ${
|
|
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,
|
|
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
|
-
|
|
47744
|
+
esc12 = (x) => String(x ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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">${
|
|
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>${
|
|
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">${
|
|
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>${
|
|
49684
|
-
<div class="cds-src">${
|
|
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 ${
|
|
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">${
|
|
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="${
|
|
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="${
|
|
49766
|
-
const sw = (label2, t) => `<tr><td><span class="bp-sw">${swatch(t.hex, label2)}</span></td><th scope="row">${
|
|
49767
|
-
const face = (label2, f) => `<tr><th scope="row">${
|
|
49768
|
-
const pairs = d.contrast.pairs.map((p) => `<tr><td class="cds-num">${
|
|
49769
|
-
const samples = d.voice.samples.map((s) => `<li><span class="ttl">${
|
|
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="${
|
|
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 ?
|
|
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 ${
|
|
49778
|
-
${d.pageStudioTheme.warnings.length ? `<h3>Before this reaches a client</h3><ul>${d.pageStudioTheme.warnings.map((w) => `<li>${
|
|
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>${
|
|
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 ${
|
|
49790
|
-
${d.voice.tone ? `<p>Tone: ${
|
|
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">${
|
|
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">${
|
|
49797
|
-
${d.provenance.source ? `<p>Source: ${
|
|
49798
|
-
${opts.dir ? `<p>Stored at ${
|
|
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 ${
|
|
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 ${
|
|
49803
|
-
<div class="cds-row" id="verdict"><span class="cds-chip ${v.tone}">${v.word}</span><p>${
|
|
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>${
|
|
49807
|
-
<dt>Logo</dt><dd><div class="bp-logo" data-ink="${
|
|
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 ${
|
|
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>${
|
|
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="${
|
|
49831
|
-
<div class="cds-field"><label for="displayFont">Heading font</label><input class="cds-input" id="displayFont" type="text" placeholder="${
|
|
49832
|
-
<div class="cds-field"><label for="bodyFont">Text font</label><input class="cds-input" id="bodyFont" type="text" placeholder="${
|
|
49833
|
-
<div class="cds-field"><label for="brandName">Brand name</label><input class="cds-input" id="brandName" type="text" placeholder="${
|
|
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="<svg \u2026>\u2026</svg>" 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
|
|
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
|
-
|
|
50346
|
+
esc13 = (s) => String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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="${
|
|
49953
|
-
brokenFontsNote = (display, body) => `<p>Fonts as read from the site: headings "${
|
|
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
|
|
50599
|
+
const esc17 = (v) => String(v ?? "").replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" })[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">${
|
|
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>${
|
|
50176
|
-
return `<!doctype html><html><head><meta charset="utf-8"><title>${
|
|
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>${
|
|
50212
|
-
<p class="sub">${agency ? `Prepared by ${
|
|
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">${
|
|
50215
|
-
${review.headline ? `<div class="lead">${
|
|
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>${
|
|
50218
|
-
${review.ica.painPoints?.length ? `<p class="kv"><b>What keeps them up:</b> ${
|
|
50219
|
-
${review.ica.objections?.length ? `<p class="kv"><b>Why they hesitate:</b> ${
|
|
50220
|
-
${review.ica.buyingTrigger ? `<p class="kv"><b>What makes them finally buy:</b> ${
|
|
50221
|
-
${review.ica.whereTheyAre ? `<p class="kv"><b>Where they already are:</b> ${
|
|
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> ${
|
|
50225
|
-
${o.believability ? `<p class="kv"><b>Believable:</b> ${
|
|
50226
|
-
${o.speed ? `<p class="kv"><b>Speed to result:</b> ${
|
|
50227
|
-
${o.effort ? `<p class="kv"><b>Effort asked of them:</b> ${
|
|
50228
|
-
${o.biggestLever ? `<p class="kv"><b>Biggest lever:</b> ${
|
|
50229
|
-
${o.recommendedOffer ? `<div class="lead"><b>Run this instead:</b> ${
|
|
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>${
|
|
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>${
|
|
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>${
|
|
50239
|
-
${opts.bought.confirmed ? opts.bought.bought.map((b) => `<div class="build"><b>${
|
|
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>${
|
|
50241
|
-
${review.unknowns?.length ? `<div class="head">What we still need from you</div><div class="ask"><ul>${review.unknowns.map((u) => `<li>${
|
|
50242
|
-
${opts.assessment ? `<div class="head">Assessment</div><p>Snapshot attached on ${
|
|
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 ?
|
|
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
|
|
50800
|
+
function esc14(s) {
|
|
50365
50801
|
return String(s === void 0 || s === null ? "" : s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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 =
|
|
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 ' +
|
|
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 ?
|
|
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">' +
|
|
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(
|
|
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>' +
|
|
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>' +
|
|
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>" +
|
|
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">' +
|
|
50433
|
-
out.push("<tr><td><strong>" +
|
|
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">' +
|
|
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">' +
|
|
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>" +
|
|
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>" +
|
|
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>" +
|
|
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>" +
|
|
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">' +
|
|
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(
|
|
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 " +
|
|
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>" +
|
|
50957
|
+
return "<footer>" + esc14(agency.name) + (agency.contact ? " · " + 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
|
|
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)
|
|
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
|
|
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
|
-
|
|
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">${
|
|
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 ${
|
|
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="${
|
|
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>${
|
|
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> ${
|
|
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="${
|
|
62990
|
-
<footer>Ruleset ${
|
|
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
|
|
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
|
-
|
|
63467
|
+
esc15 = (s) => String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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. ${
|
|
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. ${
|
|
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">${
|
|
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>${
|
|
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>${
|
|
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">${
|
|
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>${
|
|
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>${
|
|
68077
|
-
<dt>Logo</dt><dd>${b.logo ?
|
|
68078
|
-
<dt>Browser-tab icon</dt><dd>${b.favicon ?
|
|
68079
|
-
<dt>Share image</dt><dd>${b.shareImage ?
|
|
68080
|
-
${b.headlines.length ? `<dt>Headlines</dt><dd>${b.headlines.map((h) => `"${
|
|
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>${
|
|
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">${
|
|
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>${
|
|
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)}>${
|
|
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">${
|
|
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." : ""}${
|
|
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">${
|
|
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>${
|
|
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(
|
|
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">${
|
|
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">${
|
|
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>${
|
|
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 ?
|
|
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">${
|
|
68193
|
-
return `<details class="rp-card"><summary>${
|
|
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(
|
|
68231
|
-
const bars = sc.areas.map((a) => a.score !== void 0 ? `<div class="cds-bar-row"><span class="name">${
|
|
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(
|
|
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>${
|
|
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">${
|
|
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 ? `${
|
|
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>${
|
|
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">${
|
|
68260
|
-
<section data-section="hero" class="rp-hero"><div class="rp-hero-text"><div class="rp-kicker">${
|
|
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">${
|
|
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(
|
|
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>${
|
|
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
|
|
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
|
-
|
|
68757
|
+
esc16 = (s) => s.replace(new RegExp("\\p{Extended_Pictographic}\\uFE0F?", "gu"), "").replace(/\s*\u2014\s*/g, " - ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
-
|
|
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}">${
|
|
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}">${
|
|
68297
|
-
callout3 = (inner, o) => `<div class="cds-callout ${o.quote ? "quote" : ""} ${o.tone ?? ""}">${o.title ? `<span class="ttl">${
|
|
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">${
|
|
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="${
|
|
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
|
|
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(),
|