@elitedcs/ghl-mcp 3.89.0-beta.5 → 3.89.0-beta.7
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 +39 -1
- package/README.md +1 -1
- package/dist/index.js +969 -267
- 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}` });
|
|
@@ -30122,7 +30165,7 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
|
|
|
30122
30165
|
});
|
|
30123
30166
|
});
|
|
30124
30167
|
}
|
|
30125
|
-
var import_child_process3, fs21, STAFF_NOTIFICATION_RULE, STAFF_PAUSE_RULE, BUILD_REPLY_RULE, MISSING_STEP_RULE, NO_INVENTED_CAUSE_RULE, STAGE_SPECS, HUMAN_GATE_STAGES, RESULT_LAST_RULE;
|
|
30168
|
+
var import_child_process3, fs21, BRIEF_NEEDS_INTAKE, STAFF_NOTIFICATION_RULE, STAFF_PAUSE_RULE, BUILD_REPLY_RULE, MISSING_STEP_RULE, NO_INVENTED_CAUSE_RULE, STAGE_SPECS, HUMAN_GATE_STAGES, RESULT_LAST_RULE;
|
|
30126
30169
|
var init_stage_runner = __esm({
|
|
30127
30170
|
"src/stage-runner.ts"() {
|
|
30128
30171
|
"use strict";
|
|
@@ -30138,13 +30181,15 @@ 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();
|
|
30189
|
+
BRIEF_NEEDS_INTAKE = "First complete the intake. It fills most answers from a call or your notes.";
|
|
30145
30190
|
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
30191
|
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.`;
|
|
30192
|
+
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
30193
|
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
30194
|
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
30195
|
STAGE_SPECS = {
|
|
@@ -30219,10 +30264,10 @@ If anything fails, finish with: RESULT: {"ok":false,"error":"<short reason>"}`
|
|
|
30219
30264
|
2. Find the answers. They arrive one of TWO ways and you must check BOTH:
|
|
30220
30265
|
(a) a real form submission \u2014 get_forms then get_form_submissions_full; or
|
|
30221
30266
|
(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.
|
|
30267
|
+
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
30268
|
Use these authoritative normalized intake answers when provided: ${JSON.stringify(ctx?.normalizedAnswers ?? {})}.
|
|
30224
30269
|
Do NOT create, update or delete anything \u2014 this step only reads and normalizes.
|
|
30225
|
-
Only if BOTH paths come up empty, finish with RESULT: {"ok":false,"error"
|
|
30270
|
+
Only if BOTH paths come up empty, finish with RESULT: {"ok":false,"error":${JSON.stringify(BRIEF_NEEDS_INTAKE)}}.
|
|
30226
30271
|
${NO_INVENTED_CAUSE_RULE}
|
|
30227
30272
|
Finish with EXACTLY one final line:
|
|
30228
30273
|
RESULT: {"ok":true,"summary":"<one sentence: who the client is and what they want built>","issues":["<validate_brief gaps, if any>"]}`
|
|
@@ -30266,13 +30311,14 @@ RESULT: {"ok":true,"summary":"<one sentence: who the client is and what they wan
|
|
|
30266
30311
|
"mcp__ghl__get_contact",
|
|
30267
30312
|
"mcp__ghl__get_custom_fields"
|
|
30268
30313
|
],
|
|
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.
|
|
30314
|
+
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
30315
|
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
30316
|
1. switch_location to ${locationId2} (${locationName}); verify with get_current_location and stop if it is not that location.
|
|
30272
30317
|
2. Rebuild the Brief. The answers arrive one of TWO ways and you must check BOTH:
|
|
30273
30318
|
(a) a real form submission \u2014 get_forms then get_form_submissions_full, then normalize_submission_to_brief; or
|
|
30274
30319
|
(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
30320
|
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.
|
|
30321
|
+
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
30322
|
Use these authoritative normalized intake answers when provided: ${JSON.stringify(ctx?.normalizedAnswers ?? {})}.
|
|
30277
30323
|
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
30324
|
${BUILD_REPLY_RULE}
|
|
@@ -30283,6 +30329,7 @@ Use these authoritative normalized intake answers when provided: ${JSON.stringif
|
|
|
30283
30329
|
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
30330
|
9. ${MISSING_STEP_RULE} ${STAFF_PAUSE_RULE}
|
|
30285
30331
|
10. ${NO_INVENTED_CAUSE_RULE}
|
|
30332
|
+
11. ${buildFactsRule(ctx)}
|
|
30286
30333
|
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
30334
|
Finish with EXACTLY one final line:
|
|
30288
30335
|
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 +31636,7 @@ function uniqueVerifyJobs(jobs2, notificationCount, owned = 0) {
|
|
|
31589
31636
|
function renderVerifyReport(records, notes) {
|
|
31590
31637
|
const { rows: rows2, allowPublishing } = verifyReadback(records);
|
|
31591
31638
|
const normal2 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
31592
|
-
|
|
31639
|
+
const removedClaims = /* @__PURE__ */ new Set();
|
|
31593
31640
|
const aliases2 = (r) => [
|
|
31594
31641
|
r.name,
|
|
31595
31642
|
r.ref,
|
|
@@ -31601,12 +31648,12 @@ function renderVerifyReport(records, notes) {
|
|
|
31601
31648
|
...r.type === "workflow" ? ["all workflows"] : []
|
|
31602
31649
|
].map(normal2).filter(Boolean);
|
|
31603
31650
|
const names = [...new Set(rows2.flatMap(aliases2))].sort((a, b) => b.length - a.length);
|
|
31604
|
-
const
|
|
31651
|
+
const mentionPattern2 = names.length ? new RegExp(`\\b(?:${names.join("|")})\\b`, "g") : void 0;
|
|
31605
31652
|
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
31653
|
const judge = (sentence) => {
|
|
31607
31654
|
const mentions = [];
|
|
31608
31655
|
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 =
|
|
31656
|
+
const tokenized = mentionPattern2 ? text2.replace(mentionPattern2, (name) => {
|
|
31610
31657
|
mentions.push(rows2.filter((r) => aliases2(r).includes(name)));
|
|
31611
31658
|
return `OBJECT${mentions.length - 1}`;
|
|
31612
31659
|
}) : text2;
|
|
@@ -31653,7 +31700,7 @@ function renderVerifyReport(records, notes) {
|
|
|
31653
31700
|
const filter = (text2) => sentences(text2).filter((sentence) => {
|
|
31654
31701
|
if (/could not read:|unchecked:/i.test(sentence)) return false;
|
|
31655
31702
|
if (judge(sentence).contradicted || !allowPublishing && /\b(everything|all (?:planned )?objects|the account)\b.*\b(ready|complete|working|built)\b/i.test(sentence)) {
|
|
31656
|
-
|
|
31703
|
+
removedClaims.add(normal2(sentence));
|
|
31657
31704
|
return false;
|
|
31658
31705
|
}
|
|
31659
31706
|
return allowPublishing || !publishing.test(sentence);
|
|
@@ -31715,14 +31762,54 @@ function renderVerifyReport(records, notes) {
|
|
|
31715
31762
|
const summary = [
|
|
31716
31763
|
`${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
31764
|
${drafts}` : ""}${liveLine ? `
|
|
31718
|
-
${liveLine}` : ""}
|
|
31765
|
+
${liveLine}` : ""}`,
|
|
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
|
+
// Test 01 (BUG-2026-09-30-36): the left-out note is a quiet line in the notes, after the lists, not in the headline.
|
|
31769
|
+
...summaryNotes.length || removedClaims.size ? [["Notes from the check", ...summaryNotes, ...removedClaims.size ? [removedClaimNote(removedClaims.size)] : []].join("\n")] : []
|
|
31722
31770
|
].join("\n\n");
|
|
31723
|
-
|
|
31771
|
+
const cvNames = customValueDisplayNames(rows2, records.deps);
|
|
31772
|
+
return { summary: plainCustomValueNames(summary, cvNames), issues: issues.map((i) => plainCustomValueNames(i, cvNames)) };
|
|
31773
|
+
}
|
|
31774
|
+
function removedClaimNote(count2) {
|
|
31775
|
+
return `${count2 === 1 ? "One check note did not match the account and is not shown" : `${count2} check notes did not match the account and are not shown`}. The lists above are what the account holds.`;
|
|
31776
|
+
}
|
|
31777
|
+
function quietRemovedClaimLine(summary) {
|
|
31778
|
+
if (!summary) return summary;
|
|
31779
|
+
const blocks = summary.split("\n\n");
|
|
31780
|
+
const head4 = blocks[0].split("\n");
|
|
31781
|
+
const old = /^(One thing the check reported was|(\d+) things the check reported were) left out\b/;
|
|
31782
|
+
const at = head4.findIndex((l) => old.test(l.trim()));
|
|
31783
|
+
if (at < 0) return summary;
|
|
31784
|
+
const m = old.exec(head4[at].trim());
|
|
31785
|
+
const note = removedClaimNote(m[2] ? Number(m[2]) : 1);
|
|
31786
|
+
head4.splice(at, 1);
|
|
31787
|
+
blocks[0] = head4.join("\n");
|
|
31788
|
+
const notesAt = blocks.findIndex((b) => b.startsWith("Notes from the check"));
|
|
31789
|
+
if (notesAt >= 0) blocks[notesAt] = `${blocks[notesAt]}
|
|
31790
|
+
${note}`;
|
|
31791
|
+
else blocks.push(`Notes from the check
|
|
31792
|
+
${note}`);
|
|
31793
|
+
return blocks.join("\n\n");
|
|
31794
|
+
}
|
|
31795
|
+
function customValueDisplayName(key) {
|
|
31796
|
+
const words2 = key.trim().replace(/^cv\./, "").replace(/[_\s]+/g, " ").trim();
|
|
31797
|
+
return words2 ? words2.charAt(0).toUpperCase() + words2.slice(1) : key;
|
|
31798
|
+
}
|
|
31799
|
+
function customValueDisplayNames(rows2, deps = []) {
|
|
31800
|
+
const keys = /* @__PURE__ */ new Set();
|
|
31801
|
+
for (const r of [...rows2, ...deps]) {
|
|
31802
|
+
if (typeof r.ref === "string" && r.ref.startsWith("cv.")) keys.add(r.ref.slice(3));
|
|
31803
|
+
if (r.type === "custom_value" && typeof r.name === "string") keys.add(r.name.trim());
|
|
31804
|
+
}
|
|
31805
|
+
return new Map([...keys].filter((k) => /^[a-z0-9]+(?:_[a-z0-9]+)+$/.test(k)).map((k) => [k, customValueDisplayName(k)]));
|
|
31806
|
+
}
|
|
31807
|
+
function plainCustomValueNames(text2, names) {
|
|
31808
|
+
if (!names.size) return text2;
|
|
31809
|
+
const keys = [...names.keys()].sort((a, b) => b.length - a.length);
|
|
31810
|
+
return text2.replace(new RegExp(`(?<![\\w.{(])(?:${keys.join("|")})(?![\\w})])`, "g"), (k) => names.has(k) ? `${names.get(k)} (${k})` : k);
|
|
31724
31811
|
}
|
|
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;
|
|
31812
|
+
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
31813
|
var init_member_progress = __esm({
|
|
31727
31814
|
"src/member-progress.ts"() {
|
|
31728
31815
|
"use strict";
|
|
@@ -31765,6 +31852,7 @@ var init_member_progress = __esm({
|
|
|
31765
31852
|
jobText = (text2) => text2.trim().replace(/\s+/g, " ").replace(/([^.!?])$/, "$1.");
|
|
31766
31853
|
countWord = (text2) => /^\d+$/.test(text2) ? Number(text2) : ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve"].indexOf(text2.toLowerCase());
|
|
31767
31854
|
normalJob = (text2) => text2.toLowerCase().replace(/^manual action:\s*/, "").replace(/[^a-z0-9]+/g, " ").trim();
|
|
31855
|
+
REMOVED_CLAIM_LINE = /^(?:One check note did not match|\d+ check notes did not match) the account\b|^(?:One thing the check reported was|\d+ things the check reported were) left out\b|^Removed a claim\b/i;
|
|
31768
31856
|
}
|
|
31769
31857
|
});
|
|
31770
31858
|
|
|
@@ -34015,7 +34103,7 @@ var require_package = __commonJS({
|
|
|
34015
34103
|
"package.json"(exports2, module2) {
|
|
34016
34104
|
module2.exports = {
|
|
34017
34105
|
name: "@elitedcs/ghl-mcp",
|
|
34018
|
-
version: "3.89.0-beta.
|
|
34106
|
+
version: "3.89.0-beta.7",
|
|
34019
34107
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
34020
34108
|
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
34109
|
main: "dist/index.js",
|
|
@@ -34248,7 +34336,7 @@ ${cockpitSidebarHtml(active, opts.capability)}
|
|
|
34248
34336
|
<a class="cds-menu-item" role="menuitem" href="/connections">Connections</a>
|
|
34249
34337
|
<a class="cds-menu-item" role="menuitem" href="${HUB_URL}#do">Members Hub</a>
|
|
34250
34338
|
<div class="cds-menu-sep" role="separator"></div>
|
|
34251
|
-
<div class="cds-menu-row"><span>Appearance</span><div class="cds-seg" role="group" aria-label="
|
|
34339
|
+
<div class="cds-menu-row"><span>Appearance</span><div class="cds-seg" role="group" aria-label="Color theme"><button type="button" data-theme-choice="auto" aria-pressed="true">Auto</button><button type="button" data-theme-choice="light" aria-pressed="false">Light</button><button type="button" data-theme-choice="dark" aria-pressed="false">Dark</button></div></div>
|
|
34252
34340
|
</div>
|
|
34253
34341
|
</details>
|
|
34254
34342
|
</div>
|
|
@@ -36440,7 +36528,7 @@ function reportDocumentHtml(run, card, opts) {
|
|
|
36440
36528
|
const failed = stepProblems.filter((p) => p.kind === "failed_holding" || p.kind === "failed_gave_up").length;
|
|
36441
36529
|
const stepsKnown = !!st?.read && allSteps.length > 0;
|
|
36442
36530
|
const period = `${localTime2(run.startedAt)}${run.finishedAt ? ` to ${localTime2(run.finishedAt)}` : ""}`;
|
|
36443
|
-
const
|
|
36531
|
+
const clock3 = (ms) => new Date(ms).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
|
|
36444
36532
|
const tiles = [
|
|
36445
36533
|
stepsKnown ? statTile2(judged, "Steps checked") : statTile2("\u2013", "Steps checked", "warn"),
|
|
36446
36534
|
statTile2(f.sent, "Messages sent"),
|
|
@@ -36452,7 +36540,7 @@ function reportDocumentHtml(run, card, opts) {
|
|
|
36452
36540
|
].join("");
|
|
36453
36541
|
const worst = f.problems[0];
|
|
36454
36542
|
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 ${
|
|
36543
|
+
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
36544
|
const stepTitle = (p) => POINTS[p.kind].what;
|
|
36457
36545
|
const stepCard = (p, controls) => fixCard(stepTitle(p), POINTS[p.kind].points, p.detail, esc4(`"${p.stepName}" in ${p.workflowName}`), p.fix, controls);
|
|
36458
36546
|
const DRAFT_WHY = "Publishing starts sending to every real lead, so publish it yourself in GoHighLevel when it is ready.";
|
|
@@ -37425,9 +37513,9 @@ async function previewUndo(deps, run, key) {
|
|
|
37425
37513
|
return view(p);
|
|
37426
37514
|
}
|
|
37427
37515
|
async function approveFix(deps, run, hash, confirmedWording) {
|
|
37428
|
-
const
|
|
37516
|
+
const clock3 = deps.now ?? Date.now;
|
|
37429
37517
|
const p = previews.get(hash);
|
|
37430
|
-
if (!p || p.expires <
|
|
37518
|
+
if (!p || p.expires < clock3()) {
|
|
37431
37519
|
previews.delete(hash);
|
|
37432
37520
|
return { ok: false, written: false, error: "This preview has expired or was already used. Nothing was changed. Show the change again." };
|
|
37433
37521
|
}
|
|
@@ -37459,7 +37547,7 @@ async function approveFix(deps, run, hash, confirmedWording) {
|
|
|
37459
37547
|
workflowName: p.workflowName,
|
|
37460
37548
|
stepName: p.stepName,
|
|
37461
37549
|
previewHash: p.hash,
|
|
37462
|
-
at:
|
|
37550
|
+
at: clock3(),
|
|
37463
37551
|
state: "writing",
|
|
37464
37552
|
prior: current,
|
|
37465
37553
|
written: p.after,
|
|
@@ -37515,7 +37603,7 @@ async function approveFix(deps, run, hash, confirmedWording) {
|
|
|
37515
37603
|
const verdict = verifyReadBack(p.after, back, p.added, p.checks);
|
|
37516
37604
|
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
37605
|
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:
|
|
37606
|
+
if (undone && verdict.held) putRecord(run.locationId, run.runId, { ...undone, undoneAt: clock3(), undoneBy: rec2.id }, deps.base);
|
|
37519
37607
|
return { ok: true, held: verdict.held, detail, undo: p.undo };
|
|
37520
37608
|
} finally {
|
|
37521
37609
|
writing.delete(p.workflowId);
|
|
@@ -38072,7 +38160,7 @@ function createBriefRedactor(secrets) {
|
|
|
38072
38160
|
const matcher = new RegExp(`(?=(${pattern}))`, "g");
|
|
38073
38161
|
return (text2) => {
|
|
38074
38162
|
matcher.lastIndex = 0;
|
|
38075
|
-
const
|
|
38163
|
+
const pieces2 = [];
|
|
38076
38164
|
let copiedUntil = 0;
|
|
38077
38165
|
let start = -1;
|
|
38078
38166
|
let end = 0;
|
|
@@ -38086,15 +38174,15 @@ function createBriefRedactor(secrets) {
|
|
|
38086
38174
|
end = nextEnd;
|
|
38087
38175
|
} else if (nextStart <= end) end = Math.max(end, nextEnd);
|
|
38088
38176
|
else {
|
|
38089
|
-
|
|
38177
|
+
pieces2.push(text2.slice(copiedUntil, start), marker);
|
|
38090
38178
|
copiedUntil = end;
|
|
38091
38179
|
start = nextStart;
|
|
38092
38180
|
end = nextEnd;
|
|
38093
38181
|
}
|
|
38094
38182
|
}
|
|
38095
38183
|
if (start < 0) return text2;
|
|
38096
|
-
|
|
38097
|
-
return
|
|
38184
|
+
pieces2.push(text2.slice(copiedUntil, start), marker, text2.slice(end));
|
|
38185
|
+
return pieces2.join("");
|
|
38098
38186
|
};
|
|
38099
38187
|
}
|
|
38100
38188
|
var init_brief_redaction = __esm({
|
|
@@ -38847,7 +38935,7 @@ function clientTitle(name) {
|
|
|
38847
38935
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
38848
38936
|
}
|
|
38849
38937
|
function clientCheckHeadline(summary) {
|
|
38850
|
-
return summary.split(/\n\s*\n/)[0].split("\n").filter((line3) => line3.trim() &&
|
|
38938
|
+
return summary.split(/\n\s*\n/)[0].split("\n").filter((line3) => line3.trim() && !REMOVED_CLAIM_LINE.test(line3.trim())).join(" ").trim();
|
|
38851
38939
|
}
|
|
38852
38940
|
function scrubLatestCheck(raw) {
|
|
38853
38941
|
if (!raw) return void 0;
|
|
@@ -39176,6 +39264,7 @@ var init_client_status = __esm({
|
|
|
39176
39264
|
init_handoff_pack();
|
|
39177
39265
|
init_branding();
|
|
39178
39266
|
init_svg_sanitize();
|
|
39267
|
+
init_member_progress();
|
|
39179
39268
|
CLIENT_STATE_LABEL = {
|
|
39180
39269
|
"in-place": "Live in your account",
|
|
39181
39270
|
"not-yet": "Not confirmed yet",
|
|
@@ -40967,12 +41056,12 @@ function splitList(text2, opts = {}) {
|
|
|
40967
41056
|
}
|
|
40968
41057
|
function parsePricePoints(text2) {
|
|
40969
41058
|
const out = [];
|
|
40970
|
-
const
|
|
41059
|
+
const segments2 = text2.split(/\r?\n|;|\.\s+/);
|
|
40971
41060
|
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
41061
|
const clean4 = (s) => s.replace(/[\s.,:;|–—-]+$/, "").trim();
|
|
40973
41062
|
const num3 = (s) => s.replace(/,/g, "");
|
|
40974
41063
|
const recur = (s) => s ? "/" + s.replace(/^\/|^(?:per|an?|each)\b/i, "").trim().toLowerCase() : "";
|
|
40975
|
-
for (const segment of
|
|
41064
|
+
for (const segment of segments2) {
|
|
40976
41065
|
const t = segment.trim().replace(/\.$/, "");
|
|
40977
41066
|
if (!t) continue;
|
|
40978
41067
|
const m = t.match(ENTRY);
|
|
@@ -41006,8 +41095,8 @@ function staffFromLooseLine(line3) {
|
|
|
41006
41095
|
const after = line3.slice(m.index + m[0].length).replace(/^\s*[)>\]]+/, "").replace(LIGHT_EDGE, "");
|
|
41007
41096
|
let mobile;
|
|
41008
41097
|
const roleParts = [];
|
|
41009
|
-
const
|
|
41010
|
-
for (const piece of
|
|
41098
|
+
const pieces2 = after.split(/\s+[-–—|]\s+|[,;|]/).map((x) => x.replace(LIGHT_EDGE, "").replace(/^[(\[<](.*)[)\]>]$/, "$1").trim()).filter(Boolean);
|
|
41099
|
+
for (const piece of pieces2) {
|
|
41011
41100
|
const bare = piece.replace(/^(mobile|phone|cell|role)\s*[:=]\s*/i, "");
|
|
41012
41101
|
if (!mobile && PHONE_RE.test(bare) && /\d{7,}/.test(bare.replace(/\D/g, ""))) mobile = bare;
|
|
41013
41102
|
else if (!new RegExp(EMAIL_ANYWHERE.source).test(bare)) roleParts.push(bare);
|
|
@@ -41021,8 +41110,8 @@ function parseStaffLines(text2) {
|
|
|
41021
41110
|
const emailsIn = (t) => (t.match(EMAIL_ANYWHERE) ?? []).length;
|
|
41022
41111
|
const lines = cleanLines(text2).flatMap((line3) => {
|
|
41023
41112
|
if (!line3.includes(";") || emailsIn(line3) < 2) return [line3];
|
|
41024
|
-
const
|
|
41025
|
-
return
|
|
41113
|
+
const pieces2 = line3.split(";").map((p) => p.trim()).filter(Boolean);
|
|
41114
|
+
return pieces2.every((p) => emailsIn(p) === 1) ? pieces2 : [line3];
|
|
41026
41115
|
});
|
|
41027
41116
|
lines.forEach((line3, i) => {
|
|
41028
41117
|
if (emailsIn(line3) > 1) {
|
|
@@ -43816,7 +43905,7 @@ async function clientPreflight(registry2, locationId2, deps = {}) {
|
|
|
43816
43905
|
}
|
|
43817
43906
|
lines.push({
|
|
43818
43907
|
key: "authentication",
|
|
43819
|
-
ok: authenticated,
|
|
43908
|
+
ok: authenticated || !key,
|
|
43820
43909
|
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
43910
|
});
|
|
43822
43911
|
const blocking = lines.filter((l) => l.key !== "authentication" && !l.advisory);
|
|
@@ -44321,7 +44410,7 @@ var init_customization = __esm({
|
|
|
44321
44410
|
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
44411
|
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
44412
|
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.
|
|
44413
|
+
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
44414
|
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
44415
|
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
44416
|
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 +44790,159 @@ var init_testimonial_guard = __esm({
|
|
|
44701
44790
|
}
|
|
44702
44791
|
});
|
|
44703
44792
|
|
|
44793
|
+
// src/intake-to-build/business-hours.ts
|
|
44794
|
+
function dayIndex(word) {
|
|
44795
|
+
const w = word.toLowerCase().replace(/\.$/, "");
|
|
44796
|
+
for (const [re, n] of DAY_WORDS) if (re.test(w)) return n;
|
|
44797
|
+
return null;
|
|
44798
|
+
}
|
|
44799
|
+
function parseDays(spec) {
|
|
44800
|
+
const s = spec.toLowerCase().trim();
|
|
44801
|
+
if (GROUP_WORDS[s]) return [...GROUP_WORDS[s]];
|
|
44802
|
+
const days = [];
|
|
44803
|
+
for (const part of s.split(/\s*(?:,|&|\/|\band\b)\s*/).filter(Boolean)) {
|
|
44804
|
+
const range = part.split(/\s*(?:-|–|—|\bto\b|\bthrough\b|\bthru\b)\s*/);
|
|
44805
|
+
if (range.length === 1) {
|
|
44806
|
+
const d = dayIndex(range[0]);
|
|
44807
|
+
if (d === null) return null;
|
|
44808
|
+
days.push(d);
|
|
44809
|
+
} else if (range.length === 2) {
|
|
44810
|
+
const a = dayIndex(range[0]), b = dayIndex(range[1]);
|
|
44811
|
+
if (a === null || b === null) return null;
|
|
44812
|
+
for (let d = a; ; d = (d + 1) % 7) {
|
|
44813
|
+
days.push(d);
|
|
44814
|
+
if (d === b) break;
|
|
44815
|
+
}
|
|
44816
|
+
} else return null;
|
|
44817
|
+
}
|
|
44818
|
+
return days.length ? days : null;
|
|
44819
|
+
}
|
|
44820
|
+
function parseClock(text2) {
|
|
44821
|
+
const t = text2.toLowerCase().replace(/\s+/g, "").replace(/\./g, "");
|
|
44822
|
+
if (t === "noon") return { h: 12, m: 0, meridiem: "pm" };
|
|
44823
|
+
const m = /^(\d{1,2})(?::(\d{2}))?(am|pm|a|p)?$/.exec(t);
|
|
44824
|
+
if (!m) return null;
|
|
44825
|
+
const h = Number(m[1]), min = m[2] ? Number(m[2]) : 0;
|
|
44826
|
+
if (h > 23 || min > 59) return null;
|
|
44827
|
+
const mer = m[3] ? m[3].startsWith("a") ? "am" : "pm" : void 0;
|
|
44828
|
+
if (mer && (h < 1 || h > 12)) return null;
|
|
44829
|
+
return { h, m: min, meridiem: mer };
|
|
44830
|
+
}
|
|
44831
|
+
function to24(c) {
|
|
44832
|
+
if (c.meridiem === "am") return c.h === 12 ? 0 : c.h;
|
|
44833
|
+
if (c.meridiem === "pm") return c.h === 12 ? 12 : c.h + 12;
|
|
44834
|
+
return c.h;
|
|
44835
|
+
}
|
|
44836
|
+
function parseWindow(text2) {
|
|
44837
|
+
const parts = text2.split(/\s*(?:-|–|—|\bto\b|\buntil\b|\btill\b)\s*/i).filter((p) => p.trim());
|
|
44838
|
+
if (parts.length !== 2) return null;
|
|
44839
|
+
const open2 = parseClock(parts[0]), close = parseClock(parts[1]);
|
|
44840
|
+
if (!open2 || !close) return null;
|
|
44841
|
+
let openH;
|
|
44842
|
+
if (open2.meridiem) openH = to24(open2);
|
|
44843
|
+
else if (open2.h >= 13) openH = open2.h;
|
|
44844
|
+
else if (!close.meridiem && close.h >= 13) openH = open2.h;
|
|
44845
|
+
else if (!close.meridiem && (open2.h < 7 || open2.h > 12)) return null;
|
|
44846
|
+
else if (open2.h >= 5 && open2.h <= 12) openH = open2.h;
|
|
44847
|
+
else return null;
|
|
44848
|
+
let closeH;
|
|
44849
|
+
if (close.meridiem) closeH = to24(close);
|
|
44850
|
+
else if (close.h >= 13 || close.h === 0) closeH = close.h;
|
|
44851
|
+
else closeH = close.h > openH || close.h === openH && close.m > open2.m ? close.h : close.h + 12;
|
|
44852
|
+
if (!open2.meridiem && open2.h <= 12 && close.meridiem === "pm") {
|
|
44853
|
+
const openPm = open2.h === 12 ? 12 : open2.h + 12;
|
|
44854
|
+
if (openPm !== openH && openPm * 60 + open2.m < closeH * 60 + close.m) return null;
|
|
44855
|
+
}
|
|
44856
|
+
if (!open2.meridiem && !close.meridiem && open2.h <= 12 && closeH * 60 + close.m < 12 * 60) return null;
|
|
44857
|
+
const openMin = openH * 60 + open2.m, closeMin = closeH * 60 + close.m;
|
|
44858
|
+
if (closeH > 23 || closeMin <= openMin) return null;
|
|
44859
|
+
return { openHour: openH, openMinute: open2.m, closeHour: closeH, closeMinute: close.m };
|
|
44860
|
+
}
|
|
44861
|
+
function segments(text2) {
|
|
44862
|
+
const out = [];
|
|
44863
|
+
for (const piece of text2.split(/\s*(?:;|\n|\|)\s*/)) {
|
|
44864
|
+
let current = "";
|
|
44865
|
+
for (const part of piece.split(/\s*,\s*/)) {
|
|
44866
|
+
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)) {
|
|
44867
|
+
out.push(current);
|
|
44868
|
+
current = part;
|
|
44869
|
+
} else current = current ? `${current}, ${part}` : part;
|
|
44870
|
+
}
|
|
44871
|
+
if (current.trim()) out.push(current);
|
|
44872
|
+
}
|
|
44873
|
+
return out.filter((s) => s.trim());
|
|
44874
|
+
}
|
|
44875
|
+
function parseBusinessHours(answer2) {
|
|
44876
|
+
const text2 = String(answer2 ?? "").trim();
|
|
44877
|
+
if (!text2) return null;
|
|
44878
|
+
const byDay = /* @__PURE__ */ new Map();
|
|
44879
|
+
for (const raw of segments(text2)) {
|
|
44880
|
+
const seg = raw.trim().replace(/[.]$/, "");
|
|
44881
|
+
const m = DAYS_SPEC.exec(seg);
|
|
44882
|
+
if (!m) return null;
|
|
44883
|
+
const days = parseDays(m[1]);
|
|
44884
|
+
if (!days) return null;
|
|
44885
|
+
const rest = m[2].trim();
|
|
44886
|
+
const value = /^(?:closed|off|shut)$/i.test(rest) ? "closed" : parseWindow(rest);
|
|
44887
|
+
if (!value) return null;
|
|
44888
|
+
for (const d of days) {
|
|
44889
|
+
if (byDay.has(d)) return null;
|
|
44890
|
+
byDay.set(d, value);
|
|
44891
|
+
}
|
|
44892
|
+
}
|
|
44893
|
+
const open2 = [...byDay.entries()].filter((e) => e[1] !== "closed");
|
|
44894
|
+
if (!open2.length) return null;
|
|
44895
|
+
const order = [1, 2, 3, 4, 5, 6, 0];
|
|
44896
|
+
const blocks = [];
|
|
44897
|
+
for (const d of order) {
|
|
44898
|
+
const w = byDay.get(d);
|
|
44899
|
+
if (!w || w === "closed") continue;
|
|
44900
|
+
const same2 = blocks.find((b) => JSON.stringify(b.hours[0]) === JSON.stringify(w));
|
|
44901
|
+
if (same2) same2.daysOfTheWeek.push(d);
|
|
44902
|
+
else blocks.push({ daysOfTheWeek: [d], hours: [{ ...w }] });
|
|
44903
|
+
}
|
|
44904
|
+
return blocks;
|
|
44905
|
+
}
|
|
44906
|
+
function clock(h, m) {
|
|
44907
|
+
const suffix = h < 12 ? "am" : "pm";
|
|
44908
|
+
const h12 = h % 12 === 0 ? 12 : h % 12;
|
|
44909
|
+
return `${h12}${m ? `:${String(m).padStart(2, "0")}` : ""}${suffix}`;
|
|
44910
|
+
}
|
|
44911
|
+
function describeOpenHours(blocks) {
|
|
44912
|
+
return blocks.map((b) => {
|
|
44913
|
+
const days = [...b.daysOfTheWeek];
|
|
44914
|
+
const consecutive = days.every((d, i) => i === 0 || d === (days[i - 1] + 1) % 7);
|
|
44915
|
+
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 " : ", ");
|
|
44916
|
+
return `${dayText} ${b.hours.map((w) => `${clock(w.openHour, w.openMinute)} to ${clock(w.closeHour, w.closeMinute)}`).join(" and ")}`;
|
|
44917
|
+
}).join("; ");
|
|
44918
|
+
}
|
|
44919
|
+
var DAY_WORDS, GROUP_WORDS, DAY_TOKEN, DAYS_SPEC, DAY_NAMES2;
|
|
44920
|
+
var init_business_hours = __esm({
|
|
44921
|
+
"src/intake-to-build/business-hours.ts"() {
|
|
44922
|
+
"use strict";
|
|
44923
|
+
DAY_WORDS = [
|
|
44924
|
+
[/^sun(?:day)?s?$/, 0],
|
|
44925
|
+
[/^mon(?:day)?s?$/, 1],
|
|
44926
|
+
[/^tue(?:s(?:day)?)?s?$/, 2],
|
|
44927
|
+
[/^wed(?:s|nesday)?s?$/, 3],
|
|
44928
|
+
[/^thu(?:r(?:s(?:day)?)?)?s?$/, 4],
|
|
44929
|
+
[/^fri(?:day)?s?$/, 5],
|
|
44930
|
+
[/^sat(?:urday)?s?$/, 6]
|
|
44931
|
+
];
|
|
44932
|
+
GROUP_WORDS = {
|
|
44933
|
+
weekdays: [1, 2, 3, 4, 5],
|
|
44934
|
+
weekends: [0, 6],
|
|
44935
|
+
weekend: [0, 6],
|
|
44936
|
+
daily: [0, 1, 2, 3, 4, 5, 6],
|
|
44937
|
+
"every day": [0, 1, 2, 3, 4, 5, 6],
|
|
44938
|
+
"7 days": [0, 1, 2, 3, 4, 5, 6]
|
|
44939
|
+
};
|
|
44940
|
+
DAY_TOKEN = String.raw`(?:sun|mon|tue|wed|thu|fri|sat)[a-z.]*`;
|
|
44941
|
+
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");
|
|
44942
|
+
DAY_NAMES2 = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
44943
|
+
}
|
|
44944
|
+
});
|
|
44945
|
+
|
|
44704
44946
|
// src/intake-to-build/plan-from-preset.ts
|
|
44705
44947
|
function toGsm7(s) {
|
|
44706
44948
|
return GSM7_REPLACEMENTS.reduce((out, [re, to]) => out.replace(re, to), s);
|
|
@@ -45212,7 +45454,7 @@ function composeCalendars(skeletonCalendars, ctx) {
|
|
|
45212
45454
|
if (kept.length > 0 && ctx.bookingWanted) {
|
|
45213
45455
|
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
45456
|
}
|
|
45215
|
-
return kept;
|
|
45457
|
+
return applyIntakeHours(kept, ctx);
|
|
45216
45458
|
}
|
|
45217
45459
|
const out = [];
|
|
45218
45460
|
const usedRefs = /* @__PURE__ */ new Set();
|
|
@@ -45253,7 +45495,26 @@ function composeCalendars(skeletonCalendars, ctx) {
|
|
|
45253
45495
|
resolveOwnerTeam(kept[i]);
|
|
45254
45496
|
out.push(kept[i]);
|
|
45255
45497
|
}
|
|
45256
|
-
return out;
|
|
45498
|
+
return applyIntakeHours(out, ctx);
|
|
45499
|
+
}
|
|
45500
|
+
function applyIntakeHours(cals, ctx) {
|
|
45501
|
+
const answer2 = (ctx.brief.business?.hours ?? "").trim();
|
|
45502
|
+
if (!answer2 || cals.length === 0) return cals;
|
|
45503
|
+
const hours = parseBusinessHours(answer2);
|
|
45504
|
+
if (!hours) {
|
|
45505
|
+
const template = cals.find((c) => Array.isArray(c.openHours) && c.openHours.length)?.openHours;
|
|
45506
|
+
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.`);
|
|
45507
|
+
return cals;
|
|
45508
|
+
}
|
|
45509
|
+
for (const cal of cals) {
|
|
45510
|
+
cal.openHours = hours.map((b) => ({ daysOfTheWeek: [...b.daysOfTheWeek], hours: b.hours.map((w) => ({ ...w })) }));
|
|
45511
|
+
if (cal.availabilityType === void 0) cal.availabilityType = 0;
|
|
45512
|
+
}
|
|
45513
|
+
const shared = cals.filter((c) => c.calendarType === "round_robin" || c.calendarType === "collective").map((c) => `"${String(c.name)}"`);
|
|
45514
|
+
if (shared.length) {
|
|
45515
|
+
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.`);
|
|
45516
|
+
}
|
|
45517
|
+
return cals;
|
|
45257
45518
|
}
|
|
45258
45519
|
function resolveActionUsers(action, ctx) {
|
|
45259
45520
|
if (action.userRef === "user.owner") {
|
|
@@ -46043,6 +46304,7 @@ var init_plan_from_preset = __esm({
|
|
|
46043
46304
|
init_offer_normalize();
|
|
46044
46305
|
init_testimonial_guard();
|
|
46045
46306
|
init_sms_rules();
|
|
46307
|
+
init_business_hours();
|
|
46046
46308
|
init_plan_guide();
|
|
46047
46309
|
init_claims_guard();
|
|
46048
46310
|
PRESET_TOKEN_ROOTS = /* @__PURE__ */ new Set(["business", "offer", "audience", "voice", "goal", "proof"]);
|
|
@@ -46758,6 +47020,10 @@ function prefillPrompt(labels, transcript) {
|
|
|
46758
47020
|
"Answer ONLY from the material provided. Never invent a fact, a number, or a name.",
|
|
46759
47021
|
"Every answer must carry a QUOTE: the exact words from the material it comes from, copied verbatim, under 300 characters. An answer with no quote will be thrown away, and a quote that is not in the material will be thrown away.",
|
|
46760
47022
|
'If the material does not clearly answer a question, leave it out and list it under "unknown".',
|
|
47023
|
+
"When the material STATES a fact, answer it. Do not leave a stated fact out.",
|
|
47024
|
+
'Quote ONLY the words that give that one answer, not the whole line or paragraph around them. For a number, quote just the words with that one number, for example "$79 total" or "No more than 6 trials a day". For the staff and team size, quote the lines that name the people.',
|
|
47025
|
+
"Copy the material's own words into the value wherever the question allows it, rather than rewording them.",
|
|
47026
|
+
'Give every answer a "basis": "stated" when the material says it, or "inferred" when the material does not say it and you are working it out from other facts.',
|
|
46761
47027
|
"Keep answers short and concrete \u2014 this feeds an automated build, not a report.",
|
|
46762
47028
|
"",
|
|
46763
47029
|
"QUESTIONS (use these labels verbatim as keys):",
|
|
@@ -46767,7 +47033,7 @@ function prefillPrompt(labels, transcript) {
|
|
|
46767
47033
|
transcript.slice(0, 6e4),
|
|
46768
47034
|
"",
|
|
46769
47035
|
"Reply with ONE line of JSON and nothing else:",
|
|
46770
|
-
'RESULT: {"answers":{"<label>":{"value":"<answer>","quote":"<their exact words>"}},"unknown":["<label>"]}'
|
|
47036
|
+
'RESULT: {"answers":{"<label>":{"value":"<answer>","quote":"<their exact words>","basis":"stated or inferred"}},"unknown":["<label>"]}'
|
|
46771
47037
|
].join("\n");
|
|
46772
47038
|
}
|
|
46773
47039
|
function prefillPromptForSource(labels, source) {
|
|
@@ -46789,7 +47055,8 @@ async function prefillFromSource(spawnClaude, labels, source, toolContext = { se
|
|
|
46789
47055
|
if (empty) return { ok: false, answers: {}, unknown: labels, held: {}, unverified: [], error: "nothing to read" };
|
|
46790
47056
|
if (source.kind === "url") return { ok: false, answers: {}, unknown: labels, held: {}, unverified: [], error: LINK_READING_OFF_MESSAGE };
|
|
46791
47057
|
if (source.kind === "text" && source.text.length > 6e4) return { ok: false, answers: {}, unknown: labels, held: {}, unverified: [], error: "The limit is 60,000 characters. Paste an excerpt or shorten this text file." };
|
|
46792
|
-
const
|
|
47058
|
+
const byLabel = new Map(questions.map((q3) => [q3.label, q3]));
|
|
47059
|
+
const result = await runPrefill(() => spawnClaude(prefillPromptForSource(labels, source) + "\nQuestion schema (use only these choices; multiple choices use an array in the cited answer object\u2019s value):\n" + JSON.stringify(questions), prefillTools(source, toolContext)), labels, source.kind === "text" ? source.text : void 0, byLabel);
|
|
46793
47060
|
result.answers = validatePrefillAnswers(questions, result.answers);
|
|
46794
47061
|
for (const [label2, held] of Object.entries(result.held)) {
|
|
46795
47062
|
const checked = validatePrefillAnswers(questions, { [label2]: held.value });
|
|
@@ -46800,7 +47067,87 @@ async function prefillFromSource(spawnClaude, labels, source, toolContext = { se
|
|
|
46800
47067
|
result.unknown = labels.filter((label2) => !(label2 in result.answers) && !(label2 in result.held));
|
|
46801
47068
|
return result;
|
|
46802
47069
|
}
|
|
46803
|
-
|
|
47070
|
+
function numbersIn(text2) {
|
|
47071
|
+
return [...String(text2 ?? "").matchAll(/\d+(?:,\d{3})*(?:\.\d+)?/g)].map((m) => Number(m[0].replace(/,/g, ""))).filter(Number.isFinite);
|
|
47072
|
+
}
|
|
47073
|
+
function isYesNo(options) {
|
|
47074
|
+
return Boolean(options?.includes("Yes") && options?.includes("No"));
|
|
47075
|
+
}
|
|
47076
|
+
function wordsInQuote(value, quote) {
|
|
47077
|
+
const q3 = ` ${tokens(quote).join(" ")} `;
|
|
47078
|
+
const lines = value.split(/\n+/).map((l) => tokens(l).join(" ")).filter(Boolean);
|
|
47079
|
+
return lines.length > 0 && lines.every((l) => q3.includes(` ${l} `));
|
|
47080
|
+
}
|
|
47081
|
+
function onlyNumber(quote) {
|
|
47082
|
+
const nums = [...new Set(numbersIn(quote))];
|
|
47083
|
+
return nums.length === 1 ? nums[0] : null;
|
|
47084
|
+
}
|
|
47085
|
+
function amountBefore(quote, word) {
|
|
47086
|
+
const m = new RegExp(`${AMOUNT}\\s*${word}\\b`, "i").exec(quote) ?? new RegExp(`\\b${word}\\b[^0-9$]{0,12}${AMOUNT}`, "i").exec(quote);
|
|
47087
|
+
return m ? Number(m[1].replace(/,/g, "")) : null;
|
|
47088
|
+
}
|
|
47089
|
+
function depositIn(quote) {
|
|
47090
|
+
return amountBefore(quote, "deposit");
|
|
47091
|
+
}
|
|
47092
|
+
function totalIn(quote) {
|
|
47093
|
+
return amountBefore(quote, "total");
|
|
47094
|
+
}
|
|
47095
|
+
function staffNamedIn(quote) {
|
|
47096
|
+
return new Set((String(quote).match(/[^\s@()<>,;]+@[^\s@()<>,;]+\.[a-z]{2,}/gi) ?? []).map((e) => e.toLowerCase())).size;
|
|
47097
|
+
}
|
|
47098
|
+
function teamRange(n) {
|
|
47099
|
+
return n <= 1 ? "Just me" : n <= 5 ? "2-5" : "6+";
|
|
47100
|
+
}
|
|
47101
|
+
function leadRange(n) {
|
|
47102
|
+
return n < 100 ? "Under 100" : n < 500 ? "100-500" : n < 1e3 ? "500-1000" : "1000+";
|
|
47103
|
+
}
|
|
47104
|
+
function quoteGives(value, quote, label2 = "", options) {
|
|
47105
|
+
const n = numericValue(value);
|
|
47106
|
+
if (/^Balance due/i.test(label2)) {
|
|
47107
|
+
const t = totalIn(quote), d = depositIn(quote);
|
|
47108
|
+
return Boolean(n && !n.percent && t !== null && d !== null && t > d && t - d === n.n);
|
|
47109
|
+
}
|
|
47110
|
+
if (/^Deposit/i.test(label2)) return Boolean(n && !n.percent && depositIn(quote) === n.n);
|
|
47111
|
+
if (options?.length) {
|
|
47112
|
+
if (!options.includes(value)) return false;
|
|
47113
|
+
if (/[a-z]/i.test(value) && (/\d/.test(value) || tokens(value).length >= 3) && wordsInQuote(value, quote)) return true;
|
|
47114
|
+
if (/^Team Size$/i.test(label2)) {
|
|
47115
|
+
const named = staffNamedIn(quote);
|
|
47116
|
+
if (named) return named >= 2 && teamRange(named) === value;
|
|
47117
|
+
const one = onlyNumber(quote);
|
|
47118
|
+
return one !== null && /\b(?:staff|team|people|employees?|providers?)\b/i.test(quote) && teamRange(one) === value;
|
|
47119
|
+
}
|
|
47120
|
+
if (/Lead Volume/i.test(label2)) {
|
|
47121
|
+
const one = onlyNumber(quote);
|
|
47122
|
+
return one !== null && /\bleads?\b/i.test(quote) && leadRange(one) === value;
|
|
47123
|
+
}
|
|
47124
|
+
return false;
|
|
47125
|
+
}
|
|
47126
|
+
if (n) {
|
|
47127
|
+
if (n.percent || onlyNumber(quote) !== n.n) return false;
|
|
47128
|
+
if (/price|cost|value|\$/i.test(label2) && !/\$/.test(quote)) return false;
|
|
47129
|
+
if (/^Trial price/i.test(label2) && /deposit/i.test(quote)) return false;
|
|
47130
|
+
if (/How long is the trial/i.test(label2) && (!/\btrials?\b/i.test(quote) || !/minute/i.test(quote) || /\$/.test(quote))) return false;
|
|
47131
|
+
if (/in one day|per day/i.test(label2) && !/\b(?:trials?|visits?|clients?)\s+(?:a|per)\s+day\b/i.test(quote)) return false;
|
|
47132
|
+
if (/^Average program value/i.test(label2) && !/\baverage\b|\bmost popular\b|\bprogram\b/i.test(quote)) return false;
|
|
47133
|
+
return true;
|
|
47134
|
+
}
|
|
47135
|
+
return wordsInQuote(value, quote);
|
|
47136
|
+
}
|
|
47137
|
+
function intakeCitation(material, value, quote, basis, question) {
|
|
47138
|
+
const v = String(value ?? "").trim();
|
|
47139
|
+
if (v === "") return { verdict: "refused", because: "no value was given." };
|
|
47140
|
+
const said = quoteCheck(material, quote);
|
|
47141
|
+
if (said.verdict === "refused") return said;
|
|
47142
|
+
const shown = quote.length > 140 ? quote.slice(0, 137).trimEnd() + "\u2026" : quote;
|
|
47143
|
+
const based = `Based on \u201C${shown}\u201D.`;
|
|
47144
|
+
if (question && CLAIMS_GUARD_QUESTION.test(question.label)) return { verdict: "held", because: `This answer decides what the texts may promise, so it always waits for your click. ${based}`, claims: true };
|
|
47145
|
+
if (question && isYesNo(question.options)) return { verdict: "held", because: `Yes or no answers always wait for your click. ${based}` };
|
|
47146
|
+
if (!quoteGives(v, quote, question?.label, question?.options)) return { verdict: "held", because: based };
|
|
47147
|
+
if (basis === "inferred") return { verdict: "held", because: `Your Claude marked this as worked out. ${based}` };
|
|
47148
|
+
return { verdict: "ok" };
|
|
47149
|
+
}
|
|
47150
|
+
async function runPrefill(call2, labels, material, questions = /* @__PURE__ */ new Map()) {
|
|
46804
47151
|
const fail = (error) => ({ ok: false, answers: {}, unknown: labels, held: {}, unverified: [], error });
|
|
46805
47152
|
try {
|
|
46806
47153
|
const out = await call2();
|
|
@@ -46815,16 +47162,18 @@ async function runPrefill(call2, labels, material) {
|
|
|
46815
47162
|
const candidate = raw && typeof raw === "object" && !Array.isArray(raw) ? raw.value : raw;
|
|
46816
47163
|
const value = typeof candidate === "string" ? candidate.trim() : Array.isArray(candidate) && candidate.every((v) => typeof v === "string") ? candidate.map((v) => v.trim()).filter(Boolean) : "";
|
|
46817
47164
|
const quote = typeof raw?.quote === "string" ? String(raw.quote).trim() : "";
|
|
47165
|
+
const basis = raw?.basis === "inferred" ? "inferred" : void 0;
|
|
46818
47166
|
if (!value.length) continue;
|
|
46819
47167
|
if (material === void 0) {
|
|
46820
47168
|
answers[k] = value;
|
|
46821
47169
|
unverified.push(k);
|
|
46822
47170
|
continue;
|
|
46823
47171
|
}
|
|
46824
|
-
const
|
|
47172
|
+
const q3 = questions.get(k);
|
|
47173
|
+
const checks = (Array.isArray(value) ? value : [value]).map((v) => intakeCitation(material, v, quote, basis, q3 ? { label: k, options: q3.options } : void 0));
|
|
46825
47174
|
const cite = checks.find((c) => c.verdict === "refused") ?? checks.find((c) => c.verdict === "held") ?? checks[0];
|
|
46826
47175
|
if (cite.verdict === "ok") answers[k] = value;
|
|
46827
|
-
else if (cite.verdict === "held") held[k] = { value, because: cite.because };
|
|
47176
|
+
else if (cite.verdict === "held") held[k] = { value, because: cite.because, ...cite.claims ? { claims: true } : {} };
|
|
46828
47177
|
}
|
|
46829
47178
|
const unknown = labels.filter((l) => !(l in answers) && !(l in held));
|
|
46830
47179
|
return { ok: true, answers, unknown, held, unverified };
|
|
@@ -46846,7 +47195,7 @@ function validatePrefillAnswers(questions, proposed) {
|
|
|
46846
47195
|
}
|
|
46847
47196
|
return result;
|
|
46848
47197
|
}
|
|
46849
|
-
var INTAKE_FORM_NAME2, INTAKE_CONTACT_TAG, FICTIONAL_PHONE_REASON, RECORDER_HINTS, RECORDER_READ_VERBS, RECORDER_WRITE_VERBS, PREFILL_BUILTIN_DENY, LINK_READING_OFF_MESSAGE, WORD_PDF_MESSAGE, READABLE_DOCUMENT;
|
|
47198
|
+
var INTAKE_FORM_NAME2, INTAKE_CONTACT_TAG, FICTIONAL_PHONE_REASON, RECORDER_HINTS, RECORDER_READ_VERBS, RECORDER_WRITE_VERBS, PREFILL_BUILTIN_DENY, LINK_READING_OFF_MESSAGE, WORD_PDF_MESSAGE, READABLE_DOCUMENT, CLAIMS_GUARD_QUESTION, AMOUNT;
|
|
46850
47199
|
var init_client_intake = __esm({
|
|
46851
47200
|
"src/client-intake.ts"() {
|
|
46852
47201
|
"use strict";
|
|
@@ -46857,6 +47206,7 @@ var init_client_intake = __esm({
|
|
|
46857
47206
|
init_intake_overlay();
|
|
46858
47207
|
init_form_template();
|
|
46859
47208
|
init_transcript();
|
|
47209
|
+
init_words();
|
|
46860
47210
|
init_result_text();
|
|
46861
47211
|
init_links();
|
|
46862
47212
|
INTAKE_FORM_NAME2 = "GHL Command Blueprint \u2014 Client Intake";
|
|
@@ -46913,6 +47263,201 @@ var init_client_intake = __esm({
|
|
|
46913
47263
|
LINK_READING_OFF_MESSAGE = "Reading a link isn't available in this beta. Open the page, copy its text, and paste it into Paste a transcript.";
|
|
46914
47264
|
WORD_PDF_MESSAGE = "Word and PDF files can't be read yet. Open the file, copy the text, and paste it into Paste a transcript.";
|
|
46915
47265
|
READABLE_DOCUMENT = /[.](txt|md|csv|vtt|srt)$/i;
|
|
47266
|
+
CLAIMS_GUARD_QUESTION = /really run out|only good for 72 hours/i;
|
|
47267
|
+
AMOUNT = String.raw`\$?\s*(\d[\d,]*(?:\.\d+)?)`;
|
|
47268
|
+
}
|
|
47269
|
+
});
|
|
47270
|
+
|
|
47271
|
+
// src/build-facts.ts
|
|
47272
|
+
function customValueKeyOf(name) {
|
|
47273
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
47274
|
+
}
|
|
47275
|
+
function customValueStatusOf(planValues, accountValues, now = () => /* @__PURE__ */ new Date()) {
|
|
47276
|
+
const readAt = now().toISOString();
|
|
47277
|
+
if (!accountValues || planValues.length > 0 && accountValues.length === 0) return { filled: [], empty: [], unreadable: true, readAt };
|
|
47278
|
+
const byKey = new Map(accountValues.map((v) => [customValueKeyOf(v.name), v]));
|
|
47279
|
+
const filled = [];
|
|
47280
|
+
const empty = [];
|
|
47281
|
+
const unknown = [];
|
|
47282
|
+
for (const cv of planValues) {
|
|
47283
|
+
const live = byKey.get(customValueKeyOf(cv.name));
|
|
47284
|
+
if (!live) empty.push(cv.name);
|
|
47285
|
+
else if (typeof live.value !== "string") unknown.push(cv.name);
|
|
47286
|
+
else if (live.value.trim()) filled.push(cv.name);
|
|
47287
|
+
else empty.push(cv.name);
|
|
47288
|
+
}
|
|
47289
|
+
return { filled, empty, ...unknown.length ? { unknown } : {}, readAt };
|
|
47290
|
+
}
|
|
47291
|
+
function handoffCustomValueNote(handoffRef, planValues, status) {
|
|
47292
|
+
if (!status || status.unreadable) return "";
|
|
47293
|
+
const covered = planValues.filter((cv) => cv.filledBy === handoffRef).map((cv) => cv.name);
|
|
47294
|
+
if (!covered.length) return "";
|
|
47295
|
+
const filled = covered.filter((n) => status.filled.includes(n));
|
|
47296
|
+
const empty = covered.filter((n) => status.empty.includes(n));
|
|
47297
|
+
const list4 = (xs) => xs.length <= 1 ? xs.join("") : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
|
|
47298
|
+
const parts = [
|
|
47299
|
+
filled.length ? `${list4(filled)} ${filled.length === 1 ? "is" : "are"} filled` : "",
|
|
47300
|
+
empty.length ? `${list4(empty)} ${empty.length === 1 ? "is" : "are"} empty` : ""
|
|
47301
|
+
].filter(Boolean);
|
|
47302
|
+
return parts.length ? ` Read back from the account after this build: ${parts.join("; ")}.` : "";
|
|
47303
|
+
}
|
|
47304
|
+
function pieces(text2) {
|
|
47305
|
+
const guarded2 = text2.replace(/\bDr\./g, "Dr\0");
|
|
47306
|
+
const out = [];
|
|
47307
|
+
const re = /([\s\S]*?(?:[.!?](?=\s|$)|(?=\n)|$))(\s*)/g;
|
|
47308
|
+
let m;
|
|
47309
|
+
while ((m = re.exec(guarded2)) && m[0].length) out.push({ sentence: m[1].replace(/Dr\u0000/g, "Dr."), sep: m[2] });
|
|
47310
|
+
return out;
|
|
47311
|
+
}
|
|
47312
|
+
function rejoin(parts) {
|
|
47313
|
+
const kept = parts.filter((p) => p !== null && p.sentence.trim() !== "");
|
|
47314
|
+
return kept.map((p, i) => p.sentence + (i < kept.length - 1 ? p.sep : "")).join("");
|
|
47315
|
+
}
|
|
47316
|
+
function claimsVerifiedPersonNotReal(sentence, verifiedStaff) {
|
|
47317
|
+
for (const raw of verifiedStaff) {
|
|
47318
|
+
const name = raw.trim();
|
|
47319
|
+
if (!name) continue;
|
|
47320
|
+
const n = esc11(name);
|
|
47321
|
+
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");
|
|
47322
|
+
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");
|
|
47323
|
+
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");
|
|
47324
|
+
if (after.test(sentence) || isnt.test(sentence) || before.test(sentence)) return true;
|
|
47325
|
+
}
|
|
47326
|
+
return false;
|
|
47327
|
+
}
|
|
47328
|
+
function mentionPattern(key) {
|
|
47329
|
+
const words2 = customValueKeyOf(key).split("_").map(esc11);
|
|
47330
|
+
return new RegExp(`\\b${words2.join("[\\s_]+")}\\b`, "gi");
|
|
47331
|
+
}
|
|
47332
|
+
function correctClause(clause, status) {
|
|
47333
|
+
const pred = new RegExp(EMPTY_PRED, "i").exec(clause);
|
|
47334
|
+
if (!pred) return { text: clause, changed: false };
|
|
47335
|
+
const head4 = clause.slice(0, pred.index);
|
|
47336
|
+
const keys = [...status.filled, ...status.empty, ...status.unknown ?? []];
|
|
47337
|
+
const found = [];
|
|
47338
|
+
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 });
|
|
47339
|
+
if (!found.length) return { text: clause, changed: false };
|
|
47340
|
+
found.sort((a, b) => a.start - b.start);
|
|
47341
|
+
const last = found[found.length - 1];
|
|
47342
|
+
const tail = head4.slice(last.end);
|
|
47343
|
+
if (!/^\s*(?:custom values?\s*)?(?:\([^)]*\)\s*)?$/i.test(tail)) return { text: clause, changed: false };
|
|
47344
|
+
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 };
|
|
47345
|
+
const filledNamed = found.filter((f) => status.filled.includes(f.key));
|
|
47346
|
+
if (!filledNamed.length) return { text: clause, changed: false };
|
|
47347
|
+
const stillEmpty = found.filter((f) => status.empty.includes(f.key));
|
|
47348
|
+
if (!stillEmpty.length) return { text: "", changed: true };
|
|
47349
|
+
const listed = listOf(stillEmpty.map((f) => head4.slice(f.start, f.end)));
|
|
47350
|
+
let out = clause.slice(0, found[0].start) + listed + clause.slice(last.end);
|
|
47351
|
+
if (stillEmpty.length === 1 && found.length > 1) {
|
|
47352
|
+
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 };
|
|
47353
|
+
}
|
|
47354
|
+
return { text: out, changed: true };
|
|
47355
|
+
}
|
|
47356
|
+
function maskAndNames(clause, keys) {
|
|
47357
|
+
const masked = [];
|
|
47358
|
+
let text2 = clause;
|
|
47359
|
+
for (const key of keys.filter((k) => /(?:^|_)and(?:_|$)/.test(customValueKeyOf(k)))) {
|
|
47360
|
+
text2 = text2.replace(mentionPattern(key), (m) => {
|
|
47361
|
+
masked.push(m);
|
|
47362
|
+
return `${masked.length - 1}`;
|
|
47363
|
+
});
|
|
47364
|
+
}
|
|
47365
|
+
return { text: text2, unmask: (t) => t.replace(/\u0001(\d+)\u0001/g, (_m, i) => masked[Number(i)]) };
|
|
47366
|
+
}
|
|
47367
|
+
function splitAndClaims(clause, keys = []) {
|
|
47368
|
+
const { text: text2, unmask } = maskAndNames(clause, keys);
|
|
47369
|
+
return splitMasked(text2).map(unmask);
|
|
47370
|
+
}
|
|
47371
|
+
function claimsWithin(clause, keys) {
|
|
47372
|
+
const { text: text2, unmask } = maskAndNames(clause, keys);
|
|
47373
|
+
const claims = (t) => new RegExp(EMPTY_PRED, "i").test(t);
|
|
47374
|
+
const bits = text2.split(/(,?\s+and\s+)/i);
|
|
47375
|
+
let from = claims(bits[0]) ? 0 : -1;
|
|
47376
|
+
for (let i = 2; from < 0 && i < bits.length; i += 2) if (claims(bits.slice(i).join(""))) from = i;
|
|
47377
|
+
if (from < 0) return [];
|
|
47378
|
+
return splitMasked(bits.slice(from).join("")).map(unmask).filter((_, i) => i % 2 === 0).filter(claims);
|
|
47379
|
+
}
|
|
47380
|
+
function splitMasked(clause) {
|
|
47381
|
+
const bits = clause.split(/(,?\s+and\s+)/i);
|
|
47382
|
+
const out = [bits[0]];
|
|
47383
|
+
const claims = (t) => new RegExp(EMPTY_PRED, "i").test(t);
|
|
47384
|
+
for (let i = 1; i < bits.length; i += 2) {
|
|
47385
|
+
const joiner = bits[i], next = bits[i + 1] ?? "";
|
|
47386
|
+
if (claims(out[out.length - 1]) && claims(next)) out.push(joiner, next);
|
|
47387
|
+
else out[out.length - 1] += joiner + next;
|
|
47388
|
+
}
|
|
47389
|
+
return out;
|
|
47390
|
+
}
|
|
47391
|
+
function correctEmptinessClaim(sentence, status) {
|
|
47392
|
+
if (!status || status.unreadable) return { text: sentence, changed: false };
|
|
47393
|
+
const keys = [...status.filled, ...status.empty, ...status.unknown ?? []];
|
|
47394
|
+
const parts = [];
|
|
47395
|
+
const outer = sentence.split(CLAUSE_SPLIT);
|
|
47396
|
+
for (let i = 0; i < outer.length; i += 2) {
|
|
47397
|
+
parts.push(...splitAndClaims(outer[i], keys));
|
|
47398
|
+
if (i + 1 < outer.length) parts.push(outer[i + 1]);
|
|
47399
|
+
}
|
|
47400
|
+
let changed = false;
|
|
47401
|
+
const kept = [];
|
|
47402
|
+
let dropNextConsequence = false;
|
|
47403
|
+
for (let i = 0; i < parts.length; i += 2) {
|
|
47404
|
+
const joiner = parts[i - 1] ?? "";
|
|
47405
|
+
if (dropNextConsequence && /^\s*,?\s*(?:and so|so)\s+$/i.test(joiner)) {
|
|
47406
|
+
changed = true;
|
|
47407
|
+
for (const piece of claimsWithin(parts[i], keys)) {
|
|
47408
|
+
const c2 = correctClause(piece, status);
|
|
47409
|
+
if (c2.text.trim()) kept.push({ text: c2.text, joiner: ", and " });
|
|
47410
|
+
}
|
|
47411
|
+
continue;
|
|
47412
|
+
}
|
|
47413
|
+
dropNextConsequence = false;
|
|
47414
|
+
const c = correctClause(parts[i], status);
|
|
47415
|
+
if (c.changed) changed = true;
|
|
47416
|
+
if (c.text.trim()) kept.push({ text: c.text, joiner });
|
|
47417
|
+
else dropNextConsequence = true;
|
|
47418
|
+
}
|
|
47419
|
+
if (!changed) return { text: sentence, changed: false };
|
|
47420
|
+
let text2 = kept.map((k, i) => (i ? k.joiner : "") + k.text).join("").trim();
|
|
47421
|
+
if (text2 && !/[.!?]$/.test(text2)) text2 += ".";
|
|
47422
|
+
text2 = text2.charAt(0).toUpperCase() + text2.slice(1);
|
|
47423
|
+
return { text: /^[.!?]?$/.test(text2) ? "" : text2, changed: true };
|
|
47424
|
+
}
|
|
47425
|
+
function guardBuildReport(report3, facts) {
|
|
47426
|
+
const staff = (facts.verifiedStaff ?? []).filter((n) => n.trim());
|
|
47427
|
+
const others = (facts.unverifiedStaff ?? []).filter((n) => n.trim());
|
|
47428
|
+
const fix = (text2) => {
|
|
47429
|
+
let staffClaim = false, changed = false;
|
|
47430
|
+
const parts = pieces(text2).map((p) => {
|
|
47431
|
+
if (staff.length && claimsVerifiedPersonNotReal(p.sentence, staff)) {
|
|
47432
|
+
staffClaim = true;
|
|
47433
|
+
changed = true;
|
|
47434
|
+
return null;
|
|
47435
|
+
}
|
|
47436
|
+
const bullet3 = /^(\s*(?:[-*•]|\d+[.)])\s+)/.exec(p.sentence)?.[1] ?? "";
|
|
47437
|
+
const c = correctEmptinessClaim(p.sentence.slice(bullet3.length), facts.customValues);
|
|
47438
|
+
if (!c.changed) return p;
|
|
47439
|
+
changed = true;
|
|
47440
|
+
return c.text ? { sentence: bullet3 + c.text, sep: p.sep } : null;
|
|
47441
|
+
});
|
|
47442
|
+
return { text: changed ? rejoin(parts) : text2, staffClaim };
|
|
47443
|
+
};
|
|
47444
|
+
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));
|
|
47445
|
+
const out = { ...report3 };
|
|
47446
|
+
if (typeof report3.summary === "string") out.summary = fix(report3.summary).text;
|
|
47447
|
+
if (Array.isArray(report3.issues)) {
|
|
47448
|
+
out.issues = report3.issues.map((x) => ({ original: x, ...fix(x) })).filter((r) => r.text.trim() && !(r.staffClaim && !namesOther(r.original))).map((r) => r.text);
|
|
47449
|
+
}
|
|
47450
|
+
return out;
|
|
47451
|
+
}
|
|
47452
|
+
var esc11, listOf, NOT_REAL_PHRASE, EMPTY_PRED, CLAUSE_SPLIT;
|
|
47453
|
+
var init_build_facts = __esm({
|
|
47454
|
+
"src/build-facts.ts"() {
|
|
47455
|
+
"use strict";
|
|
47456
|
+
esc11 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
47457
|
+
listOf = (xs) => xs.length <= 1 ? xs.join("") : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
|
|
47458
|
+
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)))`;
|
|
47459
|
+
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)`;
|
|
47460
|
+
CLAUSE_SPLIT = /(;\s*|,\s*(?:but|while|although|though|so|and so|because|whereas)\s+|\s+but\s+)/i;
|
|
46916
47461
|
}
|
|
46917
47462
|
});
|
|
46918
47463
|
|
|
@@ -46927,8 +47472,8 @@ function agencyProfilePageHtml(p, clients = [], seatToken = "") {
|
|
|
46927
47472
|
const staff = (p.staff ?? []).map((m) => [m.name, m.role, m.email].filter(Boolean).join(" \xB7 ")).join("\n");
|
|
46928
47473
|
const hintOf2 = (id, hint) => hint ? `<div class="hint" id="${id}-hint">${hint}</div>` : "";
|
|
46929
47474
|
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)}>${
|
|
47475
|
+
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>`;
|
|
47476
|
+
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
47477
|
const filled = (xs) => xs.filter((x) => Array.isArray(x) ? x.length > 0 : x !== void 0 && x !== null && String(x).trim() !== "").length;
|
|
46933
47478
|
const progress = (xs) => {
|
|
46934
47479
|
const n = filled(xs);
|
|
@@ -46946,20 +47491,20 @@ function agencyProfilePageHtml(p, clients = [], seatToken = "") {
|
|
|
46946
47491
|
const cur = e.currency || p.pricing?.currency || "";
|
|
46947
47492
|
const recorded = Object.keys(e).filter((k) => k !== "updated_at").length > 0;
|
|
46948
47493
|
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">${
|
|
47494
|
+
return ` <li><details class="cl cds-details" data-id="${esc12(c.locationId)}"${recorded ? "" : ' data-empty="1"'}>
|
|
47495
|
+
<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
47496
|
<div class="ap-body">
|
|
46952
47497
|
<div class="cds-grid">
|
|
46953
47498
|
<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="${
|
|
47499
|
+
<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>
|
|
47500
|
+
<div class="cds-field"><label>They pay</label><input class="cds-input f" data-k="price" inputmode="numeric" value="${esc12(e.price)}"></div>
|
|
46956
47501
|
<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="${
|
|
47502
|
+
<div class="cds-field"><label>Started</label><input class="cds-input f" data-k="startedOn" placeholder="2026-01-15" value="${esc12(e.startedOn)}"></div>
|
|
47503
|
+
<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
47504
|
<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="${
|
|
47505
|
+
<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
47506
|
</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">${
|
|
47507
|
+
<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
47508
|
<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
47509
|
</div>
|
|
46965
47510
|
</details></li>`;
|
|
@@ -47015,7 +47560,7 @@ ${p.agencyName ? "" : ` <div class="cds-callout warn"><span class="ttl">Add an a
|
|
|
47015
47560
|
${input("legalName", "Registered entity, if different", p.legalName, { placeholder: "Your Agency LLC, used on contracts and A2P" })}
|
|
47016
47561
|
<div class="cds-grid">
|
|
47017
47562
|
${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="${
|
|
47563
|
+
<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
47564
|
</div>
|
|
47020
47565
|
${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
47566
|
${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 +47605,7 @@ ${p.agencyName ? "" : ` <div class="cds-callout warn"><span class="ttl">Add an a
|
|
|
47060
47605
|
<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
47606
|
<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
47607
|
<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>${
|
|
47608
|
+
${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
47609
|
</div>
|
|
47065
47610
|
${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
47611
|
</section>
|
|
@@ -47143,8 +47688,8 @@ ${SERVICE_IDS.map((id) => ` <label class="cds-check"><input type="checkbox" n
|
|
|
47143
47688
|
<details class="cds-details"><summary>Details</summary>
|
|
47144
47689
|
<div class="cds-stack">
|
|
47145
47690
|
<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): ${
|
|
47691
|
+
<p>${esc12(claudeCodeAddHelp())}</p>
|
|
47692
|
+
<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
47693
|
<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
47694
|
<p>Enable the intake and workflow tools in your GHL Command settings.</p>
|
|
47150
47695
|
<p>Use Check and save this key, in the Claude key panel, to verify Claude authentication.</p>
|
|
@@ -47285,7 +47830,7 @@ document.querySelectorAll(".save-cl").forEach(btn=>btn.addEventListener("click",
|
|
|
47285
47830
|
</script>
|
|
47286
47831
|
<script>${COCKPIT_TYPE_JS}</script></body></html>`;
|
|
47287
47832
|
}
|
|
47288
|
-
var READ_NUMBER_JS, fmt,
|
|
47833
|
+
var READ_NUMBER_JS, fmt, esc12;
|
|
47289
47834
|
var init_agency_profile_page = __esm({
|
|
47290
47835
|
"src/agency-profile-page.ts"() {
|
|
47291
47836
|
"use strict";
|
|
@@ -47305,7 +47850,7 @@ var init_agency_profile_page = __esm({
|
|
|
47305
47850
|
return Number.isFinite(n)?{value:n}:{bad:true};
|
|
47306
47851
|
}`;
|
|
47307
47852
|
fmt = (n, currency) => `${currency ? currency + " " : ""}${n.toLocaleString("en-US")}`;
|
|
47308
|
-
|
|
47853
|
+
esc12 = (x) => String(x ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
47309
47854
|
}
|
|
47310
47855
|
});
|
|
47311
47856
|
|
|
@@ -49671,24 +50216,24 @@ function storedDay(iso2, now = /* @__PURE__ */ new Date()) {
|
|
|
49671
50216
|
function storedCard(r) {
|
|
49672
50217
|
const st = brandState(r);
|
|
49673
50218
|
if (r.problem) {
|
|
49674
|
-
return `<article class="cds-card bp-card"><div class="cds-row"><h3 class="cds-panel-title">${
|
|
50219
|
+
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
50220
|
<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>${
|
|
50221
|
+
<details class="cds-details"><summary>Details</summary><p>${esc13(r.problem)}</p></details></article>`;
|
|
49677
50222
|
}
|
|
49678
50223
|
const href = `/brand/${encodeURIComponent(r.slug)}`;
|
|
49679
50224
|
const f = fontWords(r.display, r.body);
|
|
49680
50225
|
return `<article class="cds-card bp-card">
|
|
49681
|
-
<div class="cds-row"><h3 class="cds-panel-title">${
|
|
50226
|
+
<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
50227
|
${r.accent ? `<div class="bp-sw">${swatch(r.accent, "Accent")}</div>` : ""}
|
|
49683
|
-
<p>${
|
|
49684
|
-
<div class="cds-src">${
|
|
50228
|
+
<p>${esc13(f.text)}</p>
|
|
50229
|
+
<div class="cds-src">${esc13(brandSource(r.extractedVia))}</div>
|
|
49685
50230
|
<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 ${
|
|
50231
|
+
<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
50232
|
</article>`;
|
|
49688
50233
|
}
|
|
49689
50234
|
function houseRow(d) {
|
|
49690
50235
|
const sub = [d.voice.tone, fontWords(d.typography.display.family, d.typography.body.family).text].filter(Boolean).join(". ");
|
|
49691
|
-
return `<li><span class="ttl">${
|
|
50236
|
+
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
50237
|
}
|
|
49693
50238
|
function brandsPageHtml(rows2, house, refs2, seatToken = "") {
|
|
49694
50239
|
const first2 = rows2.slice(0, SHOW_FIRST), rest = rows2.slice(SHOW_FIRST);
|
|
@@ -49723,7 +50268,7 @@ ${refs2.length ? `<details id="refs-more" class="cds-more"><summary>More looks f
|
|
|
49723
50268
|
<div class="cds-stack">
|
|
49724
50269
|
<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
50270
|
<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="${
|
|
50271
|
+
<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
50272
|
</div>
|
|
49728
50273
|
</details>` : ""}
|
|
49729
50274
|
<div class="cds-field"><p class="hint" id="look-status" role="status" aria-live="polite"></p></div>
|
|
@@ -49762,20 +50307,20 @@ function brandCardHtml(d, opts = {}) {
|
|
|
49762
50307
|
};
|
|
49763
50308
|
const fw = composeFonts(faceWords(disp), faceWords(bod), !disp.family?.trim() && !bod.family?.trim());
|
|
49764
50309
|
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">${
|
|
50310
|
+
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>`;
|
|
50311
|
+
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>`;
|
|
50312
|
+
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>`;
|
|
50313
|
+
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("");
|
|
50314
|
+
const samples = d.voice.samples.map((s) => `<li><span class="ttl">${esc13(s)}</span></li>`);
|
|
49770
50315
|
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
50316
|
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="${
|
|
50317
|
+
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
50318
|
const day = storedDay(d.provenance.extractedAt);
|
|
49774
|
-
const sub = [d.website ?
|
|
50319
|
+
const sub = [d.website ? esc13(d.website) : "", esc13(brandSource(d.provenance.extractedVia)), day ? `stored ${esc13(day)}` : ""].filter(Boolean).join(" \xB7 ");
|
|
49775
50320
|
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
50321
|
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>${
|
|
50322
|
+
<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>
|
|
50323
|
+
${d.pageStudioTheme.warnings.length ? `<h3>Before this reaches a client</h3><ul>${d.pageStudioTheme.warnings.map((w) => `<li>${esc13(w)}</li>`).join("")}</ul>` : ""}
|
|
49779
50324
|
${fw.broken ? brokenFontsNote(disp.family, bod.family) : ""}
|
|
49780
50325
|
<h3>Colours</h3>
|
|
49781
50326
|
${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 +50329,34 @@ ${table(["Pair", "Ratio", '<span class="cds-sr">Correction</span>'], pairs)}
|
|
|
49784
50329
|
<h3>Fonts</h3>
|
|
49785
50330
|
${table(["Use", "Face", "Renders as", "Source"], `${face("Display", disp)}${face("Body", bod)}`)}
|
|
49786
50331
|
<h3>Logo</h3>
|
|
49787
|
-
<p>${
|
|
50332
|
+
<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
50333
|
<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: ${
|
|
50334
|
+
<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>
|
|
50335
|
+
${d.voice.tone ? `<p>Tone: ${esc13(d.voice.tone)}${d.voice.audience ? ` \xB7 Audience: ${esc13(d.voice.audience)}` : ""}</p>` : ""}
|
|
49791
50336
|
${headlines}
|
|
49792
50337
|
<h3>For Page Studio</h3>
|
|
49793
50338
|
<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">${
|
|
50339
|
+
<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
50340
|
<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 ${
|
|
50341
|
+
${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(""))}
|
|
50342
|
+
${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>` : ""}
|
|
50343
|
+
${opts.dir ? `<p>Stored at ${esc13(opts.dir)}</p>` : ""}
|
|
49799
50344
|
</div></details>`;
|
|
49800
50345
|
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>${
|
|
50346
|
+
<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>
|
|
50347
|
+
<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>
|
|
50348
|
+
<div class="cds-row" id="verdict"><span class="cds-chip ${v.tone}">${v.word}</span><p>${esc13(v.say)}</p></div>
|
|
49804
50349
|
<dl class="cds-kv">
|
|
49805
50350
|
<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="${
|
|
50351
|
+
<dt>Fonts</dt><dd>${esc13(fw.text)}</dd>
|
|
50352
|
+
<dt>Logo</dt><dd><div class="bp-logo" data-ink="${esc13(c.fg.hex)}" data-sw="${esc13(c.bg.hex)}">${logo}</div></dd>
|
|
49808
50353
|
</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 ${
|
|
50354
|
+
${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
50355
|
${details}
|
|
49811
50356
|
</section>
|
|
49812
50357
|
<div class="cds-grid two">
|
|
49813
50358
|
${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>${
|
|
50359
|
+
<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
50360
|
<form id="make" class="cds-stack" novalidate><div class="cds-grid">
|
|
49816
50361
|
<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
50362
|
<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 +50372,10 @@ ${panel2("Make", "Make something in this brand", `<p>Runs on <b>your own Claude<
|
|
|
49827
50372
|
<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
50373
|
${panel2("Change", "Change something", `<p>Fill in only what you want to change. An empty field keeps what is there.</p>
|
|
49829
50374
|
<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="${
|
|
50375
|
+
<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>
|
|
50376
|
+
<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>
|
|
50377
|
+
<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>
|
|
50378
|
+
<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
50379
|
<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
50380
|
<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
50381
|
</div>
|
|
@@ -49897,7 +50442,7 @@ t.addEventListener("input",()=>{const v=t.value.trim();if(/^#[0-9a-f]{6}$/i.test
|
|
|
49897
50442
|
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
50443
|
`)}`;
|
|
49899
50444
|
}
|
|
49900
|
-
var
|
|
50445
|
+
var esc13, STYLE, SHELL_JS, foot, SENTENCE_WORDS, pickerHex, swatch, brokenFontsNote, SHOW_FIRST, panel2;
|
|
49901
50446
|
var init_brand_page = __esm({
|
|
49902
50447
|
"src/command-os/design/brand-page.ts"() {
|
|
49903
50448
|
"use strict";
|
|
@@ -49907,7 +50452,7 @@ var init_brand_page = __esm({
|
|
|
49907
50452
|
init_cockpit_shell();
|
|
49908
50453
|
init_script_json();
|
|
49909
50454
|
init_svg_sanitize();
|
|
49910
|
-
|
|
50455
|
+
esc13 = (s) => String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
49911
50456
|
STYLE = `
|
|
49912
50457
|
[hidden]{display:none!important}
|
|
49913
50458
|
.bp-cards{display:grid;gap:var(--space-4);grid-template-columns:repeat(auto-fill,minmax(240px,1fr))}
|
|
@@ -49949,8 +50494,8 @@ for(const s of document.querySelectorAll("[data-ink]")){const v=s.dataset.ink;if
|
|
|
49949
50494
|
if (/^#[0-9a-f]{8}$/i.test(h)) return h.slice(0, 7).toLowerCase();
|
|
49950
50495
|
return "#000000";
|
|
49951
50496
|
};
|
|
49952
|
-
swatch = (hex, label2) => `<span data-sw="${
|
|
49953
|
-
brokenFontsNote = (display, body) => `<p>Fonts as read from the site: headings "${
|
|
50497
|
+
swatch = (hex, label2) => `<span data-sw="${esc13(hex)}" title="${esc13(label2)} ${esc13(hex)}" role="img" aria-label="${esc13(label2)} ${esc13(hex)}"></span>`;
|
|
50498
|
+
brokenFontsNote = (display, body) => `<p>Fonts as read from the site: headings "${esc13(display)}", text "${esc13(body)}".</p>`;
|
|
49954
50499
|
SHOW_FIRST = 8;
|
|
49955
50500
|
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
50501
|
}
|
|
@@ -50160,9 +50705,9 @@ async function generateReview(spawnClaude, input) {
|
|
|
50160
50705
|
function reviewDocumentHtml(review, opts) {
|
|
50161
50706
|
const accent = opts.accent && /^#[0-9a-fA-F]{6}$/.test(opts.accent) ? opts.accent : "#B8934A";
|
|
50162
50707
|
const agency = opts.agencyName?.trim() || "";
|
|
50163
|
-
const
|
|
50708
|
+
const esc17 = (v) => String(v ?? "").replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" })[c]);
|
|
50164
50709
|
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">${
|
|
50710
|
+
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
50711
|
const o = review.offer;
|
|
50167
50712
|
const confirmed = opts.scope && !opts.scope.wholePreset && !opts.scope.awaitingConfirmation ? opts.scope : void 0;
|
|
50168
50713
|
const purchased = confirmed || opts.purchasedFeatureIds ? [.../* @__PURE__ */ new Set([
|
|
@@ -50172,8 +50717,8 @@ function reviewDocumentHtml(review, opts) {
|
|
|
50172
50717
|
const unbuilt = opts.scope?.notBuilt ?? [];
|
|
50173
50718
|
const represented = [...opts.scope?.built ?? [], ...unbuilt].map((f) => BUILD_ASSESSMENT_FEATURE[f.id]).concat(opts.scope && !opts.scope.awaitingConfirmation ? ["pipeline"] : []);
|
|
50174
50719
|
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>${
|
|
50720
|
+
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("");
|
|
50721
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${esc17(opts.clientName)} \u2014 Growth Blueprint</title><style>
|
|
50177
50722
|
@page{margin:0.6in}
|
|
50178
50723
|
*{box-sizing:border-box}
|
|
50179
50724
|
body{font-family:-apple-system,Segoe UI,Helvetica,sans-serif;color:#1f2430;margin:0;line-height:1.55}
|
|
@@ -50208,40 +50753,40 @@ function reviewDocumentHtml(review, opts) {
|
|
|
50208
50753
|
<div class="wrap">
|
|
50209
50754
|
<div class="cover">
|
|
50210
50755
|
<p class="eyebrow">Growth Blueprint</p>
|
|
50211
|
-
<h1>${
|
|
50212
|
-
<p class="sub">${agency ? `Prepared by ${
|
|
50756
|
+
<h1>${esc17(opts.clientName)}</h1>
|
|
50757
|
+
<p class="sub">${agency ? `Prepared by ${esc17(agency)} \xB7 ` : ""}${date}</p>
|
|
50213
50758
|
</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">${
|
|
50759
|
+
${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("")}` : ""}
|
|
50760
|
+
${review.headline ? `<div class="lead">${esc17(review.headline)}</div>` : ""}
|
|
50216
50761
|
${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> ${
|
|
50762
|
+
<p>${esc17(review.ica.who)}</p>
|
|
50763
|
+
${review.ica.painPoints?.length ? `<p class="kv"><b>What keeps them up:</b> ${esc17(review.ica.painPoints.join(" \xB7 "))}</p>` : ""}
|
|
50764
|
+
${review.ica.objections?.length ? `<p class="kv"><b>Why they hesitate:</b> ${esc17(review.ica.objections.join(" \xB7 "))}</p>` : ""}
|
|
50765
|
+
${review.ica.buyingTrigger ? `<p class="kv"><b>What makes them finally buy:</b> ${esc17(review.ica.buyingTrigger)}</p>` : ""}
|
|
50766
|
+
${review.ica.whereTheyAre ? `<p class="kv"><b>Where they already are:</b> ${esc17(review.ica.whereTheyAre)}</p>` : ""}` : ""}
|
|
50222
50767
|
${o ? `<div class="head">Your offer, graded</div>
|
|
50223
50768
|
<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> ${
|
|
50769
|
+
${o.dreamOutcome ? `<p class="kv"><b>Worth wanting:</b> ${esc17(o.dreamOutcome)}</p>` : ""}
|
|
50770
|
+
${o.believability ? `<p class="kv"><b>Believable:</b> ${esc17(o.believability)}</p>` : ""}
|
|
50771
|
+
${o.speed ? `<p class="kv"><b>Speed to result:</b> ${esc17(o.speed)}</p>` : ""}
|
|
50772
|
+
${o.effort ? `<p class="kv"><b>Effort asked of them:</b> ${esc17(o.effort)}</p>` : ""}
|
|
50773
|
+
${o.biggestLever ? `<p class="kv"><b>Biggest lever:</b> ${esc17(o.biggestLever)}</p>` : ""}
|
|
50774
|
+
${o.recommendedOffer ? `<div class="lead"><b>Run this instead:</b> ${esc17(o.recommendedOffer)}</div>` : ""}` : ""}
|
|
50230
50775
|
${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>${
|
|
50776
|
+
${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
50777
|
${opts.scope?.awaitingConfirmation ? `<div class="head">What this build includes</div>
|
|
50233
50778
|
<p>Not confirmed yet. The build waits until the features to build are confirmed.</p>
|
|
50234
50779
|
` : opts.scope ? `<div class="head">What this build includes</div>
|
|
50235
|
-
<p>${opts.scope.package ? `Built to your <b>${
|
|
50780
|
+
<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
50781
|
` : ""}
|
|
50237
50782
|
${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 ${
|
|
50783
|
+
<p>${esc17(opts.bought.assessment)}</p>
|
|
50784
|
+
${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>"}
|
|
50785
|
+
${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>` : ""}` : ""}
|
|
50786
|
+
${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>` : ""}
|
|
50787
|
+
${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
50788
|
${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 ?
|
|
50789
|
+
<footer><span>${agency ? esc17(agency) : "Growth Blueprint"}</span><span>${esc17(opts.clientName)} \xB7 ${date}</span></footer>
|
|
50245
50790
|
</div></body></html>`;
|
|
50246
50791
|
}
|
|
50247
50792
|
var GAP_CHECKLIST;
|
|
@@ -50361,7 +50906,7 @@ var init_client_fields = __esm({
|
|
|
50361
50906
|
});
|
|
50362
50907
|
|
|
50363
50908
|
// src/command-os/assessment/report.js
|
|
50364
|
-
function
|
|
50909
|
+
function esc14(s) {
|
|
50365
50910
|
return String(s === void 0 || s === null ? "" : s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
50366
50911
|
}
|
|
50367
50912
|
function isFigure(v) {
|
|
@@ -50381,7 +50926,7 @@ function renderReport2(opts) {
|
|
|
50381
50926
|
var assumptionLabels = opts.assumptionLabels || {};
|
|
50382
50927
|
var date = opts.date || "";
|
|
50383
50928
|
var accent = safeColour(agency.accent);
|
|
50384
|
-
var biz =
|
|
50929
|
+
var biz = esc14(state.biz || "your business");
|
|
50385
50930
|
var F = Array.isArray(r.F) ? r.F : [];
|
|
50386
50931
|
var O = Array.isArray(r.O) ? r.O : [];
|
|
50387
50932
|
var hrs = Array.isArray(r.hrs) ? r.hrs : [];
|
|
@@ -50392,23 +50937,23 @@ function renderReport2(opts) {
|
|
|
50392
50937
|
out.push("<title>Where " + biz + " is leaking</title>");
|
|
50393
50938
|
out.push("<style>" + css(accent) + "</style></head><body><div class=wrap>");
|
|
50394
50939
|
out.push("<header>");
|
|
50395
|
-
if (agency.name) out.push('<p class="by">Prepared by ' +
|
|
50940
|
+
if (agency.name) out.push('<p class="by">Prepared by ' + esc14(agency.name) + "</p>");
|
|
50396
50941
|
out.push("<h1>Where " + biz + " is leaking</h1>");
|
|
50397
|
-
var who = state.client && state.client.name ?
|
|
50942
|
+
var who = state.client && state.client.name ? esc14(state.client.name) + (state.client.role ? ", " + esc14(state.client.role) : "") : "";
|
|
50398
50943
|
if (who) out.push('<p class="for">Prepared for ' + who + "</p>");
|
|
50399
|
-
if (date) out.push('<p class="when">' +
|
|
50944
|
+
if (date) out.push('<p class="when">' + esc14(date) + "</p>");
|
|
50400
50945
|
if (agency.method || agency.promise) {
|
|
50401
|
-
out.push('<p class="about">' + [agency.method, agency.promise].filter(Boolean).map(
|
|
50946
|
+
out.push('<p class="about">' + [agency.method, agency.promise].filter(Boolean).map(esc14).join(" ") + "</p>");
|
|
50402
50947
|
}
|
|
50403
50948
|
out.push("</header>");
|
|
50404
50949
|
if (r.verdict === "insufficient") {
|
|
50405
|
-
out.push('<div class="verdict stop"><b>We could not put a number on this yet</b>' +
|
|
50950
|
+
out.push('<div class="verdict stop"><b>We could not put a number on this yet</b>' + esc14(r.line || "") + "</div>");
|
|
50406
50951
|
out.push(measureFirst(O));
|
|
50407
50952
|
out.push(footer(agency));
|
|
50408
50953
|
return out.join("\n") + "</div></body></html>";
|
|
50409
50954
|
}
|
|
50410
50955
|
if (r.verdict === "thin") {
|
|
50411
|
-
out.push('<div class="verdict warn"><b>Honestly, there is not much here</b>' +
|
|
50956
|
+
out.push('<div class="verdict warn"><b>Honestly, there is not much here</b>' + esc14(r.line || "") + "</div>");
|
|
50412
50957
|
}
|
|
50413
50958
|
out.push('<div class="hgrid">');
|
|
50414
50959
|
if (r.hrsWk > 0) out.push(head3(r.hrsWk.toFixed(1), "hours a week going into work a system should do"));
|
|
@@ -50420,7 +50965,7 @@ function renderReport2(opts) {
|
|
|
50420
50965
|
for (var a = 0; a < opts.answered.length; a++) {
|
|
50421
50966
|
var q3 = opts.answered[a];
|
|
50422
50967
|
var numeric = q3.unit !== void 0 ? q3.unit !== "text" : isFigure(q3.value);
|
|
50423
|
-
out.push("<tr><td>" +
|
|
50968
|
+
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
50969
|
}
|
|
50425
50970
|
out.push("</table>");
|
|
50426
50971
|
}
|
|
@@ -50429,15 +50974,15 @@ function renderReport2(opts) {
|
|
|
50429
50974
|
out.push('<tr><th>What is happening</th><th class="num">A month</th></tr>');
|
|
50430
50975
|
for (var i = 0; i < F.length; i++) {
|
|
50431
50976
|
var f = F[i];
|
|
50432
|
-
var note = f.assumption ? '<span class="assume">' +
|
|
50433
|
-
out.push("<tr><td><strong>" +
|
|
50977
|
+
var note = f.assumption ? '<span class="assume">' + esc14(assumptionLabels[f.assumption] || "This is your assumption, not a measurement.") + "</span>" : "";
|
|
50978
|
+
out.push("<tr><td><strong>" + esc14(f.title) + "</strong>" + note + '</td><td class="num">' + money6(f.monthly) + "</td></tr>");
|
|
50434
50979
|
}
|
|
50435
50980
|
out.push('<tr class="tot"><td><strong>Leaking, every month</strong></td><td class="num"><strong>' + money6(r.monthly) + "</strong></td></tr>");
|
|
50436
50981
|
if (r.stop > 0) {
|
|
50437
|
-
out.push('<tr><td><strong>What we expect to actually stop</strong><span class="assume">' +
|
|
50982
|
+
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
50983
|
}
|
|
50439
50984
|
if (r.oneoff !== null && r.oneoff !== void 0 && r.oneoff > 0) {
|
|
50440
|
-
out.push('<tr><td>Your dormant list, worked once<span class="assume">' +
|
|
50985
|
+
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
50986
|
}
|
|
50442
50987
|
out.push("</table>");
|
|
50443
50988
|
}
|
|
@@ -50448,14 +50993,14 @@ function renderReport2(opts) {
|
|
|
50448
50993
|
out.push("<h2>Named, and deliberately not priced</h2><ul>");
|
|
50449
50994
|
for (var j = 0; j < named.length; j++) {
|
|
50450
50995
|
var o = named[j];
|
|
50451
|
-
out.push("<li><strong>" +
|
|
50996
|
+
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
50997
|
}
|
|
50453
50998
|
out.push("</ul><p class=small>We can see these matter and we are not going to invent a number for them.</p>");
|
|
50454
50999
|
}
|
|
50455
51000
|
if (hrs.length) {
|
|
50456
51001
|
out.push("<h2>Where the time goes</h2><table>");
|
|
50457
51002
|
for (var h = 0; h < hrs.length; h++) {
|
|
50458
|
-
out.push("<tr><td>" +
|
|
51003
|
+
out.push("<tr><td>" + esc14(hrs[h].t) + '</td><td class="num">' + Number(hrs[h].h).toFixed(1) + " hrs</td></tr>");
|
|
50459
51004
|
}
|
|
50460
51005
|
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
51006
|
}
|
|
@@ -50467,7 +51012,7 @@ function renderReport2(opts) {
|
|
|
50467
51012
|
var svc = services.find(function(s) {
|
|
50468
51013
|
return Array.isArray(s.covers) && s.covers.indexOf(fx.id) !== -1;
|
|
50469
51014
|
});
|
|
50470
|
-
out.push("<tr><td>" +
|
|
51015
|
+
out.push("<tr><td>" + esc14(fx.title) + "</td><td>" + esc14(svc ? svc.name : fx.fix || "") + "</td></tr>");
|
|
50471
51016
|
}
|
|
50472
51017
|
out.push("</table>");
|
|
50473
51018
|
if (!services.length) {
|
|
@@ -50483,7 +51028,7 @@ function renderReport2(opts) {
|
|
|
50483
51028
|
}, 0);
|
|
50484
51029
|
out.push("<h2>The investment</h2><table>");
|
|
50485
51030
|
for (var p = 0; p < priced.length; p++) {
|
|
50486
|
-
out.push("<tr><td>" +
|
|
51031
|
+
out.push("<tr><td>" + esc14(priced[p].name) + '</td><td class="num">' + money6(priced[p].price) + "</td></tr>");
|
|
50487
51032
|
}
|
|
50488
51033
|
out.push('<tr class="tot"><td><strong>Total</strong></td><td class="num"><strong>' + money6(total) + "</strong></td></tr>");
|
|
50489
51034
|
out.push('<tr><td>Against what we expect to stop, first year</td><td class="num">' + money6(r.annualStop) + "</td></tr>");
|
|
@@ -50498,7 +51043,7 @@ function renderReport2(opts) {
|
|
|
50498
51043
|
return out.join("\n") + "</div></body></html>";
|
|
50499
51044
|
}
|
|
50500
51045
|
function head3(n, label2) {
|
|
50501
|
-
return '<div><span class="hn">' +
|
|
51046
|
+
return '<div><span class="hn">' + esc14(n) + '</span><span class="hl">' + esc14(label2) + "</span></div>";
|
|
50502
51047
|
}
|
|
50503
51048
|
function measureFirst(O) {
|
|
50504
51049
|
var seen = [];
|
|
@@ -50507,18 +51052,18 @@ function measureFirst(O) {
|
|
|
50507
51052
|
for (var j = 0; j < m.length; j++) if (seen.indexOf(m[j]) === -1) seen.push(m[j]);
|
|
50508
51053
|
}
|
|
50509
51054
|
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(
|
|
51055
|
+
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
51056
|
}
|
|
50512
51057
|
function sourceLine(state, r, labels) {
|
|
50513
51058
|
var used = [];
|
|
50514
51059
|
var F = r.F || [];
|
|
50515
51060
|
for (var i = 0; i < F.length; i++) if (F[i].assumption && used.indexOf(F[i].assumption) === -1) used.push(F[i].assumption);
|
|
50516
51061
|
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 " +
|
|
51062
|
+
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
51063
|
}
|
|
50519
51064
|
function footer(agency) {
|
|
50520
51065
|
if (!agency.name) return "";
|
|
50521
|
-
return "<footer>" +
|
|
51066
|
+
return "<footer>" + esc14(agency.name) + (agency.contact ? " · " + esc14(agency.contact) : "") + (agency.guarantee ? '<br><span class="guarantee">' + esc14(agency.guarantee) + "</span>" : "") + "</footer>";
|
|
50522
51067
|
}
|
|
50523
51068
|
function css(accent) {
|
|
50524
51069
|
return [
|
|
@@ -51788,9 +52333,11 @@ __export(dashboard_exports, {
|
|
|
51788
52333
|
APPROVAL_WITHDRAWN_FOR_ASSESSMENT: () => APPROVAL_WITHDRAWN_FOR_ASSESSMENT,
|
|
51789
52334
|
APPROVAL_WITHDRAWN_FOR_SCOPE: () => APPROVAL_WITHDRAWN_FOR_SCOPE,
|
|
51790
52335
|
ASSESSMENT_CHANGED_SINCE_APPROVAL: () => ASSESSMENT_CHANGED_SINCE_APPROVAL,
|
|
52336
|
+
BRIEF_NEEDS_INTAKE: () => BRIEF_NEEDS_INTAKE2,
|
|
51791
52337
|
CHOOSE_BEFORE_BUILD: () => CHOOSE_BEFORE_BUILD,
|
|
51792
52338
|
CHOOSE_WHAT_THEY_BOUGHT: () => CHOOSE_WHAT_THEY_BOUGHT,
|
|
51793
52339
|
CLAUDE_REPAIR_MESSAGES: () => CLAUDE_REPAIR_MESSAGES,
|
|
52340
|
+
DOC_NOT_SAVED: () => DOC_NOT_SAVED,
|
|
51794
52341
|
INTAKE_ANSWERS_MISSING: () => INTAKE_ANSWERS_MISSING,
|
|
51795
52342
|
INTAKE_CONNECT_REQUIRED: () => INTAKE_CONNECT_REQUIRED,
|
|
51796
52343
|
INTAKE_FORM_REQUIRED: () => INTAKE_FORM_REQUIRED,
|
|
@@ -51841,10 +52388,13 @@ __export(dashboard_exports, {
|
|
|
51841
52388
|
guarded: () => guarded,
|
|
51842
52389
|
hasChosenScope: () => hasChosenScope,
|
|
51843
52390
|
insideDir: () => insideDir,
|
|
52391
|
+
intakeErrorDetail: () => intakeErrorDetail,
|
|
51844
52392
|
intakeInput: () => intakeInput,
|
|
52393
|
+
intakeRefusal: () => intakeRefusal,
|
|
51845
52394
|
intakeRequestError: () => intakeRequestError,
|
|
51846
52395
|
intakeUnavailableError: () => intakeUnavailableError,
|
|
51847
52396
|
isAllowedCockpitHost: () => isAllowedCockpitHost,
|
|
52397
|
+
isBriefNeedsIntake: () => isBriefNeedsIntake,
|
|
51848
52398
|
isLoopback: () => isLoopback,
|
|
51849
52399
|
isUpstreamFailure: () => isUpstreamFailure,
|
|
51850
52400
|
launcherAlreadyOffered: () => launcherAlreadyOffered,
|
|
@@ -51866,6 +52416,7 @@ __export(dashboard_exports, {
|
|
|
51866
52416
|
readFailureText: () => readFailureText,
|
|
51867
52417
|
readOverlay: () => readOverlay,
|
|
51868
52418
|
readRunCosts: () => readRunCosts,
|
|
52419
|
+
readSavedClientDoc: () => readSavedClientDoc,
|
|
51869
52420
|
readableAccount: () => readableAccount,
|
|
51870
52421
|
recordLauncherOffered: () => recordLauncherOffered,
|
|
51871
52422
|
recoverStuckStages: () => recoverStuckStages,
|
|
@@ -51887,8 +52438,11 @@ __export(dashboard_exports, {
|
|
|
51887
52438
|
setClientScope: () => setClientScope,
|
|
51888
52439
|
setPending: () => setPending,
|
|
51889
52440
|
setStage: () => setStage,
|
|
52441
|
+
statusFromThisRun: () => statusFromThisRun,
|
|
51890
52442
|
uniqueDocName: () => uniqueDocName,
|
|
51891
52443
|
unknownCapabilityHtml: () => unknownCapabilityHtml,
|
|
52444
|
+
unverifiedStaffNames: () => unverifiedStaffNames,
|
|
52445
|
+
verifiedStaffNames: () => verifiedStaffNames,
|
|
51892
52446
|
verifyPanelOutcome: () => verifyPanelOutcome,
|
|
51893
52447
|
withBoughtJobs: () => withBoughtJobs,
|
|
51894
52448
|
withCockpitShell: () => withCockpitShell,
|
|
@@ -52110,8 +52664,19 @@ async function resolvePlan2(creds) {
|
|
|
52110
52664
|
}
|
|
52111
52665
|
return { error: "Could not verify the license (offline and no valid cached attestation)." };
|
|
52112
52666
|
}
|
|
52667
|
+
function unverifiedStaffNames(check) {
|
|
52668
|
+
return (check?.missing ?? []).map((m) => String(m.name ?? "").trim()).filter(Boolean);
|
|
52669
|
+
}
|
|
52670
|
+
function statusFromThisRun(status, runStartedAt) {
|
|
52671
|
+
if (!status?.readAt || !runStartedAt) return void 0;
|
|
52672
|
+
return Date.parse(status.readAt) >= Date.parse(runStartedAt) ? status : void 0;
|
|
52673
|
+
}
|
|
52674
|
+
function verifiedStaffNames(check) {
|
|
52675
|
+
return (check?.matched ?? []).map((m) => String(m.name ?? "").trim()).filter(Boolean);
|
|
52676
|
+
}
|
|
52113
52677
|
function verifyPanelOutcome(value) {
|
|
52114
|
-
|
|
52678
|
+
const shown = value.plain?.summary?.trim() ? value.plain : { summary: value.summary, issues: value.issues };
|
|
52679
|
+
return { ...shown, summary: quietRemovedClaimLine(shown.summary) };
|
|
52115
52680
|
}
|
|
52116
52681
|
function startRun(locationId2, stage, onDone, meta2 = {}) {
|
|
52117
52682
|
const rk = clientRecordKey(locationId2, meta2.clientKey);
|
|
@@ -52145,6 +52710,8 @@ function startRun(locationId2, stage, onDone, meta2 = {}) {
|
|
|
52145
52710
|
// A build re-run reuses the approved plan already saved for this account (finding 10).
|
|
52146
52711
|
// ...and only while it still builds the scope the member has confirmed now: a
|
|
52147
52712
|
// changed selection composes the plan fresh from the preset (delta round 3, T-M4).
|
|
52713
|
+
// Rehearsal 5 (BUG-2026-09-30-10): Build is told who the staff check matched in this account.
|
|
52714
|
+
...stage === 3 ? { verifiedStaff: verifiedStaffNames(state.staffChecks?.[locationId2]) } : {},
|
|
52148
52715
|
savedPlan: (() => {
|
|
52149
52716
|
const sp = stage === 3 ? readSavedPlan(rk) : null;
|
|
52150
52717
|
return sp && sp.inputHash === meta2.inputHash && savedPlanMatchesScope(sp, state.scope?.[rk]) ? { planId: sp.planId, savedAt: sp.savedAt, summary: planSummary(sp) } : void 0;
|
|
@@ -52220,6 +52787,12 @@ function startRun(locationId2, stage, onDone, meta2 = {}) {
|
|
|
52220
52787
|
outcome.issues = [];
|
|
52221
52788
|
outcome.plain = { summary: outcome.error, issues: [] };
|
|
52222
52789
|
}
|
|
52790
|
+
const briefNeedsIntake = stage === 1 && !outcome.ok && isBriefNeedsIntake(outcome.error);
|
|
52791
|
+
if (briefNeedsIntake) {
|
|
52792
|
+
outcome.summary = BRIEF_NEEDS_INTAKE2;
|
|
52793
|
+
outcome.issues = [];
|
|
52794
|
+
outcome.plain = { summary: BRIEF_NEEDS_INTAKE2, issues: [] };
|
|
52795
|
+
}
|
|
52223
52796
|
if (stage === 3 && !outcome.summary && !outcome.issues?.length) {
|
|
52224
52797
|
try {
|
|
52225
52798
|
const st = readCockpitState();
|
|
@@ -52227,19 +52800,22 @@ function startRun(locationId2, stage, onDone, meta2 = {}) {
|
|
|
52227
52800
|
} catch {
|
|
52228
52801
|
}
|
|
52229
52802
|
}
|
|
52803
|
+
const buildFacts = stage === 3 ? { verifiedStaff: context2.verifiedStaff ?? [], unverifiedStaff: unverifiedStaffNames(state.staffChecks?.[locationId2]), customValues: statusFromThisRun(readSavedPlan(rk)?.customValueStatus, run.startedAt) } : void 0;
|
|
52804
|
+
if (buildFacts) Object.assign(outcome, guardBuildReport({ summary: outcome.summary, issues: outcome.issues }, buildFacts));
|
|
52230
52805
|
if (stage === 4 || outcome.summary || outcome.issues?.length) {
|
|
52231
52806
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
52232
52807
|
const save = (plain) => {
|
|
52233
52808
|
const st = readCockpitState();
|
|
52234
|
-
writeCockpitState({ ...st, outcomes: { ...st.outcomes ?? {}, [`${rk}:${stage}`]: { ok: outcome.ok, error: outcome.error, summary: outcome.summary, issues: outcome.issues, ...plain && { plain }, at, inputHash: runInputHash, ...stage === 4 ? { buildHash: runBuildHash, verification: verifyRaw?.verification } : {}, hash: contentHash({ inputHash: runInputHash, at, ok: outcome.ok, summary: outcome.summary, issues: outcome.issues }) } } });
|
|
52809
|
+
writeCockpitState({ ...st, outcomes: { ...st.outcomes ?? {}, [`${rk}:${stage}`]: { ok: outcome.ok, error: outcome.error, summary: outcome.summary, issues: outcome.issues, ...plain && { plain }, ...briefNeedsIntake && { reason: "intake-needed" }, at, inputHash: runInputHash, ...stage === 4 ? { buildHash: runBuildHash, verification: verifyRaw?.verification } : {}, hash: contentHash({ inputHash: runInputHash, at, ok: outcome.ok, summary: outcome.summary, issues: outcome.issues }) } } });
|
|
52235
52810
|
};
|
|
52236
|
-
save(scopeRefusal ? outcome.plain : void 0);
|
|
52237
|
-
if (PLAIN_REPORT_STAGES.has(stage) && !scopeRefusal) {
|
|
52811
|
+
save(scopeRefusal || briefNeedsIntake ? outcome.plain : void 0);
|
|
52812
|
+
if (PLAIN_REPORT_STAGES.has(stage) && !scopeRefusal && !briefNeedsIntake) {
|
|
52238
52813
|
run.lines.push("Writing the plain-English version of what came back\u2026");
|
|
52239
|
-
const
|
|
52814
|
+
const rewritten = await rewriteOutcomePlain(STAGES2[stage], verifyRaw ?? outcome, { onEvent: (l) => run.lines.push(l), onCost: (costUsd) => {
|
|
52240
52815
|
recordRunCost({ at, locationId: locationId2, stage, kind: "plain rewrite", ok: true, costUsd });
|
|
52241
52816
|
run.lines.push(`Plain rewrite: ${costUsd === void 0 ? "cost not reported" : formatUsd(costUsd)}.`);
|
|
52242
52817
|
} });
|
|
52818
|
+
const plain = buildFacts ? guardBuildReport(rewritten, buildFacts) : rewritten;
|
|
52243
52819
|
save(plain);
|
|
52244
52820
|
}
|
|
52245
52821
|
}
|
|
@@ -52408,7 +52984,7 @@ function clientDocsDir(locationId2, base = appDataDir()) {
|
|
|
52408
52984
|
return path40.join(base, "clients", safeLoc, "documents");
|
|
52409
52985
|
}
|
|
52410
52986
|
function safeDocName(name) {
|
|
52411
|
-
const cleaned = path40.basename(String(name ?? "")).replace(/[^A-Za-z0-9._ -]/g, "_").replace(/\s+/g, " ").trim();
|
|
52987
|
+
const cleaned = path40.basename(String(name ?? "")).replace(/[^A-Za-z0-9._ ()-]/g, "_").replace(/\s+/g, " ").trim();
|
|
52412
52988
|
const ext = path40.extname(cleaned).toLowerCase();
|
|
52413
52989
|
if (!cleaned || cleaned.startsWith(".") || !DOC_EXTS.has(ext)) throw new Error("Only plain text (.txt, .md, .csv), .vtt and .srt files can be saved here.");
|
|
52414
52990
|
const stem = cleaned.slice(0, cleaned.length - ext.length).slice(0, 120 - ext.length).trim();
|
|
@@ -52569,6 +53145,16 @@ function insideDir(dir, p) {
|
|
|
52569
53145
|
return false;
|
|
52570
53146
|
}
|
|
52571
53147
|
}
|
|
53148
|
+
function readSavedClientDoc(locationId2, file2, base = appDataDir()) {
|
|
53149
|
+
const name = safeDocName(file2);
|
|
53150
|
+
const dir = clientDocsDir(locationId2, base);
|
|
53151
|
+
const p = path40.join(dir, name);
|
|
53152
|
+
if (!fs42.existsSync(p)) throw new Error(DOC_NOT_SAVED);
|
|
53153
|
+
if (fs42.lstatSync(p).isSymbolicLink() || !insideDir(dir, p)) throw new Error(DOC_NOT_SAVED);
|
|
53154
|
+
const text2 = fs42.readFileSync(p, "utf8");
|
|
53155
|
+
if (text2.length > 6e4) throw new Error("The limit is 60,000 characters. Paste an excerpt or shorten this text file.");
|
|
53156
|
+
return text2;
|
|
53157
|
+
}
|
|
52572
53158
|
function uniqueDocName(dir, name) {
|
|
52573
53159
|
const ext = path40.extname(name), stem = name.slice(0, name.length - ext.length);
|
|
52574
53160
|
let candidate = name;
|
|
@@ -52607,9 +53193,12 @@ function intakeUnavailableError(snap) {
|
|
|
52607
53193
|
const reason3 = snap.unavailableReason;
|
|
52608
53194
|
return new Error(reason3 === "auth" ? INTAKE_UNREADABLE : reason3 === "missing" ? INTAKE_ANSWERS_MISSING : reason3 === "error" ? INTAKE_READ_FAILED : INTAKE_READ_TRANSIENT);
|
|
52609
53195
|
}
|
|
53196
|
+
function isBriefNeedsIntake(error) {
|
|
53197
|
+
return typeof error === "string" && /^\s*First complete the intake\b/i.test(error);
|
|
53198
|
+
}
|
|
52610
53199
|
function runRefusal(e) {
|
|
52611
53200
|
const error = e && typeof e.message === "string" ? e.message : "bad request";
|
|
52612
|
-
return error === INTAKE_ANSWERS_MISSING ? { ok: false, error, reason: "intake-missing" } : error === RUN_IN_PROGRESS ? { ok: false, error, reason: "busy" } : { ok: false, error };
|
|
53201
|
+
return error === INTAKE_ANSWERS_MISSING ? { ok: false, error, reason: "intake-missing" } : error === RUN_IN_PROGRESS ? { ok: false, error, reason: "busy" } : error === BRIEF_NEEDS_INTAKE2 ? { ok: false, error, reason: "intake-needed" } : { ok: false, error };
|
|
52613
53202
|
}
|
|
52614
53203
|
function planClaimChecks(saved) {
|
|
52615
53204
|
const plan = saved?.plan && typeof saved.plan === "object" ? saved.plan : void 0;
|
|
@@ -52642,6 +53231,16 @@ function intakeRequestError(e) {
|
|
|
52642
53231
|
const text2 = e instanceof Error ? e.message : "";
|
|
52643
53232
|
return INTAKE_OWN_MESSAGES.includes(text2) ? text2 : "The intake request did not finish. Retry this step.";
|
|
52644
53233
|
}
|
|
53234
|
+
function intakeErrorDetail(e) {
|
|
53235
|
+
const raw = e instanceof Error ? e.message : typeof e === "string" ? e : "";
|
|
53236
|
+
if (!raw || INTAKE_OWN_MESSAGES.includes(raw)) return void 0;
|
|
53237
|
+
const clean4 = raw.replace(/(?:[A-Za-z]:)?(?:[\\/][^\s"'\\/]+){2,}[\\/]?/g, "[path]").replace(/\b(?:pit|sk|key|token|bearer)[-_ ][A-Za-z0-9._-]{6,}/gi, "[hidden]").replace(/\b[A-Za-z0-9_-]{24,}\b/g, "[hidden]").replace(/\s+/g, " ").trim();
|
|
53238
|
+
return clean4 ? clean4.length > 240 ? clean4.slice(0, 237) + "\u2026" : clean4 : void 0;
|
|
53239
|
+
}
|
|
53240
|
+
function intakeRefusal(e) {
|
|
53241
|
+
const detail = intakeErrorDetail(e);
|
|
53242
|
+
return { ok: false, error: intakeRequestError(e), ...detail ? { detail } : {} };
|
|
53243
|
+
}
|
|
52645
53244
|
function escHtml2(s) {
|
|
52646
53245
|
return String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
52647
53246
|
}
|
|
@@ -52794,6 +53393,10 @@ function initials(name){const p=String(name||"").trim().split(/\\s+/);return ((p
|
|
|
52794
53393
|
/* Command Design System pieces, built as text (never markup from data). */
|
|
52795
53394
|
function mk(tag,cls,text){const e=document.createElement(tag);if(cls)e.className=cls;if(text!=null)e.textContent=text;return e}
|
|
52796
53395
|
function chip(tone,text){return mk("span","cds-chip"+(tone?" "+tone:""),text)}
|
|
53396
|
+
/* Sentences joined for display (rehearsal 5, BUG-2026-09-30-17): a part that already ends a sentence
|
|
53397
|
+
gets no second full stop, so "cannot send." + "Buy a local..." never reads "cannot send.. Buy". */
|
|
53398
|
+
function endSentence(s){const t=String(s==null?"":s).trim();return !t||/[.!?]["')\\]]?$/.test(t)?t:t+"."}
|
|
53399
|
+
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
53400
|
function panel(label,title,meta){const s=mk("section","cds-panel");const h=mk("div","cds-panel-head");const t=mk("div","txt");
|
|
52798
53401
|
if(label)t.appendChild(mk("div","cds-panel-label",label));if(title)t.appendChild(mk("h2","cds-panel-title",title));h.appendChild(t);
|
|
52799
53402
|
if(meta){const m=mk("div","cds-panel-meta");m.appendChild(meta);h.appendChild(m)}s.appendChild(h);return s}
|
|
@@ -52966,7 +53569,7 @@ function preparedFormBlock(box,id){const p=iPrepared[id];
|
|
|
52966
53569
|
"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
53570
|
for(const l of left){const li=document.createElement("li");li.textContent=l.label+": ";
|
|
52968
53571
|
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)
|
|
53572
|
+
li.appendChild(document.createTextNode(". "+endSentence(WHY[l.reason]||l.reason)));ul.appendChild(li)}
|
|
52970
53573
|
box.appendChild(ul)}
|
|
52971
53574
|
const rf=document.createElement("button");rf.className="cds-btn ghost";rf.textContent="Refresh status";
|
|
52972
53575
|
rf.onclick=()=>iLoad(id);box.appendChild(rf)}
|
|
@@ -52994,7 +53597,8 @@ function outcomePanel(id,i,state){const oc=(state.outcomes||{})[id+":"+i];if(!oc
|
|
|
52994
53597
|
t.textContent="Every planned item: "+part.split(NL).filter(l=>l.startsWith(" ")&&l.trim().length>0).length+" read back from the account";box.appendChild(t);
|
|
52995
53598
|
const body=document.createElement("p");body.className="runlog";body.textContent=part.split(NL).slice(1).join(NL);
|
|
52996
53599
|
box.appendChild(body);d.appendChild(box);continue}
|
|
52997
|
-
|
|
53600
|
+
/* Notes from the check are background, drawn quietly (test 01, BUG-2026-09-30-36). */
|
|
53601
|
+
const p=document.createElement("p");if(i===4)p.className=/^Notes from the check/.test(part)?"runlog muted small vnotes":"runlog";p.textContent=part;d.appendChild(p)}}
|
|
52998
53602
|
for(const x of iss){const g=document.createElement("div");g.className="cds-callout warn pendpanel";
|
|
52999
53603
|
g.textContent=x;d.appendChild(g)}
|
|
53000
53604
|
const when=document.createElement("p");when.className="cds-src";
|
|
@@ -53058,7 +53662,7 @@ let iFieldN=0;
|
|
|
53058
53662
|
function iField(id,q,val,prefilled){const w=document.createElement("div");w.className="cds-field ifield"+(prefilled?" prefilled":"");
|
|
53059
53663
|
const fid="ifield-"+(++iFieldN);
|
|
53060
53664
|
const l=document.createElement("label");l.htmlFor=fid;l.textContent=q.label+(q.required?" *":"");if(prefilled)l.setAttribute("data-src",iPreSrc[id]||"your call");w.appendChild(l);
|
|
53061
|
-
if(prefilled)w.appendChild(mk("div","hint","Filled in from "+(
|
|
53665
|
+
if(prefilled)w.appendChild(mk("div","hint","Filled in from "+iSrcWords(id)+". Check it."));
|
|
53062
53666
|
let inp;
|
|
53063
53667
|
if(q.multi&&q.options&&q.options.length){
|
|
53064
53668
|
// Several answers allowed (the GHL field is multi-choice) . checkboxes, stored as a list. (2026-08-25, finding 8)
|
|
@@ -53152,17 +53756,13 @@ function intakeWorkspace(id,name){const d=iData[id];const box=document.createEle
|
|
|
53152
53756
|
:"Your own Claude asks the recorder you connected (Plaud, Otter, Fireflies\u2026) for that call; the transcript then goes to your Claude account and nowhere else.";
|
|
53153
53757
|
const r=await iWork(id,async()=>(await clientPageRequest("/api/intake-prefill",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)})).json());
|
|
53154
53758
|
b.disabled=false;b.textContent="Fill the intake from this";
|
|
53155
|
-
if(!r.ok){st
|
|
53156
|
-
|
|
53759
|
+
if(!r.ok){iFailNote(st,r);return}
|
|
53760
|
+
for(const n of ["transcript","url","recorder"])delete iDraft[id+":"+n];
|
|
53157
53761
|
/* Codex F2 (2026-09-06): a HELD answer (the words were said, the value was worked out) is a
|
|
53158
53762
|
suggestion beside the field, never an answer. It reaches iAnswers only when the operator
|
|
53159
|
-
clicks "Use it". Unverified answers (read by their Claude, not
|
|
53160
|
-
but the banner names them. */
|
|
53161
|
-
|
|
53162
|
-
iHeld[id]=r.held||{};iUnv[id]=r.unverified||[];
|
|
53163
|
-
const heldKeys=Object.keys(iHeld[id]),unv=iUnv[id];
|
|
53164
|
-
iNote[id]=(heldKeys.length?heldKeys.length+" answer"+(heldKeys.length===1?" was":"s were")+" worked out rather than heard and NOT filled in: "+heldKeys.join(", ")+". Each is shown beside its field with a Use it button. ":"")+(unv.length?unv.length+" could not be checked against the material because your Claude read it rather than you pasting it here: "+unv.join(", ")+". Check them.":"");
|
|
53165
|
-
iMode[id]="fill";render(lastState,lastRun)};
|
|
53763
|
+
clicks "Use it" or "Use all suggestions". Unverified answers (read by their Claude, not
|
|
53764
|
+
pasted here) are answers, but the banner names them. */
|
|
53765
|
+
iAfterFill(id,r,sub==="url"?"the link":"your call")};
|
|
53166
53766
|
b.onclick=()=>readIt(false);icAfter(b,limitKey);
|
|
53167
53767
|
const urlIn=document.createElement("input");urlIn.placeholder="https://\u2026 a shared transcript or recording page";
|
|
53168
53768
|
urlIn.className="cds-input";urlIn.setAttribute("aria-label","Link to the transcript or recording");keepDraft(urlIn,id,"url");
|
|
@@ -53198,8 +53798,8 @@ function intakeWorkspace(id,name){const d=iData[id];const box=document.createEle
|
|
|
53198
53798
|
rb.onclick=async()=>{rb.disabled=true;rb.textContent="Reading it\u2026";st.textContent="The text of "+f.name+" goes to your own Claude account to be read, and nowhere else.";
|
|
53199
53799
|
const r=await iWork(id,async()=>(await clientPageRequest("/api/intake-prefill",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({locationId:id,labels:d.questions.map(q=>q.label),file:f.name})})).json());
|
|
53200
53800
|
rb.disabled=false;rb.textContent="Fill the intake from this";
|
|
53201
|
-
if(!r.ok){st
|
|
53202
|
-
|
|
53801
|
+
if(!r.ok){iFailNote(st,r);return}
|
|
53802
|
+
iAfterFill(id,r,"the document")};
|
|
53203
53803
|
const end=mk("span","end");end.appendChild(rb);row.appendChild(end);list.appendChild(row)}
|
|
53204
53804
|
|
|
53205
53805
|
w.appendChild(list);
|
|
@@ -53209,9 +53809,7 @@ function intakeWorkspace(id,name){const d=iData[id];const box=document.createEle
|
|
|
53209
53809
|
if(iSub[id]!=="doc"){w.appendChild(b);w.appendChild(st)}box.appendChild(w)}
|
|
53210
53810
|
if(mode==="fill"||mode==="interview"){
|
|
53211
53811
|
const qs=d.questions;const ans=iAnswers[id]||{};const pre=iPre[id]||[];
|
|
53212
|
-
if(
|
|
53213
|
-
banner.textContent=pre.length+" answers pre-filled from "+(iPreSrc[id]||"your call")+". Correct anything wrong; "+(qs.length-pre.length)+" still need you.";box.appendChild(banner)}
|
|
53214
|
-
if(mode==="fill"&&iNote[id]){const nb=document.createElement("div");nb.className="cds-callout warn pendpanel";nb.textContent=iNote[id];box.appendChild(nb)}
|
|
53812
|
+
if(mode==="fill")iFillBanners(id,qs,box);
|
|
53215
53813
|
if(mode==="interview"){const i=Math.min(iIdx[id]||0,qs.length-1);const q=qs[i];
|
|
53216
53814
|
const prog=document.createElement("p");prog.className="cds-panel-label";prog.textContent="Question "+(i+1)+" of "+qs.length+" \xB7 "+q.sectionLabel;
|
|
53217
53815
|
const qq=document.createElement("div");qq.className="cds-panel-title iq";qq.textContent=q.label;
|
|
@@ -53231,13 +53829,13 @@ function intakeWorkspace(id,name){const d=iData[id];const box=document.createEle
|
|
|
53231
53829
|
{const rp=iReplacePanel(id,q.label);if(rp)box.appendChild(rp)}
|
|
53232
53830
|
const held=(iHeld[id]||{})[q.label];
|
|
53233
53831
|
if(held){const hr=document.createElement("div");hr.className="cds-callout warn cds-row";
|
|
53234
|
-
const ht=document.createElement("span");ht.textContent="
|
|
53832
|
+
const ht=document.createElement("span");ht.textContent="Suggested from "+iSrcWords(id)+": \u201C"+iShowVal(held.value)+"\u201D. "+(held.because||"");hr.appendChild(ht);
|
|
53235
53833
|
const use=document.createElement("button");use.className="cds-btn ghost";use.textContent="Use it";
|
|
53236
53834
|
const heldKey="replaceheld:"+id+":"+q.label;
|
|
53237
53835
|
const dropHeld=()=>{const h=Object.assign({},iHeld[id]);delete h[q.label];iHeld[id]=h};
|
|
53238
53836
|
use.onclick=()=>{const cur=(iAnswers[id]||{})[q.label];
|
|
53239
53837
|
if(iConflicts(iAnswers[id]||{},q.label,held.value))return icShow(hr,heldKey,iReplaceSpec(id,q.label,cur,held.value,dropHeld));
|
|
53240
|
-
|
|
53838
|
+
iUseHeld(id,{[q.label]:held.value});dropHeld();render(lastState,lastRun)};
|
|
53241
53839
|
hr.appendChild(use);box.appendChild(icAfter(hr,heldKey))}}
|
|
53242
53840
|
if(hasForm){const save=document.createElement("button");save.className=d.connectionPending?"cds-btn ghost":"cds-btn";
|
|
53243
53841
|
save.textContent="Review and submit the real form";save.disabled=Boolean(d.connectionPending);
|
|
@@ -53264,6 +53862,45 @@ function intakeWorkspace(id,name){const d=iData[id];const box=document.createEle
|
|
|
53264
53862
|
if(hasForm&&iPrepared[id])preparedFormBlock(box,id)}}
|
|
53265
53863
|
return box}
|
|
53266
53864
|
let iNote={},iHeld={},iUnv={};
|
|
53865
|
+
/* A failed read says the plain sentence, and the real reason under Details (BUG-2026-09-30-38). */
|
|
53866
|
+
function iFailNote(st,r){st.textContent=clientPageError(r&&r.error,"Check the source and try reading it again.");
|
|
53867
|
+
if(r&&r.detail){const d=mk("details","cds-details ifaildetail");d.appendChild(mk("summary","","Details"));d.appendChild(mk("p","small",r.detail));st.appendChild(document.createTextNode(" "));st.appendChild(d)}}
|
|
53868
|
+
/* What the intake was filled from, in the banner's words. */
|
|
53869
|
+
/* One word for the source in every banner line (review nit): your notes for a document, the transcript otherwise. */
|
|
53870
|
+
function iSrcWords(id){return (iPreSrc[id]||"your call")==="the document"?"your notes":"the transcript"}
|
|
53871
|
+
/* After Fill the intake from this (test 01 walkthrough, BUG-2026-09-30-35): the stated answers land, the
|
|
53872
|
+
suggestions wait beside their fields, and the banners are drawn from what is on screen at each redraw. */
|
|
53873
|
+
function iAfterFill(id,r,src){if(r.costs)iData[id].costs=r.costs;iPre[id]=iApplyProposals(id,r.answers);iPreSrc[id]=src;
|
|
53874
|
+
iHeld[id]=r.held||{};iHeldLeft[id]=0;iUnv[id]=r.unverified||[];const unv=iUnv[id];
|
|
53875
|
+
iNote[id]=unv.length?unv.length+" could not be checked against the material because your Claude read it rather than you pasting it here: "+unv.join(", ")+". Check them.":"";
|
|
53876
|
+
iMode[id]="fill";render(lastState,lastRun)}
|
|
53877
|
+
/* A suggestion the operator took counts as filled from the source, and is marked to check like the rest. */
|
|
53878
|
+
function iUseHeld(id,proposals){const applied=iApplyProposals(id,proposals);const pre=iPre[id]||(iPre[id]=[]);for(const l of applied)if(!pre.includes(l))pre.push(l);return applied}
|
|
53879
|
+
function iHasAnswer(v){return Array.isArray(v)?v.length>0:String(v==null?"":v).trim()!==""}
|
|
53880
|
+
/* Use all suggestions (BUG-2026-09-30-34): every suggestion that would not replace an answer already there.
|
|
53881
|
+
The ones that would replace one stay beside their fields for the person to decide. Returns how many stayed. */
|
|
53882
|
+
function iUseAllHeld(id){const held=iHeld[id]||{};const answers=iAnswers[id]||{};const keep={};const take={};
|
|
53883
|
+
/* The claims guard's questions (spots running out, the 72-hour price) are never taken in bulk (review): each waits for its own Use it. */
|
|
53884
|
+
for(const [label,h] of Object.entries(held)){if(h.claims||iConflicts(answers,label,h.value))keep[label]=h;else take[label]=h.value}
|
|
53885
|
+
iUseHeld(id,take);iHeld[id]=keep;return Object.keys(keep).length}
|
|
53886
|
+
/* The fill banners, counted from what is on screen: filled, suggestions waiting for a click, and what the
|
|
53887
|
+
source did not answer at all, by name. */
|
|
53888
|
+
function iFillBanners(id,qs,box){const pre=iPre[id]||[];const held=iHeld[id]||{};const ans=iAnswers[id]||{};const heldN=Object.keys(held).length;
|
|
53889
|
+
if(!pre.length&&!heldN){if(iNote[id]){const nb=mk("div","cds-callout warn pendpanel",iNote[id]);box.appendChild(nb)}return}
|
|
53890
|
+
const missing=qs.filter(q=>!iHasAnswer(ans[q.label])&&!held[q.label]).map(q=>q.label);
|
|
53891
|
+
const src=iSrcWords(id);
|
|
53892
|
+
const banner=mk("div","cds-callout info pendpanel ifillbanner");
|
|
53893
|
+
banner.appendChild(mk("p","",[pre.length+" filled from "+src+". Correct anything wrong.",heldN?heldN+" suggested, waiting for your click.":"",missing.length?missing.length+" not in "+src+": listed below.":"Nothing else is missing."].filter(Boolean).join(" ")));
|
|
53894
|
+
if(missing.length){const ul=mk("ul","cds-list imissing");for(const l of missing)ul.appendChild(mk("li","",l));banner.appendChild(ul)}
|
|
53895
|
+
box.appendChild(banner);
|
|
53896
|
+
if(heldN){const nb=mk("div","cds-callout warn pendpanel cds-stack iheldbanner");
|
|
53897
|
+
nb.appendChild(mk("p","",heldN+" more "+(heldN===1?"answer was":"answers were")+" suggested from "+src+". Check "+(heldN===1?"it":"them")+" and click Use all suggestions, or Use it on each."));
|
|
53898
|
+
const all=mk("button","cds-btn","Use all suggestions");all.type="button";
|
|
53899
|
+
all.onclick=()=>{const left=iUseAllHeld(id);iHeldLeft[id]=left;render(lastState,lastRun)};nb.appendChild(all);
|
|
53900
|
+
if(iHeldLeft[id])nb.appendChild(mk("p","muted",iHeldLeft[id]+" "+(iHeldLeft[id]===1?"suggestion was":"suggestions were")+" not used because "+(iHeldLeft[id]===1?"it":"they")+" would replace an answer already there, or decide"+(iHeldLeft[id]===1?"s":"")+" what the texts may promise. Use it on each to choose."));
|
|
53901
|
+
box.appendChild(nb)}
|
|
53902
|
+
if(iNote[id])box.appendChild(mk("div","cds-callout warn pendpanel",iNote[id]))}
|
|
53903
|
+
let iHeldLeft={};
|
|
53267
53904
|
${ASSESSMENT_PANEL_JS}
|
|
53268
53905
|
let rOpen={},rData={},rBusy={},rSeen={};
|
|
53269
53906
|
let profCache=null,profLoading=false;
|
|
@@ -53379,7 +54016,7 @@ function reviewPanel(id,name,state,run){const r=rData[id];const d=document.creat
|
|
|
53379
54016
|
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
54017
|
const gl=mk("ul","cds-list gaps");
|
|
53381
54018
|
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
|
|
54019
|
+
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
54020
|
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
54021
|
s3.appendChild(gl);
|
|
53385
54022
|
d.appendChild(s3)}
|
|
@@ -53451,6 +54088,13 @@ b.textContent=answer?"Yes":"No";
|
|
|
53451
54088
|
let agencyReady=false;
|
|
53452
54089
|
function ncField(label,control,hint,full){const w=mk("div","cds-field"+(full?" ncfull":""));const l=mk("label","",label);l.htmlFor=control.id;w.appendChild(l);
|
|
53453
54090
|
if(hint){const h=mk("div","hint",hint);h.id=control.id+"-hint";control.setAttribute("aria-describedby",h.id);w.appendChild(h)}w.appendChild(control);return w}
|
|
54091
|
+
/* What was typed on Build a client account, by field id (test 01 walkthrough, BUG-2026-09-30-32). The screen
|
|
54092
|
+
draws again when late data arrives (the snapshot list, the agency check), and a sub-account id typed in the
|
|
54093
|
+
first seconds was wiped. Each field takes its typed value back when drawn; a successful create or register
|
|
54094
|
+
clears it. */
|
|
54095
|
+
let ncDraft={};
|
|
54096
|
+
function ncKeep(el){const k=el.id;if(ncDraft[k]!=null)el.value=ncDraft[k];const save=()=>{ncDraft[k]=el.value};el.addEventListener("input",save);el.addEventListener("change",save);return el}
|
|
54097
|
+
function ncForget(ids){for(const k of ids)delete ncDraft[k]}
|
|
53454
54098
|
function newClientForm(){const f=document.createElement("div");f.className="cds-panel ncform";
|
|
53455
54099
|
f.innerHTML="<h2 class='cds-panel-title'>Create the sub-account</h2><p>GHL Command creates sub-accounts on GoHighLevel's Agency Pro plan; on other plans you create it in GoHighLevel and paste its key into Command OS.</p>";
|
|
53456
54100
|
const g=document.createElement("div");g.className="ncgrid";
|
|
@@ -53460,6 +54104,7 @@ function newClientForm(){const f=document.createElement("div");f.className="cds-
|
|
|
53460
54104
|
const snap=document.createElement("select");snap.id="nc-snapshot";snap.innerHTML='<option value="">No snapshot (blank account)</option>';snap.className="cds-select nccontrol";
|
|
53461
54105
|
if(snapshots)for(const sn of snapshots){const e=document.createElement("option");e.value=sn.id;e.textContent=(sn.type==="own"?"Yours: ":"")+sn.name;snap.appendChild(e)}
|
|
53462
54106
|
const fn=mkIn("nc-first"),ln=mkIn("nc-last"),em=mkIn("nc-email"),city=mkIn("nc-city"),st=mkIn("nc-state"),tz=mkIn("nc-timezone");em.type="email";tz.value=${scriptJson(readAgencyProfile().defaults?.timezone || "")};
|
|
54107
|
+
for(const el of [name,ind,snap,fn,ln,em,city,st,tz])ncKeep(el);
|
|
53463
54108
|
for(const [label,el,hint,full] of [["Business name *",name,"",true],["Industry *",ind,"Tailors this client's intake."],["Snapshot",snap,"",false],["Owner first name",fn],["Owner last name",ln],["Owner email",em,"",true],["City",city],["State",st],["Timezone",tz,"Confirm this client\u2019s timezone for appointment times. For example America/Phoenix.",true]])g.appendChild(ncField(label,el,hint,full));
|
|
53464
54109
|
f.appendChild(g);
|
|
53465
54110
|
if(snapshotError)showClientError(snapshotError,f);
|
|
@@ -53467,7 +54112,7 @@ function newClientForm(){const f=document.createElement("div");f.className="cds-
|
|
|
53467
54112
|
b.onclick=async()=>{if(!ind.value){showClientError("Choose the client industry.",f);return}if(!name.value.trim()){showClientError("Enter the business name, then create the sub-account.",f);return}
|
|
53468
54113
|
b.disabled=true;b.textContent="Creating\u2026";
|
|
53469
54114
|
const r=await(await clientPageRequest("/api/new-client",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:name.value.trim(),industry:ind.value,snapshot_id:snap.value||undefined,first_name:fn.value.trim()||undefined,last_name:ln.value.trim()||undefined,email:em.value.trim()||undefined,city:city.value.trim()||undefined,state:st.value.trim()||undefined,timezone:tz.value.trim()||undefined})})).json();
|
|
53470
|
-
if(r.ok){canCreateSubAccounts=true;planNotice="";registerNotice="";chooseIntakeClient({locationId:r.locationId,name:r.name,pending:!r.registered})}
|
|
54115
|
+
if(r.ok){ncForget(["nc-name","nc-industry","nc-snapshot","nc-first","nc-last","nc-email","nc-city","nc-state","nc-timezone"]);canCreateSubAccounts=true;planNotice="";registerNotice="";chooseIntakeClient({locationId:r.locationId,name:r.name,pending:!r.registered})}
|
|
53471
54116
|
else{if(r.canCreateSubAccounts===false){canCreateSubAccounts=false;planNotice=r.error;render(lastState,lastRun)}
|
|
53472
54117
|
else showClientError(r.error,f,"GHL Command creates sub-accounts on GoHighLevel's Agency Pro plan; on other plans you create it in GoHighLevel and paste its key into Command OS.");b.disabled=false;b.textContent="Create sub-account"}};
|
|
53473
54118
|
b.disabled=!agencyReady;
|
|
@@ -53478,7 +54123,8 @@ function pasteKeyCard(quiet){const card=document.createElement("section");card.c
|
|
|
53478
54123
|
const help=document.createElement("p");help.textContent="Create the sub-account in GoHighLevel, then paste its Private Integration key here.";card.appendChild(help);
|
|
53479
54124
|
const grid=document.createElement("div");grid.className="ncgrid";card.appendChild(grid);
|
|
53480
54125
|
const field=(text,id)=>{const input=document.createElement("input");input.id=id;input.className="cds-input nccontrol";input.setAttribute("aria-label",text);grid.appendChild(ncField(text,input));return input};
|
|
53481
|
-
|
|
54126
|
+
/* The id is kept across a redraw; the key never is, in memory or anywhere else (review LOW 9). */
|
|
54127
|
+
const id=ncKeep(field("Sub-account id","register-client-id")),key=field("Private Integration key","register-client-key");key.type="password";key.autocomplete="off";key.setAttribute("spellcheck","false");key.setAttribute("data-lpignore","true");key.disabled=registerBusy;id.disabled=registerBusy;
|
|
53482
54128
|
// Paths live in ghl-click-paths.ts (key path checked 2026-09-06 through Basic info, and 2026-09-29 with the
|
|
53483
54129
|
// Location ID in Business Profile). Scopes step exact labels: [to confirm live]; do not name them here.
|
|
53484
54130
|
const helpDetails=document.createElement("details");helpDetails.className="cds-details";
|
|
@@ -53493,8 +54139,11 @@ function pasteKeyCard(quiet){const card=document.createElement("section");card.c
|
|
|
53493
54139
|
const r=await(await clientPageRequest("/api/register-client",{method:"POST",headers:{"Content-Type":"application/json"},body:body})).json();
|
|
53494
54140
|
key.value="";key.disabled=false;id.disabled=false;registerBusy=false;registerFailed=!r.ok;
|
|
53495
54141
|
registerNotice=r.ok?r.message:(r.error||"The sub-account could not be registered; check the connection and try again.");
|
|
53496
|
-
if(r.ok){planNotice="";registerNextId=String(r.locationId||"");chooseIntakeClient({locationId:r.locationId,name:r.name})}
|
|
54142
|
+
if(r.ok){ncForget(["register-client-id"]);planNotice="";registerNextId=String(r.locationId||"");chooseIntakeClient({locationId:r.locationId,name:r.name})}
|
|
53497
54143
|
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"}};
|
|
54144
|
+
/* Enter in either field submits, as a form would (rehearsal 5, BUG-2026-09-30-18): the key was
|
|
54145
|
+
pasted, Register never clicked, and nothing said so. The button's own handler does the work. */
|
|
54146
|
+
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
54147
|
return card}
|
|
53499
54148
|
/* The live build panel. Renders run.progress when the cockpit reports it (grouped counts, the
|
|
53500
54149
|
line being written right now); with no progress object it falls back to today's plain log. */
|
|
@@ -53572,6 +54221,32 @@ function staffCard(c,state){const box=document.createElement("section");box.clas
|
|
|
53572
54221
|
for(const line of staffSaid[c.locationId])said.appendChild(mk("p","",line));
|
|
53573
54222
|
const done=mk("button","cds-btn ghost","Done");done.type="button";done.onclick=()=>{delete staffSaid[c.locationId];said.remove()};said.appendChild(done);box.appendChild(said)}
|
|
53574
54223
|
return box}
|
|
54224
|
+
/* Is a submitted intake in for this client? From the intake as last read here, else as the server last read it. */
|
|
54225
|
+
function intakeIsIn(id,state){const d=iData[id];if(d&&d.ok)return d.status==="received";return (((state||{}).intakeInputs||{})[id]||{}).status==="received"}
|
|
54226
|
+
/* The Complete the intake button, its status chip and, when open, the intake itself (one copy, drawn where the card puts it). */
|
|
54227
|
+
let iUnder={};
|
|
54228
|
+
function intakeEntry(c,div,under){const ir=document.createElement("div");ir.className="cds-row";ir.id="intake-"+c.locationId;
|
|
54229
|
+
const ib2=document.createElement("button");ib2.className="cds-btn ghost";ib2.textContent=iOpen[c.locationId]?"Close the intake":"Complete the intake";ib2.setAttribute("aria-expanded",String(Boolean(iOpen[c.locationId])));
|
|
54230
|
+
ib2.onclick=async()=>{iOpen[c.locationId]=!iOpen[c.locationId];if(iOpen[c.locationId]&&under)iUnder[c.locationId]=true;if(!iOpen[c.locationId]){iRetryStop(c.locationId);delete iUnder[c.locationId]}if(iOpen[c.locationId]&&!iData[c.locationId])await iLoad(c.locationId);else render(lastState,lastRun)};
|
|
54231
|
+
ir.appendChild(ib2);
|
|
54232
|
+
const dd=iData[c.locationId];
|
|
54233
|
+
if(dd&&dd.ok){const ic=document.createElement("span");ic.className="cds-chip istatus "+(dd.status==="received"?"ok":dd.status==="sent"?"warn":"");
|
|
54234
|
+
ic.textContent=dd.status==="received"?("ANSWERS IN"+(dd.channel==="form"?" \xB7 FORM":dd.channel==="cockpit"?" \xB7 SAVED HERE":"")):dd.status==="sent"?"SENT":"NOT STARTED";
|
|
54235
|
+
ic.title=dd.channel==="form"?"A real form submission, visible in GHL's Submissions and re-readable later.":dd.channel==="cockpit"?"Saved from Command OS onto a tagged contact (no form submission).":"";ir.appendChild(ic)}
|
|
54236
|
+
div.appendChild(ir);
|
|
54237
|
+
if(iOpen[c.locationId])div.appendChild(unnest(intakeWorkspace(c.locationId,c.name)))}
|
|
54238
|
+
/* Opens the intake where the card draws it and scrolls to it (test 01, BUG-2026-09-30-30/-31). A first opening
|
|
54239
|
+
goes straight to From a call or a document, the fastest way to fill it. */
|
|
54240
|
+
/* The scroll waits for the redraw (BUG-2026-09-30-39): a smooth scroll started before the intake had drawn,
|
|
54241
|
+
and the redraw that followed (its document list arriving) put the page back where it was. For a moment
|
|
54242
|
+
after the click, every redraw ends by bringing the intake into view, instantly. */
|
|
54243
|
+
let iScrollTo=null;const I_SCROLL_MS=2500;
|
|
54244
|
+
function iScrollAfterRender(){const p=iScrollTo;if(!p)return;if(Date.now()>p.until){iScrollTo=null;return}
|
|
54245
|
+
const t=document.getElementById("intake-"+p.id);if(!t)return;try{t.scrollIntoView({block:"start"})}catch(e){}
|
|
54246
|
+
if(!p.focused){p.focused=true;const f=t.querySelector("button");if(f)try{f.focus({preventScroll:true})}catch(e){}}}
|
|
54247
|
+
function openIntake(id){iOpen[id]=true;iUnder[id]=true;if(!iMode[id])iMode[id]="transcript";iScrollTo={id:id,until:Date.now()+I_SCROLL_MS};
|
|
54248
|
+
const go=()=>{render(lastState,lastRun);requestAnimationFrame(iScrollAfterRender)};
|
|
54249
|
+
if(!iData[id])iLoad(id).then(go,go);else go()}
|
|
53575
54250
|
function card(c,state,run){const st=(state.clients[c.locationId]||{stages:STAGES.map(()=>"pending")}).stages;
|
|
53576
54251
|
const sandbox=typeOf(state,c.locationId)==="sandbox";
|
|
53577
54252
|
// A template or hidden account opens only so its owner can lock or unlock it and change its
|
|
@@ -53603,8 +54278,18 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
|
|
|
53603
54278
|
// and the handler it had in the seven-column layout; the next-step panel draws the SAME
|
|
53604
54279
|
// buttons again for the current step, so nothing appears there that its row would not offer.
|
|
53605
54280
|
const staleOf=i=>state.buildViews?.[c.locationId]?.stale?.find(x=>x.stage===i);
|
|
53606
|
-
|
|
54281
|
+
/* A step that is running right now is never counted done (rehearsal 5, BUG-2026-09-30-15): Brief is
|
|
54282
|
+
marked done and then writes the Blueprint Review while still running, and the header read
|
|
54283
|
+
"Step 3 of 7: Approve \xB7 2 done" beside a Brief row that said Running. */
|
|
54284
|
+
const liveI=run&&run.running&&run.locationId===c.locationId&&typeof run.stage==="number"?run.stage:-1;
|
|
54285
|
+
const isDone=i=>(st[i]||"pending")==="done"&&!staleOf(i)&&i!==liveI;
|
|
53607
54286
|
let curI=-1;for(let i=0;i<STAGES.length;i++){if(!isDone(i)){curI=i;break}}
|
|
54287
|
+
/* Test 01 walkthrough (BUG-2026-09-30-31): while Brief is next and no intake is in, the intake is the
|
|
54288
|
+
next job, so its entry point sits right under the steps instead of four screens down. */
|
|
54289
|
+
const intakeNext=!parkedType&&curI===1&&st[0]==="done"&&!intakeIsIn(c.locationId,state);
|
|
54290
|
+
/* Opened under the steps, it stays there while it is open, after a submit too (review LOW 7). */
|
|
54291
|
+
const intakeUp=!parkedType&&st[0]==="done"&&(intakeNext||Boolean(iUnder[c.locationId]&&iOpen[c.locationId]));
|
|
54292
|
+
const intakeFirst=intakeNext;
|
|
53608
54293
|
/* A step still waiting on the owner's approval is not counted as done, so the count matches its "Your call" chip. */
|
|
53609
54294
|
const nDone=STAGES.filter((_,i)=>isDone(i)&&!(i===2&&!(state.buildApproved||{})[c.locationId])).length;
|
|
53610
54295
|
// Where it stands, in words; each row's pin repeats its state and its chip names it.
|
|
@@ -53672,6 +54357,10 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
|
|
|
53672
54357
|
/* What the last run found, in owner words: a warn chip counting the issues with its See details toggle;
|
|
53673
54358
|
Verify's own verdict; the plain summary; a raw system note only under Details. */
|
|
53674
54359
|
const mkNote=()=>{let note=null;
|
|
54360
|
+
/* Brief's own run found no intake (review LOW 8): the gate's sentence, and its button unless the next step already shows one. */
|
|
54361
|
+
if(!live&&oc&&i===1&&oc.reason==="intake-needed"){note=mk("span","snote sintakenote",${scriptJson(BRIEF_NEEDS_INTAKE2)});
|
|
54362
|
+
if(!intakeFirst){const go=mk("button","cds-btn ghost srunintake","Complete the intake");go.type="button";go.onclick=e=>{e.stopPropagation();openIntake(c.locationId)};note.appendChild(document.createTextNode(" "));note.appendChild(go)}
|
|
54363
|
+
return note}
|
|
53675
54364
|
if(!live&&oc&&(oc.summary||(oc.issues||[]).length)){
|
|
53676
54365
|
const passed=i===4&&oc.ok===true,plainSaid=Boolean(oc.plain&&oc.plain.summary);
|
|
53677
54366
|
if(!passed&&nIss){note=mk("span","snote");if(!doneWithIssues&&!staleIssues)note.appendChild(chip("warn",nIss+" "+plural(nIss,"issue")));const see=mk("button","cds-link snote-see","See details");see.type="button";see.onclick=toggleIssues;note.appendChild(see);return note}
|
|
@@ -53717,6 +54406,8 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
|
|
|
53717
54406
|
rnShow(btn,runKeyOf,null);const j=await postRun();
|
|
53718
54407
|
if(j&&j.ok){load();return}
|
|
53719
54408
|
if(i===1&&j&&j.reason==="intake-missing"){rnShow(btn,runKeyOf,{wait:true,waitId:c.locationId,text:BRIEF_WAIT_TEXT});await briefWait(c.locationId,runKeyOf,postRun,j.error);return}
|
|
54409
|
+
/* One Complete the intake button (review nit): the line carries it only when the next step does not. */
|
|
54410
|
+
if(i===1&&j&&j.reason==="intake-needed"){rnShow(btn,runKeyOf,intakeFirst?{text:runFailText(j)}:{text:runFailText(j),openIntake:c.locationId});load();return}
|
|
53720
54411
|
rnShow(btn,runKeyOf,{text:runFailText(j),wasDone:wasDone});load()};
|
|
53721
54412
|
const mkRun=(cls,label)=>{const b=document.createElement("button");b.className=cls;
|
|
53722
54413
|
const busy=run&&run.running;b.textContent=busy&&run.locationId===c.locationId&&run.stage===i?"Running\u2026":label;
|
|
@@ -53732,11 +54423,13 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
|
|
|
53732
54423
|
const h3=document.createElement("h3");h3.className="cds-panel-title";h3.textContent=parkedType?"Nothing is built into this account":(i+1)+". "+LABELS[i];main.appendChild(h3);
|
|
53733
54424
|
const pa=document.createElement("p");pa.textContent=parkedType?"It opens here so you can lock or unlock it, or change its type to Client to build it.":stepAbout(i,c,true);main.appendChild(pa);
|
|
53734
54425
|
if(mustChoose&&!parkedType){const pc=document.createElement("p");pc.className="swarn";pc.textContent="First choose what the client bought. Only those parts get built.";main.appendChild(pc)}
|
|
54426
|
+
if(i===1&&intakeFirst&&!live&&!(oc&&oc.reason==="intake-needed")){main.appendChild(mk("p","swarn sintake",${scriptJson(BRIEF_NEEDS_INTAKE2)}))}
|
|
53735
54427
|
if(live)main.appendChild(mkLive());
|
|
53736
54428
|
if(stale&&!live){const w=document.createElement("div");w.className="cds-row";w.appendChild(chip(chipOf[1],chipOf[0]));w.appendChild(mk("span","swarn",stale.reason));main.appendChild(w)}
|
|
53737
54429
|
const pn=mkNote();if(pn){const w=document.createElement("div");w.className="snoteline";w.appendChild(pn);main.appendChild(w)}
|
|
53738
54430
|
const btns=document.createElement("div");btns.className="sbtns";
|
|
53739
54431
|
if(gateOpen&&!anyBusy&&!parkedType)btns.appendChild(mkGate("cds-btn gatebtn sbig"));
|
|
54432
|
+
if(i===1&&intakeFirst&&!live&&!iOpen[c.locationId]){const ib=mk("button","cds-btn sbig sintakebtn","Complete the intake");ib.type="button";ib.onclick=ev=>{ev.stopPropagation();openIntake(c.locationId)};btns.appendChild(ib)}
|
|
53740
54433
|
if(canBuild&&POWERED.includes(i))btns.appendChild(mkRun("cds-btn runbtn sbig","Run "+LABELS[i]));
|
|
53741
54434
|
if(!parkedType&&canBuild&&POWERED.includes(i)&&run&&run.running&&run.locationId!==c.locationId){const why=document.createElement("p");why.className="muted swhy";why.textContent="A step is running for another client. This one can start when it finishes.";btns.appendChild(why)}
|
|
53742
54435
|
if(!btns.children.length&&!live&&!parkedType){const why=document.createElement("p");why.className="muted swhy";
|
|
@@ -53754,6 +54447,8 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
|
|
|
53754
54447
|
const needsN=(curI<0||curLive?0:1)+approvalWaits;const afterN=Math.max(0,after-approvalWaits);
|
|
53755
54448
|
s.textContent=nDone+" done"+(curI<0&&!approvalWaits?"":(curLive?" \xB7 1 running now":"")+(needsN?" \xB7 "+needsN+" needs you":"")+(curI<0?"":" \xB7 "+afterN+" after that"));plabel.appendChild(s)}
|
|
53756
54449
|
div.appendChild(row);
|
|
54450
|
+
/* Closed, the next step's own Complete the intake button is the one way in (review nit: one button); open, it sits here. */
|
|
54451
|
+
if(intakeUp&&iOpen[c.locationId])intakeEntry(c,div,true);
|
|
53757
54452
|
for(let i=0;i<STAGES.length;i++){if(oOpen[c.locationId+":"+i]){const op=outcomePanel(c.locationId,i,state);if(op)div.appendChild(unnest(op))}}
|
|
53758
54453
|
const view=(state.buildViews||{})[c.locationId];
|
|
53759
54454
|
// The plan is reference material, not the page. Printed open it was 33,759 px of
|
|
@@ -53859,16 +54554,7 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
|
|
|
53859
54554
|
rb.onclick=async()=>{rOpen[c.locationId]=!rOpen[c.locationId];rSeen[c.locationId]=true;if(rOpen[c.locationId]&&!rData[c.locationId])await rLoad(c.locationId);else render(lastState,lastRun)};
|
|
53860
54555
|
rr.appendChild(rb);div.appendChild(rr);
|
|
53861
54556
|
if(rOpen[c.locationId])div.appendChild(unnest(reviewPanel(c.locationId,c.name,state,run)))}
|
|
53862
|
-
if(st[0]==="done")
|
|
53863
|
-
const ib2=document.createElement("button");ib2.className="cds-btn ghost";ib2.textContent=iOpen[c.locationId]?"Close the intake":"Complete the intake";ib2.setAttribute("aria-expanded",String(Boolean(iOpen[c.locationId])));
|
|
53864
|
-
ib2.onclick=async()=>{iOpen[c.locationId]=!iOpen[c.locationId];if(!iOpen[c.locationId])iRetryStop(c.locationId);if(iOpen[c.locationId]&&!iData[c.locationId])await iLoad(c.locationId);else render(lastState,lastRun)};
|
|
53865
|
-
ir.appendChild(ib2);
|
|
53866
|
-
const dd=iData[c.locationId];
|
|
53867
|
-
if(dd&&dd.ok){const ic=document.createElement("span");ic.className="cds-chip istatus "+(dd.status==="received"?"ok":dd.status==="sent"?"warn":"");
|
|
53868
|
-
ic.textContent=dd.status==="received"?("ANSWERS IN"+(dd.channel==="form"?" \xB7 FORM":dd.channel==="cockpit"?" \xB7 SAVED HERE":"")):dd.status==="sent"?"SENT":"NOT STARTED";
|
|
53869
|
-
ic.title=dd.channel==="form"?"A real form submission, visible in GHL's Submissions and re-readable later.":dd.channel==="cockpit"?"Saved from Command OS onto a tagged contact (no form submission).":"";ir.appendChild(ic)}
|
|
53870
|
-
div.appendChild(ir);
|
|
53871
|
-
if(iOpen[c.locationId])div.appendChild(unnest(intakeWorkspace(c.locationId,c.name)))}
|
|
54557
|
+
if(st[0]==="done"&&!intakeUp)intakeEntry(c,div,false);
|
|
53872
54558
|
if(run&&run.locationId===c.locationId&&(run.running||run.lines&&run.lines.length)){
|
|
53873
54559
|
const bp=buildPanel(run);
|
|
53874
54560
|
if(bp){div.appendChild(unnest(bp));
|
|
@@ -53966,6 +54652,8 @@ function icLive(key){return [...document.querySelectorAll(".ic-panel")].filter(x
|
|
|
53966
54652
|
let runNote={},rnQueue=[];
|
|
53967
54653
|
function runFailText(j){return (j&&j.error)||"Command OS could not confirm the result. Refresh this client to read its saved state before retrying."}
|
|
53968
54654
|
function rnEl(key){const n=runNote[key];const p=mk("p",n.wait?"muted srunnote":"cds-callout warn srunnote",n.text);p.dataset.rn=key;p.setAttribute("role",n.wait?"status":"alert");
|
|
54655
|
+
/* Brief refused for want of an intake (test 01, BUG-2026-09-30-30): the line carries the way to the intake panel. */
|
|
54656
|
+
if(n.openIntake){const go=mk("button","cds-btn ghost srunintake","Complete the intake");go.type="button";go.onclick=e=>{e.stopPropagation();openIntake(n.openIntake)};p.appendChild(document.createTextNode(" "));p.appendChild(go)}
|
|
53969
54657
|
if(n.wait&&n.waitId){const stop=mk("button","cds-link srunstop","Stop waiting");stop.type="button";stop.onclick=e=>{e.stopPropagation();briefStop(n.waitId,key)};p.appendChild(document.createTextNode(" "));p.appendChild(stop)}
|
|
53970
54658
|
return p}
|
|
53971
54659
|
function rnLive(key){return [...document.querySelectorAll(".srunnote")].filter(x=>x.dataset.rn===key)}
|
|
@@ -54482,7 +55170,7 @@ function restoreView(v){const m=document.getElementById("main");if(!v||!m)return
|
|
|
54482
55170
|
if(v.focus){const el=[...m.querySelectorAll(v.focus.tag)].filter(x=>fieldKey(x,m)===v.focus.key)[v.focus.n];
|
|
54483
55171
|
if(el&&document.activeElement!==el){try{el.focus({preventScroll:true})}catch(e){}try{if(v.focus.st!=null)el.setSelectionRange(v.focus.st,v.focus.en)}catch(e){}}}
|
|
54484
55172
|
if(window.scrollX!==v.x||window.scrollY!==v.y){try{window.scrollTo(v.x,v.y)}catch(e){}}}
|
|
54485
|
-
function render(state,run){const v=captureView();try{if(state)icPrune(state);rnPrune(state,run);renderPage(state,run);icFlush();rnFlush()}finally{lastDrawnSig=dataSig(state,run);restoreView(v)}}
|
|
55173
|
+
function render(state,run){const v=captureView();try{if(state)icPrune(state);rnPrune(state,run);renderPage(state,run);icFlush();rnFlush()}finally{lastDrawnSig=dataSig(state,run);restoreView(v);iScrollAfterRender()}}
|
|
54486
55174
|
function renderPage(state,run){if(state.roster){ROSTER.splice(0,ROSTER.length,...state.roster)}syncReviews(state);lastState=state;lastRun=run;
|
|
54487
55175
|
if(state&&state.protectedAccounts)PROTECTED=state.protectedAccounts;const m=document.getElementById("main");m.innerHTML="";
|
|
54488
55176
|
/* Four screens share this page's data and differ only in what they show. */
|
|
@@ -55781,9 +56469,10 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
55781
56469
|
return titleCache.get(rk);
|
|
55782
56470
|
};
|
|
55783
56471
|
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))) } });
|
|
56472
|
+
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
56473
|
const outcomes = Object.fromEntries(Object.entries(st.outcomes ?? {}).map(([key, raw]) => {
|
|
55785
56474
|
const rk = key.slice(0, key.lastIndexOf(":"));
|
|
55786
|
-
const value = { ...named(raw, rk), ...raw.plain && { plain: named(raw.plain, rk) } };
|
|
56475
|
+
const value = briefShown(key, { ...named(raw, rk), ...raw.plain && { plain: named(raw.plain, rk) } });
|
|
55787
56476
|
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
56477
|
}));
|
|
55789
56478
|
res.end(JSON.stringify({ ...st, assessmentFigures, boughtViews, outcomes, manualTasks: Object.fromEntries(Object.entries(st.manualTasks ?? {}).map(([id, tasks2]) => {
|
|
@@ -55896,7 +56585,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
55896
56585
|
if (!c) throw new Error(INTAKE_UNREADABLE);
|
|
55897
56586
|
const snap = await readIntakeSnapshot(c, locationId2);
|
|
55898
56587
|
if (snap.status === "unavailable") throw intakeUnavailableError(snap);
|
|
55899
|
-
if (snap.status !== "received") throw new Error(
|
|
56588
|
+
if (snap.status !== "received") throw new Error(BRIEF_NEEDS_INTAKE2);
|
|
55900
56589
|
const required = composeQuestionSet({ industry: st0.accounts?.[locationId2]?.industry, agency: readOverlay() }).filter((q3) => q3.required && !String(normalizedAnswers?.[q3.label] ?? normalizedAnswers?.[q3.key] ?? "").trim());
|
|
55901
56590
|
if (required.length) throw new Error("Complete these required intake answers: " + required.map((q3) => q3.label).join(", ") + ".");
|
|
55902
56591
|
}
|
|
@@ -55995,7 +56684,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
55995
56684
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
55996
56685
|
logQuietly(`client-intake ${new URL(req.url || "/", "http://x").searchParams.get("locationId") || ""}: ${e instanceof Error ? e.message : String(e)}`);
|
|
55997
56686
|
if (e instanceof Error && e.message === INTAKE_READ_TRANSIENT) res.end(JSON.stringify({ ok: false, transient: true, error: INTAKE_REFRESH_RETRYING }));
|
|
55998
|
-
else res.end(JSON.stringify(
|
|
56687
|
+
else res.end(JSON.stringify(intakeRefusal(e)));
|
|
55999
56688
|
}
|
|
56000
56689
|
})();
|
|
56001
56690
|
return;
|
|
@@ -56016,7 +56705,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
56016
56705
|
res.end(JSON.stringify({ ok: true, url: pf.url, leftovers: pf.leftovers }));
|
|
56017
56706
|
} catch (e) {
|
|
56018
56707
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
56019
|
-
|
|
56708
|
+
logQuietly(`${url}: ${e instanceof Error ? e.message : String(e)}`);
|
|
56709
|
+
res.end(JSON.stringify(intakeRefusal(e)));
|
|
56020
56710
|
}
|
|
56021
56711
|
});
|
|
56022
56712
|
return;
|
|
@@ -56053,17 +56743,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
56053
56743
|
if (ready.smoked) recordRunCost({ at: (/* @__PURE__ */ new Date()).toISOString(), locationId: args.locationId, stage: 0, kind: "engine-check", ok: ready.ok, costUsd: ready.smokeCostUsd });
|
|
56054
56744
|
if (!ready.ok) throw new Error(engineRepair(ready.version));
|
|
56055
56745
|
if (typeof args.transcript === "string" && args.transcript.length > 6e4) throw new Error("The limit is 60,000 characters. Paste an excerpt or shorten this text file.");
|
|
56056
|
-
|
|
56057
|
-
if (args.file) {
|
|
56058
|
-
const name = safeDocName(args.file);
|
|
56059
|
-
const dir = clientDocsDir(args.locationId);
|
|
56060
|
-
const p = path40.join(dir, name);
|
|
56061
|
-
if (!fs42.existsSync(p)) throw new Error("that document is not saved for this client");
|
|
56062
|
-
if (fs42.lstatSync(p).isSymbolicLink() || !insideDir(dir, p)) throw new Error("that document is not saved for this client");
|
|
56063
|
-
const text2 = fs42.readFileSync(p, "utf8");
|
|
56064
|
-
if (text2.length > 6e4) throw new Error("The limit is 60,000 characters. Paste an excerpt or shorten this text file.");
|
|
56065
|
-
fileSource = { kind: "text", text: text2 };
|
|
56066
|
-
}
|
|
56746
|
+
const fileSource = args.file ? { kind: "text", text: readSavedClientDoc(args.locationId, args.file) } : null;
|
|
56067
56747
|
const source = fileSource ? fileSource : args.url ? { kind: "url", url: String(args.url) } : args.instruction ? { kind: "recorder", instruction: String(args.instruction) } : { kind: "text", text: String(args.transcript ?? "") };
|
|
56068
56748
|
const toolContext = { servers: [] };
|
|
56069
56749
|
if (source.kind === "recorder") {
|
|
@@ -56147,7 +56827,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
56147
56827
|
return;
|
|
56148
56828
|
} catch (e) {
|
|
56149
56829
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
56150
|
-
|
|
56830
|
+
logQuietly(`${url}: ${e instanceof Error ? e.message : String(e)}`);
|
|
56831
|
+
res.end(JSON.stringify(intakeRefusal(e)));
|
|
56151
56832
|
}
|
|
56152
56833
|
});
|
|
56153
56834
|
return;
|
|
@@ -57060,7 +57741,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
57060
57741
|
process.on("SIGTERM", stop2);
|
|
57061
57742
|
});
|
|
57062
57743
|
}
|
|
57063
|
-
var import_zod67, import_node_util5, fs42, path40, http, import_child_process4, import_child_process5, osmod, STAGES2, ACCOUNT_TYPES, SECOND_CLIENT_NO_INTAKE, CHOOSE_WHAT_THEY_BOUGHT, CHOOSE_BEFORE_BUILD, SCOPE_REFUSAL, NOTHING_BOUGHT, NOTHING_BUILT_FROM_TICKS, ASSESSMENT_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_ASSESSMENT, NOTHING_TICKED_IN_PLAN, SCOPE_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_SCOPE, activeRun2, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, queueScanOnce, PARKED_NOT_BUILT, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_READ_TRANSIENT, INTAKE_REFRESH_RETRYING, INTAKE_ANSWERS_MISSING, INTAKE_READ_FAILED, RUN_IN_PROGRESS, BUSINESS_TYPE_FOR_INDUSTRY, INTAKE_FORM_REQUIRED, INTAKE_CONNECT_REQUIRED, INTAKE_OWN_MESSAGES, currentRosterRef, staffGateRef, BOARD_ROUTES, BOARD_TITLES, SCREEN_HEADS, anthropicKeyWrites, SEAT_TOKEN_HEADER, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS, ConnectionRequest, DisconnectRequest, ReportBrandSaveRequest;
|
|
57744
|
+
var import_zod67, import_node_util5, fs42, path40, http, import_child_process4, import_child_process5, osmod, STAGES2, ACCOUNT_TYPES, SECOND_CLIENT_NO_INTAKE, CHOOSE_WHAT_THEY_BOUGHT, CHOOSE_BEFORE_BUILD, SCOPE_REFUSAL, NOTHING_BOUGHT, NOTHING_BUILT_FROM_TICKS, ASSESSMENT_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_ASSESSMENT, NOTHING_TICKED_IN_PLAN, SCOPE_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_SCOPE, activeRun2, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, queueScanOnce, DOC_NOT_SAVED, PARKED_NOT_BUILT, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_READ_TRANSIENT, INTAKE_REFRESH_RETRYING, INTAKE_ANSWERS_MISSING, INTAKE_READ_FAILED, RUN_IN_PROGRESS, BRIEF_NEEDS_INTAKE2, BUSINESS_TYPE_FOR_INDUSTRY, INTAKE_FORM_REQUIRED, INTAKE_CONNECT_REQUIRED, INTAKE_OWN_MESSAGES, currentRosterRef, staffGateRef, BOARD_ROUTES, BOARD_TITLES, SCREEN_HEADS, anthropicKeyWrites, SEAT_TOKEN_HEADER, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS, ConnectionRequest, DisconnectRequest, ReportBrandSaveRequest;
|
|
57064
57745
|
var init_dashboard = __esm({
|
|
57065
57746
|
"src/dashboard.ts"() {
|
|
57066
57747
|
"use strict";
|
|
@@ -57133,6 +57814,8 @@ var init_dashboard = __esm({
|
|
|
57133
57814
|
init_client_intake();
|
|
57134
57815
|
init_client_intake();
|
|
57135
57816
|
init_plan_store();
|
|
57817
|
+
init_brief_issues();
|
|
57818
|
+
init_build_facts();
|
|
57136
57819
|
init_plain_outcome();
|
|
57137
57820
|
init_staff_first();
|
|
57138
57821
|
init_user_provisioning();
|
|
@@ -57200,6 +57883,7 @@ var init_dashboard = __esm({
|
|
|
57200
57883
|
DOC_EXTS = /* @__PURE__ */ new Set([".md", ".txt", ".vtt", ".srt", ".csv"]);
|
|
57201
57884
|
DOC_MAX_BYTES = 20 * 1024 * 1024;
|
|
57202
57885
|
queueScanOnce = scanGate();
|
|
57886
|
+
DOC_NOT_SAVED = "That document is not saved for this client any more. Choose it again from the list, or add it again.";
|
|
57203
57887
|
PARKED_NOT_BUILT = "Template and hidden accounts are not built from Command OS. Change the account's type to Client first.";
|
|
57204
57888
|
NEEDS_AGENCY_KEY = "No agency key on file. Run register_agency_key in GHL Command first.";
|
|
57205
57889
|
INTAKE_UNREADABLE = "Command OS could not read this client's account. Paste its key again in Build a client account.";
|
|
@@ -57208,6 +57892,7 @@ var init_dashboard = __esm({
|
|
|
57208
57892
|
INTAKE_ANSWERS_MISSING = "The intake form shows a submission, but no contact in this client's account has the answers yet. If the client has not submitted the form, send it again. If they have, press Refresh status in a minute.";
|
|
57209
57893
|
INTAKE_READ_FAILED = "The intake could not be read. Press Refresh status to try again.";
|
|
57210
57894
|
RUN_IN_PROGRESS = "a run is already in progress";
|
|
57895
|
+
BRIEF_NEEDS_INTAKE2 = BRIEF_NEEDS_INTAKE;
|
|
57211
57896
|
BUSINESS_TYPE_FOR_INDUSTRY = { "med-spa": "Med spa", "wellness-clinic": "Wellness clinic", clinic: "Clinic / practice", coach: "Coach / consultant", "local-service": "Local service", ecommerce: "Ecommerce", agency: "Agency" };
|
|
57212
57897
|
INTAKE_FORM_REQUIRED = "Run the intake step in Client builds to create the form, then come back and submit.";
|
|
57213
57898
|
INTAKE_CONNECT_REQUIRED = "Connect this sub-account by pasting its Private Integration key in Command OS.";
|
|
@@ -57223,7 +57908,9 @@ var init_dashboard = __esm({
|
|
|
57223
57908
|
INTAKE_FORM_REQUIRED,
|
|
57224
57909
|
"Confirm the contact creation and email before sending the questionnaire.",
|
|
57225
57910
|
"Choose a registered client before saving the intake.",
|
|
57226
|
-
"Enter intake answers as text or a list of choices."
|
|
57911
|
+
"Enter intake answers as text or a list of choices.",
|
|
57912
|
+
DOC_NOT_SAVED,
|
|
57913
|
+
"Only plain text (.txt, .md, .csv), .vtt and .srt files can be saved here."
|
|
57227
57914
|
];
|
|
57228
57915
|
currentRosterRef = () => [];
|
|
57229
57916
|
staffGateRef = async () => null;
|
|
@@ -62946,10 +63633,10 @@ function renderVerificationHtml(v, branding = {}, base) {
|
|
|
62946
63633
|
];
|
|
62947
63634
|
const s = v.summary;
|
|
62948
63635
|
const verdictClass = v.verdict === "PASS" ? "ok" : v.verdict === "FAIL" ? "fail" : "warn";
|
|
62949
|
-
const row = (r) => `<tr class="${STATE_CLASS[r.state]}"><td class="id">${
|
|
63636
|
+
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
63637
|
return `<!doctype html>
|
|
62951
63638
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
62952
|
-
<title>Design check \u2014 ${
|
|
63639
|
+
<title>Design check \u2014 ${esc15(v.target.split("/").pop() ?? v.target)}</title>
|
|
62953
63640
|
<style>
|
|
62954
63641
|
:root{--accent:${b.accent};--ink:#1F2428;--muted:#5C6570;--line:#E3E6E9;--bg:#FFFFFF;--ok:#2F7A5A;--fail:#A63D3D;--warn:#B9621C;--na:#6E7681}
|
|
62955
63642
|
body{margin:0;background:var(--bg);color:var(--ink);font:18px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}
|
|
@@ -62980,23 +63667,23 @@ td.ev{font-family:ui-monospace,Menlo,monospace;font-size:12.5px;color:var(--mute
|
|
|
62980
63667
|
.shots a{font-size:13px;color:var(--accent)}
|
|
62981
63668
|
footer{margin-top:40px;color:var(--muted);font-size:13px;border-top:1px solid var(--line);padding-top:12px}
|
|
62982
63669
|
</style></head><body><div class="wrap">
|
|
62983
|
-
<header><div>${isRasterDataUri(b.logoDataUri) ? `<img src="${
|
|
63670
|
+
<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
63671
|
<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>${
|
|
63672
|
+
<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
63673
|
<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> ${
|
|
63674
|
+
${v.brandMatch ? `<p class="note"><b>Brand:</b> ${esc15(v.brandMatch.detail)}</p>` : ""}
|
|
62988
63675
|
${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 ${
|
|
63676
|
+
${v.screenshots.length ? `<h2>Screenshots</h2><div class="shots">${v.screenshots.map((f) => `<a href="${esc15(f)}">${esc15(f)}</a>`).join("")}</div>` : ""}
|
|
63677
|
+
<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
63678
|
</div></body></html>`;
|
|
62992
63679
|
}
|
|
62993
|
-
var
|
|
63680
|
+
var esc15, STATE_LABEL, STATE_CLASS;
|
|
62994
63681
|
var init_report2 = __esm({
|
|
62995
63682
|
"src/command-os/design/report.ts"() {
|
|
62996
63683
|
"use strict";
|
|
62997
63684
|
init_branding();
|
|
62998
63685
|
init_svg_sanitize();
|
|
62999
|
-
|
|
63686
|
+
esc15 = (s) => String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
63000
63687
|
STATE_LABEL = {
|
|
63001
63688
|
"MEASURED-PASS": "Pass",
|
|
63002
63689
|
"MEASURED-FAIL": "Fail",
|
|
@@ -67923,10 +68610,10 @@ var init_run3 = __esm({
|
|
|
67923
68610
|
|
|
67924
68611
|
// src/command-os/prospect/report-page.ts
|
|
67925
68612
|
function gradeRing2(score2, caption, size2 = 176) {
|
|
67926
|
-
if (score2 === void 0) return `<div class="cds-ring" role="img" aria-label="Not graded. ${
|
|
68613
|
+
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
68614
|
const s = Math.max(0, Math.min(100, Math.round(score2)));
|
|
67928
68615
|
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. ${
|
|
68616
|
+
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
68617
|
}
|
|
67931
68618
|
function withoutPrice(name) {
|
|
67932
68619
|
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 +68707,11 @@ function bookButton(b, prefill) {
|
|
|
68020
68707
|
const t = walkthroughTarget(b, prefill);
|
|
68021
68708
|
if (!t) return "";
|
|
68022
68709
|
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">${
|
|
68710
|
+
return `${link2(t.href, WALKTHROUGH.button, "cds-btn rp-book")}<span class="rp-print-only">${esc16(printed)}</span>`;
|
|
68024
68711
|
}
|
|
68025
68712
|
function walkthroughBlock(id, b, workflowAudit, prefill) {
|
|
68026
68713
|
const reach = reachLinks(b);
|
|
68027
|
-
return `<section data-section="${id}" id="${id}" class="rp-cta"><h2>${
|
|
68714
|
+
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
68715
|
}
|
|
68029
68716
|
function competitorsAhead(r) {
|
|
68030
68717
|
const mine = r.business.score.score;
|
|
@@ -68053,16 +68740,16 @@ function patientHeadline(gaps, a) {
|
|
|
68053
68740
|
function checksCard(area, checks) {
|
|
68054
68741
|
const cs = checks.filter((c) => c.area === area);
|
|
68055
68742
|
const known = cs.filter((c) => c.verdict !== "unknown").length;
|
|
68056
|
-
return `<div class="rp-card"><div class="rp-card-head"><h3>${
|
|
68743
|
+
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
68744
|
const [tone, word] = VERDICT[c.verdict];
|
|
68058
|
-
return `<li><span class="q">${
|
|
68745
|
+
return `<li><span class="q">${esc16(c.label)}</span>${chip4(tone, word)}<span class="ev">${esc16(c.evidence)}</span></li>`;
|
|
68059
68746
|
}).join("")}</ul></div>`;
|
|
68060
68747
|
}
|
|
68061
68748
|
function brandCard(r) {
|
|
68062
68749
|
const b = r.site.brand;
|
|
68063
68750
|
const look = r.site.phone?.ok ? r.site.phone.look : void 0;
|
|
68064
68751
|
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>${
|
|
68752
|
+
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
68753
|
const norm6 = (x) => x.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
68067
68754
|
const core = b.names.filter((n) => n.where !== "page title").map((n) => n.name);
|
|
68068
68755
|
const base = core[0] ?? "";
|
|
@@ -68073,12 +68760,12 @@ function brandCard(r) {
|
|
|
68073
68760
|
if (!b.shareImage) notes.push("No share image: a link to the site posted on Facebook or in a text shows no picture.");
|
|
68074
68761
|
if (clash) notes.push("The site does not call itself by one name everywhere (listed on the left).");
|
|
68075
68762
|
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>${
|
|
68763
|
+
${b.names.map((n) => `<dt>${esc16(n.where.charAt(0).toUpperCase() + n.where.slice(1))}</dt><dd>${esc16(n.name)}</dd>`).join("")}
|
|
68764
|
+
<dt>Logo</dt><dd>${b.logo ? esc16(b.logo.url ? `${b.logo.where}: ${b.logo.url}` : b.logo.where) : "Not found"}</dd>
|
|
68765
|
+
<dt>Browser-tab icon</dt><dd>${b.favicon ? esc16(b.favicon) : "Not found"}</dd>
|
|
68766
|
+
<dt>Share image</dt><dd>${b.shareImage ? esc16(b.shareImage) : "Not found"}</dd>
|
|
68767
|
+
${b.headlines.length ? `<dt>Headlines</dt><dd>${b.headlines.map((h) => `"${esc16(h)}"`).join("<br>")}</dd>` : ""}
|
|
68768
|
+
</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
68769
|
}
|
|
68083
68770
|
function memberColumn(ids, packages, sold) {
|
|
68084
68771
|
if (!sold && !packages.length) return void 0;
|
|
@@ -68115,17 +68802,17 @@ function compareTable(r, member) {
|
|
|
68115
68802
|
const shown = COMPARE.filter(([id]) => sites.some((s) => s.checks.some((c) => c.id === id && c.verdict !== "unknown")));
|
|
68116
68803
|
const col = member ? memberColumn(shown.map(([id]) => id), member.packages, member.sold) : void 0;
|
|
68117
68804
|
const covered = col ? [...col.values()].filter(Boolean).length : 0;
|
|
68118
|
-
const ours = col && member ? `<th scope="col">${
|
|
68805
|
+
const ours = col && member ? `<th scope="col">${esc16(`With ${member.name}`)}<br><small>${esc16(`${covered} of ${shown.length} included`)}</small></th>` : "";
|
|
68119
68806
|
const oursCell = (id) => {
|
|
68120
68807
|
if (!col) return "";
|
|
68121
68808
|
const n = col.get(id);
|
|
68122
|
-
return n ? `<td class="cds-yes">Yes<br><small>${
|
|
68809
|
+
return n ? `<td class="cds-yes">Yes<br><small>${esc16(n)}</small></td>` : `<td class="cds-unk">Not included</td>`;
|
|
68123
68810
|
};
|
|
68124
|
-
const head4 = sites.map((s, i) => `<th scope="col"${hl(i)}>${
|
|
68811
|
+
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
68812
|
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">${
|
|
68813
|
+
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
68814
|
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." : ""}${
|
|
68815
|
+
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
68816
|
}
|
|
68130
68817
|
function offerParts(r, packages, sold, audience = "client") {
|
|
68131
68818
|
const gaps = gapsOf(r.business.checks);
|
|
@@ -68151,10 +68838,10 @@ function offerParts(r, packages, sold, audience = "client") {
|
|
|
68151
68838
|
const pk = allHave ? packages.filter((p) => fs52.every((f) => p.features.includes(f))).map((p) => withoutPrice(p.name)).filter(Boolean) : [];
|
|
68152
68839
|
const inPk = pk.length ? `Included in ${list3(pk)}.` : allHave && fs52.length === 1 ? FEATURES[fs52[0]].what : "";
|
|
68153
68840
|
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">${
|
|
68841
|
+
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
68842
|
}).join("");
|
|
68156
68843
|
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>${
|
|
68844
|
+
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
68845
|
return {
|
|
68159
68846
|
label: items ? "What closes the gaps" : "The gaps",
|
|
68160
68847
|
// The one gap count (gapCount), the same number as the hero and the tile.
|
|
@@ -68170,7 +68857,7 @@ function speedNote(r) {
|
|
|
68170
68857
|
const ph = r.business.site.phone;
|
|
68171
68858
|
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
68859
|
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(
|
|
68860
|
+
return callout3(esc16(`${why} ${fallback}`), { tone: "warn", title: "Speed check" });
|
|
68174
68861
|
}
|
|
68175
68862
|
function scoringTable() {
|
|
68176
68863
|
const total = AREA_IDS.reduce((a, id) => a + SCORING.areas[id].weight, 0);
|
|
@@ -68178,19 +68865,19 @@ function scoringTable() {
|
|
|
68178
68865
|
const rows2 = AREA_IDS.map((a) => {
|
|
68179
68866
|
const cs = CHECK_IDS.filter((id) => SCORING.checks[id].area === a);
|
|
68180
68867
|
const w = SCORING.areas[a].weight;
|
|
68181
|
-
return `<tr><th scope="row">${
|
|
68868
|
+
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
68869
|
}).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">${
|
|
68870
|
+
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
68871
|
}
|
|
68185
68872
|
function evidenceCard(r) {
|
|
68186
68873
|
const sites = [r.business, ...r.competitors];
|
|
68187
68874
|
return sites.map((s) => {
|
|
68188
|
-
const rows2 = s.site.log.map((l) => `<tr><td>${
|
|
68875
|
+
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
68876
|
const ph = s.site.phone;
|
|
68190
|
-
const phoneRow = ph ? `<p class="rp-meta">${ph.ok ?
|
|
68877
|
+
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
68878
|
const ps = s.site.pagespeed;
|
|
68192
|
-
const psRow = ps ? `<p class="rp-meta">${
|
|
68193
|
-
return `<details class="rp-card"><summary>${
|
|
68879
|
+
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>` : "";
|
|
68880
|
+
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
68881
|
}).join("");
|
|
68195
68882
|
}
|
|
68196
68883
|
function prospectReportHtml(r, opts = {}) {
|
|
@@ -68227,21 +68914,21 @@ function prospectReportHtml(r, opts = {}) {
|
|
|
68227
68914
|
})()
|
|
68228
68915
|
].join("");
|
|
68229
68916
|
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">${
|
|
68917
|
+
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" });
|
|
68918
|
+
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
68919
|
const areasOrder = ["website", "search", "ai", "google", "contact"];
|
|
68233
68920
|
const cards = areasOrder.map((a) => checksCard(a, b.checks));
|
|
68234
68921
|
const gbpLinks = site.home ? [...new Set([site.home, ...site.pages].flatMap((p) => p.googleProfileLinks))] : [];
|
|
68235
68922
|
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>${
|
|
68923
|
+
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" });
|
|
68924
|
+
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
68925
|
const offer = offerParts(r, opts.packages ?? [], opts.services, aud);
|
|
68239
68926
|
const fu = r.followUp;
|
|
68240
68927
|
const sellsFollowUp = !!serviceFor("followup", offeredNames(opts.services));
|
|
68241
68928
|
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">${
|
|
68929
|
+
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
68930
|
const reach = reachLinks(brand);
|
|
68244
|
-
const footRight = [reach.length ? `${
|
|
68931
|
+
const footRight = [reach.length ? `${esc16(brand.name)} \xB7 ${reach.join(" \xB7 ")}` : esc16(brand.name), ...opts.credit ? [esc16(PRODUCT_CREDIT)] : []].join("<br>");
|
|
68245
68932
|
const notChecked = [
|
|
68246
68933
|
"the Google Business Profile itself (needs a Google Maps key)",
|
|
68247
68934
|
"keyword rankings and backlinks (need paid data)",
|
|
@@ -68250,33 +68937,33 @@ function prospectReportHtml(r, opts = {}) {
|
|
|
68250
68937
|
"how fast they answer a new enquiry (the follow-up add-on)",
|
|
68251
68938
|
"phone calls, including after hours"
|
|
68252
68939
|
];
|
|
68253
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${
|
|
68940
|
+
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
68941
|
<style>${DS_TOKENS_CSS}</style>
|
|
68255
68942
|
<style>${DS_BUNDLE_CSS}</style>
|
|
68256
68943
|
<style>${LAYOUT_CSS2}</style></head><body>
|
|
68257
68944
|
<div class="rp">
|
|
68258
68945
|
<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">${
|
|
68946
|
+
<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>
|
|
68947
|
+
<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
68948
|
<section data-section="tiles" class="rp-band"><div class="rp-tiles">${tiles}</div></section>
|
|
68262
68949
|
<section data-section="quote" class="rp-band">${quote}</section>
|
|
68263
68950
|
${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">${
|
|
68951
|
+
${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
68952
|
<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
68953
|
${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
68954
|
${section("offer", offer.label, offer.h2, "", offer.inner)}
|
|
68268
68955
|
${walkthroughBlock("walkthrough", brand, opts.workflowAudit === true, opts.prefill)}
|
|
68269
68956
|
${section("followup", "4 \xB7 Follow-up test", "How fast they answer a new enquiry", "Optional add-on, 1 to 2 days", followHtml)}
|
|
68270
68957
|
${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(
|
|
68958
|
+
<section data-section="coverage" class="rp-band">${callout3(esc16(`Not checked in this report: ${list3(notChecked)}.`), { tone: "info", title: "Not checked" })}</section>
|
|
68272
68959
|
${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
68960
|
${walkthroughBlock("book", brand, opts.workflowAudit === true, opts.prefill)}
|
|
68274
|
-
<footer data-section="footer" class="rp-foot"><span>${
|
|
68961
|
+
<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
68962
|
</div>
|
|
68276
68963
|
<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
68964
|
</body></html>`;
|
|
68278
68965
|
}
|
|
68279
|
-
var
|
|
68966
|
+
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
68967
|
var init_report_page2 = __esm({
|
|
68281
68968
|
"src/command-os/prospect/report-page.ts"() {
|
|
68282
68969
|
"use strict";
|
|
@@ -68286,19 +68973,19 @@ var init_report_page2 = __esm({
|
|
|
68286
68973
|
init_features();
|
|
68287
68974
|
init_services();
|
|
68288
68975
|
init_weights();
|
|
68289
|
-
|
|
68976
|
+
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
68977
|
when2 = (iso2) => new Date(iso2).toLocaleString("en-US", { weekday: "short", month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit", timeZoneName: "short" });
|
|
68291
|
-
|
|
68978
|
+
clock2 = (iso2) => new Date(iso2).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", second: "2-digit" });
|
|
68292
68979
|
HEX2 = /^#[0-9a-f]{6}$/i;
|
|
68293
68980
|
gcls2 = (score2) => `cds-g-${gradeOf(score2).toLowerCase()}`;
|
|
68294
|
-
chip4 = (tone, text2) => `<span class="cds-chip ${tone}">${
|
|
68981
|
+
chip4 = (tone, text2) => `<span class="cds-chip ${tone}">${esc16(text2)}</span>`;
|
|
68295
68982
|
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">${
|
|
68983
|
+
statTile3 = (value, label2, tone = "") => `<div class="cds-stat"><div class="big ${tone}">${esc16(String(value))}</div><div class="lbl">${esc16(label2)}</div></div>`;
|
|
68984
|
+
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>`;
|
|
68985
|
+
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
68986
|
link2 = (href, text2, cls = "") => {
|
|
68300
68987
|
const h = safeHref(href);
|
|
68301
|
-
return h ? `<a${cls ? ` class="${cls}"` : ""} href="${
|
|
68988
|
+
return h ? `<a${cls ? ` class="${cls}"` : ""} href="${esc16(h)}">${esc16(text2)}</a>` : esc16(text2);
|
|
68302
68989
|
};
|
|
68303
68990
|
shortUrl = (u) => u.replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
|
|
68304
68991
|
BOOKING_UTM = "utm_source=secret-shopper";
|
|
@@ -69065,6 +69752,8 @@ function compactExecute(full) {
|
|
|
69065
69752
|
published: full.published,
|
|
69066
69753
|
publishFailed: full.publishFailed,
|
|
69067
69754
|
nextManualSteps: full.nextManualSteps,
|
|
69755
|
+
// The account's custom values after the build (BUG-2026-09-30-11): the only source for "empty".
|
|
69756
|
+
...full.customValues ? { customValues: full.customValues, customValuesNote: full.customValuesNote } : {},
|
|
69068
69757
|
...full.journalWarning ? { journalWarning: full.journalWarning } : {},
|
|
69069
69758
|
...composed ? { composed: { presetId: composed.presetId, presetVersion: composed.presetVersion, note: composed.note, warnings: composed.warnings, scope: composed.scope, scopeSource: composed.scopeSource } } : {},
|
|
69070
69759
|
// A second client in a shared sub-account: what was scoped and who else lives here.
|
|
@@ -69284,7 +69973,7 @@ var init_build_reply = __esm({
|
|
|
69284
69973
|
{ name: "composed.warnings", apply: (r) => shrinkWarnings(r) }
|
|
69285
69974
|
];
|
|
69286
69975
|
CARRIES = {
|
|
69287
|
-
execute: "summary, countsByType, notClean, calendars, missingSteps, workflowsNotBuilt, staffPause, recipients, templates, publishFailed and
|
|
69976
|
+
execute: "summary, countsByType, notClean, calendars, missingSteps, workflowsNotBuilt, staffPause, recipients, templates, publishFailed, nextManualSteps and customValues",
|
|
69288
69977
|
dry_run: "summary, validation, countsByType, calendars, workflows, handoffs and composed.copySlots"
|
|
69289
69978
|
};
|
|
69290
69979
|
}
|
|
@@ -70199,7 +70888,7 @@ async function executeBackbone(plan, deps, opts = {}) {
|
|
|
70199
70888
|
}
|
|
70200
70889
|
if (matches.length === 1) {
|
|
70201
70890
|
idMap[cal.ref] = matches[0].id;
|
|
70202
|
-
const mismatch = calendarSlotMismatch(cal, matches[0]);
|
|
70891
|
+
const mismatch = [calendarSlotMismatch(cal, matches[0]), calendarHoursMismatch(cal, matches[0])].filter(Boolean).join(" ") || void 0;
|
|
70203
70892
|
const boundExpected = resolveExpectedTeam(cal);
|
|
70204
70893
|
let boundSeen = matches[0].teamMemberIds;
|
|
70205
70894
|
let teamReadError2;
|
|
@@ -71289,6 +71978,7 @@ function pickCalendars(raw) {
|
|
|
71289
71978
|
if (typeof o.slotDuration === "number") rec2.slotDuration = o.slotDuration;
|
|
71290
71979
|
else if (typeof o.slotDuration === "string" && /^\d+$/.test(o.slotDuration)) rec2.slotDuration = Number(o.slotDuration);
|
|
71291
71980
|
if (typeof o.slotDurationUnit === "string") rec2.slotDurationUnit = o.slotDurationUnit;
|
|
71981
|
+
if (Array.isArray(o.openHours) && o.openHours.length) rec2.openHours = o.openHours;
|
|
71292
71982
|
if (Array.isArray(o.teamMembers)) {
|
|
71293
71983
|
rec2.teamMemberIds = o.teamMembers.map((tm) => tm && typeof tm === "object" ? tm.userId : void 0).filter((v) => typeof v === "string" && v.length > 0);
|
|
71294
71984
|
}
|
|
@@ -72396,10 +73086,19 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
72396
73086
|
throw raw;
|
|
72397
73087
|
}
|
|
72398
73088
|
progressSink?.finish(exec);
|
|
73089
|
+
let accountCustomValues;
|
|
73090
|
+
try {
|
|
73091
|
+
const raw = await client.get(`/locations/${activeLocation}/customValues`);
|
|
73092
|
+
const hasList = Array.isArray(raw) || !!raw && typeof raw === "object" && Array.isArray(raw.customValues);
|
|
73093
|
+
accountCustomValues = hasList ? pickCustomValues(raw) : void 0;
|
|
73094
|
+
} catch {
|
|
73095
|
+
accountCustomValues = void 0;
|
|
73096
|
+
}
|
|
73097
|
+
const customValueStatus = customValueStatusOf(typedPlan.customValues ?? [], accountCustomValues);
|
|
72399
73098
|
let savedPlanPath;
|
|
72400
73099
|
const publishRecord = savedPublishRecord(saved, exec, typedPlan, publishWorkflows ?? false, gatedWorkflowRefs);
|
|
72401
73100
|
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 });
|
|
73101
|
+
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
73102
|
} catch {
|
|
72404
73103
|
}
|
|
72405
73104
|
let journalPathOut;
|
|
@@ -72430,7 +73129,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
72430
73129
|
...w.manual.map((m) => `[${w.name}] ${m.reason}`),
|
|
72431
73130
|
...w.needsContent.map((c) => `[${w.name}] ${c.reason}`)
|
|
72432
73131
|
]);
|
|
72433
|
-
const handoffLines = result.handoffs.filter((h) => !h.met).map((h) => `[${h.owner}] ${h.title}: ${h.instruction}`);
|
|
73132
|
+
const handoffLines = result.handoffs.filter((h) => !h.met).map((h) => `[${h.owner}] ${h.title}: ${h.instruction}${handoffCustomValueNote(h.ref, typedPlan.customValues ?? [], customValueStatus)}`);
|
|
72434
73133
|
return buildReplyResponse({
|
|
72435
73134
|
ok: exec.ok,
|
|
72436
73135
|
mode: "execute",
|
|
@@ -72498,6 +73197,8 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
72498
73197
|
deferred: exec.deferred,
|
|
72499
73198
|
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
73199
|
nextManualSteps: [...execManualLines, ...manualLines, ...handoffLines],
|
|
73200
|
+
customValues: customValueStatus,
|
|
73201
|
+
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
73202
|
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
73203
|
}, "execute", activeLocation);
|
|
72503
73204
|
}
|
|
@@ -73029,6 +73730,7 @@ var init_intake_to_build = __esm({
|
|
|
73029
73730
|
init_features();
|
|
73030
73731
|
init_executor();
|
|
73031
73732
|
init_execute();
|
|
73733
|
+
init_build_facts();
|
|
73032
73734
|
customFieldItemSchema = import_zod93.z.object({
|
|
73033
73735
|
id: import_zod93.z.string(),
|
|
73034
73736
|
name: import_zod93.z.string(),
|