@elitedcs/ghl-mcp 3.93.1 → 3.94.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +15 -10
- package/dist/index.js +563 -66
- package/guide/guide.html +3 -8
- package/package.json +1 -1
- package/skills/blueprint/examples/validate-plan.cjs +9 -0
- package/skills/blueprint/presets/clinic.preset.json +49 -5
- package/skills/blueprint/presets/coach.preset.json +48 -4
- package/skills/blueprint/presets/ecommerce.preset.json +46 -2
- package/skills/blueprint/presets/generic-client.preset.json +47 -3
- package/skills/blueprint/presets/local-service.preset.json +48 -4
- package/skills/blueprint/presets/med-spa.preset.json +48 -4
- package/skills/blueprint/presets/wellness-clinic.preset.json +122 -12
package/dist/index.js
CHANGED
|
@@ -24340,6 +24340,8 @@ var init_features = __esm({
|
|
|
24340
24340
|
BY_WORKFLOW_REF = {
|
|
24341
24341
|
"workflow.speed_to_lead": "speed_to_lead",
|
|
24342
24342
|
"workflow.missed_call": "missed_call",
|
|
24343
|
+
// F6 (2026-10-29): a missed caller with no card gets one. Goes with the text-back it hangs off.
|
|
24344
|
+
"workflow.missed_call_card": "missed_call",
|
|
24343
24345
|
"workflow.lead_nurture": "nurture",
|
|
24344
24346
|
"workflow.win_back": "winback",
|
|
24345
24347
|
"workflow.appointment_booked": "booking",
|
|
@@ -26084,10 +26086,14 @@ var init_registry = __esm({
|
|
|
26084
26086
|
whatItIs: "Google sign-in gives this machine permission to read your calendar without pasting a password here.",
|
|
26085
26087
|
checkedOn: null,
|
|
26086
26088
|
gives: "Adds your own day beside your clients' appointments as a separate part of the brief.",
|
|
26087
|
-
|
|
26088
|
-
|
|
26089
|
+
// ba's Hub audit (2026-10-29): the guide read as if this connects today, while CHANGELOG 3.92.2 and the card say
|
|
26090
|
+
// CALENDAR_SETUP_MESSAGE. It does not connect until GHL Command ships its own Google sign-in, so the guide says so
|
|
26091
|
+
// first and the steps below are what it will be.
|
|
26092
|
+
needs: ["A Google account with a calendar you can read.", "Google sign-in for GHL Command, which is not available yet."],
|
|
26093
|
+
before: `${CALENDAR_SETUP_MESSAGE} Choosing Connect says so and saves nothing. When Google sign-in arrives in an update, these are the steps:`,
|
|
26089
26094
|
steps: ["On the Google Calendar card on the Connections screen, choose Connect to open Google sign-in.", "Select your Google account and allow read-only calendar access.", "Return to the Google Calendar card and choose Finish. Your sign-in is saved only after your calendars can be read."],
|
|
26090
|
-
say: [
|
|
26095
|
+
say: [],
|
|
26096
|
+
// none until Google sign-in ships: each one would fail (ba audit 2026-10-29)
|
|
26091
26097
|
revoke: "Choose Disconnect on this system's Connections card to remove the saved sign-in and request revocation with Google. You can also remove the app in your Google Account's third-party connections at myaccount.google.com/connections.",
|
|
26092
26098
|
tools: { connect: "connect_google_calendar", disconnect: "connect_google_calendar", read: "read_my_calendar" }
|
|
26093
26099
|
},
|
|
@@ -43533,6 +43539,7 @@ function goLiveTopic(text3) {
|
|
|
43533
43539
|
function jobTiming(text3) {
|
|
43534
43540
|
const t = String(text3).trim();
|
|
43535
43541
|
if (t === HANDOFF_NO_HOME_JOB) return "later";
|
|
43542
|
+
if (HELD_JOB.test(t)) return "later";
|
|
43536
43543
|
if (LEAD_MAGNET_PAGES_JOB.test(t)) return "later";
|
|
43537
43544
|
if (SWITCH_ON_JOB.test(t) || STILL_OFF_NOTE.test(t)) return "at-go-live";
|
|
43538
43545
|
if (LATER_JOB.test(t)) return "later";
|
|
@@ -43649,11 +43656,12 @@ function manualSignOffRefusal(tasks2, verifyHasRun) {
|
|
|
43649
43656
|
}
|
|
43650
43657
|
return "Everything is ticked, but the check has not confirmed it yet. Run the check again and this will clear on its own.";
|
|
43651
43658
|
}
|
|
43652
|
-
var SELF_REPORTED, SWITCH_ON_JOB, LATER_JOB, LEAD_MAGNET_PAGES_JOB, HANDOFF_NO_HOME_JOB, GO_LIVE_TOPICS, NAMED_LATER, STILL_OFF_NOTE, keepsTick;
|
|
43659
|
+
var SELF_REPORTED, HELD_JOB, SWITCH_ON_JOB, LATER_JOB, LEAD_MAGNET_PAGES_JOB, HANDOFF_NO_HOME_JOB, GO_LIVE_TOPICS, NAMED_LATER, STILL_OFF_NOTE, keepsTick;
|
|
43653
43660
|
var init_manual_tasks = __esm({
|
|
43654
43661
|
"src/manual-tasks.ts"() {
|
|
43655
43662
|
"use strict";
|
|
43656
43663
|
SELF_REPORTED = /^[^:]+: Bought, but not built automatically yet\./;
|
|
43664
|
+
HELD_JOB = /^(?:Waiting for your client's answer|Not built, your client said not yet|Not built yet, chosen and coming in a later update): /;
|
|
43657
43665
|
SWITCH_ON_JOB = /^[^\n]+: This workflow is built and waiting for you to switch it on in GoHighLevel under Automation > Workflows\.$/;
|
|
43658
43666
|
LATER_JOB = /^Manual action: (?:[^:\n]*\b(?:after (?:a|the|each) (?:trial )?visit|before any bulk)\b[^:\n]*|How [^:\n]{1,80} starts): /;
|
|
43659
43667
|
LEAD_MAGNET_PAGES_JOB = /^[^:\n]+: Bought, but not built automatically yet\. The form is built\. The pages are separate work\.$/;
|
|
@@ -43668,6 +43676,7 @@ var init_manual_tasks = __esm({
|
|
|
43668
43676
|
{ key: "price", title: "Check the trial price, deposit and balance", re: /trial price|deposit amount|balance amount|trial_price/i }
|
|
43669
43677
|
];
|
|
43670
43678
|
NAMED_LATER = [
|
|
43679
|
+
HELD_JOB,
|
|
43671
43680
|
/^[^:\n]+: Bought, but not built automatically yet\. (?:Set up rebooking and recall|Payment setup is done|Set up referral requests|The form is built|It is not in the plan)/,
|
|
43672
43681
|
/^Manual action: Set up the trial balance link\b/,
|
|
43673
43682
|
/^Manual action: Limit the Trial Balance Paid workflow\b/,
|
|
@@ -45666,7 +45675,13 @@ var init_client_capabilities = __esm({
|
|
|
45666
45675
|
what: "One sentence about a prospect's business becomes a bot they can text, on your own demo account, and one command puts it back. It messages a real prospect, so it runs from your own Claude with you watching, never from a button.",
|
|
45667
45676
|
says: "Prepare an AI employee demo for {client} from their website and arm it so they can text it.",
|
|
45668
45677
|
hub: "modules/11-your-ai-employee/lesson-04-the-five-minute-demo.html",
|
|
45669
|
-
|
|
45678
|
+
// command-center 2026-10-29: read live, the same two lists the demo tools use (the demo bot, and one knowledge
|
|
45679
|
+
// base per prepared prospect, named "Demo: ..."). Whether a demo is armed is the bot's mode, which only
|
|
45680
|
+
// show_ai_employee_demo reads; this page does not claim it.
|
|
45681
|
+
reads: [
|
|
45682
|
+
N("bots", "/conversation-ai/agents/search", { limit: "50" }, "agents", "name", AI2),
|
|
45683
|
+
W(N("knowledge bases", "/knowledge-bases/", { locationId: "{loc}", limit: "50" }, "knowledgeBases", "name", AI2), { one: "knowledge base" })
|
|
45684
|
+
]
|
|
45670
45685
|
},
|
|
45671
45686
|
// ── Money and paperwork ─────────────────────────────────────────────────
|
|
45672
45687
|
{
|
|
@@ -45809,7 +45824,7 @@ var require_package = __commonJS({
|
|
|
45809
45824
|
"package.json"(exports2, module2) {
|
|
45810
45825
|
module2.exports = {
|
|
45811
45826
|
name: "@elitedcs/ghl-mcp",
|
|
45812
|
-
version: "3.
|
|
45827
|
+
version: "3.94.0",
|
|
45813
45828
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
45814
45829
|
description: "GoHighLevel MCP Server for Claude. 250 tools for CRM, automation and marketing, an account-wide workflow audit, live funnel-capture verification and a workflow builder, across client accounts.",
|
|
45815
45830
|
main: "dist/index.js",
|
|
@@ -53038,7 +53053,7 @@ function compute(ans, skip, asm) {
|
|
|
53038
53053
|
function() {
|
|
53039
53054
|
return raw("calls_missed") * share("missed_never_return") * c * v;
|
|
53040
53055
|
},
|
|
53041
|
-
"Call answering
|
|
53056
|
+
"Call answering"
|
|
53042
53057
|
);
|
|
53043
53058
|
add2(
|
|
53044
53059
|
"waited",
|
|
@@ -53721,7 +53736,12 @@ function programBuiltLabel(name2) {
|
|
|
53721
53736
|
}
|
|
53722
53737
|
return `Built: Program follow-up${win ? ` (texts over the ${win} after the visit)` : ""}`;
|
|
53723
53738
|
}
|
|
53724
|
-
function
|
|
53739
|
+
function statusLabel(label2, feature, status) {
|
|
53740
|
+
const what = label2.replace(/^Built: /, "");
|
|
53741
|
+
if (!status?.made) return `Will build: ${what}`;
|
|
53742
|
+
return status.made.has(feature) ? `Built: ${what}` : `Not built yet: ${what}`;
|
|
53743
|
+
}
|
|
53744
|
+
function boughtItemsFor(offered, wording = {}, status) {
|
|
53725
53745
|
return ASSESSMENT_FEATURES.map((f) => {
|
|
53726
53746
|
const id2 = f.id;
|
|
53727
53747
|
const base = { id: id2, name: f.name, means: BOUGHT_MEANS[id2] };
|
|
@@ -53731,11 +53751,12 @@ function boughtItemsFor(offered, wording = {}) {
|
|
|
53731
53751
|
if (!offered.includes(feature)) {
|
|
53732
53752
|
return { ...base, kind: "not-in-plan", feature, ...next, delivery: `Not built for this client: ${FEATURES[feature].name.toLowerCase()} is not in the plan for this industry.` };
|
|
53733
53753
|
}
|
|
53734
|
-
const
|
|
53754
|
+
const label2 = id2 === "quotes" && wording.programFollowUp ? programBuiltLabel(wording.programFollowUp) : BUILT_LABEL[id2];
|
|
53755
|
+
const delivery = status?.held?.[id2] ?? statusLabel(label2, feature, status);
|
|
53735
53756
|
return { ...base, kind: "built", feature, ...next, delivery };
|
|
53736
53757
|
}
|
|
53737
53758
|
const other = OTHER_DELIVERY[id2];
|
|
53738
|
-
return { ...base, kind: other.kind, delivery: other.text };
|
|
53759
|
+
return { ...base, kind: other.kind, delivery: status?.held?.[id2] ?? other.text };
|
|
53739
53760
|
});
|
|
53740
53761
|
}
|
|
53741
53762
|
function featuresFromBought(ticks, offered) {
|
|
@@ -53781,13 +53802,19 @@ function assessmentGateRefusal(state2, rk, clientKey, name2) {
|
|
|
53781
53802
|
function approvalBindingHash(scopeHash2, state2, rk) {
|
|
53782
53803
|
const saved = state2.bought?.[rk];
|
|
53783
53804
|
const answer2 = assessmentAnswerOf(state2, rk);
|
|
53784
|
-
|
|
53805
|
+
const asks = asksBound(state2, rk);
|
|
53806
|
+
if (!Array.isArray(saved) && !answer2 && !asks) return scopeHash2;
|
|
53785
53807
|
const snapshot = state2.assessmentAttachments?.[rk];
|
|
53786
|
-
return contentHash2({ scope: scopeHash2, bought: Array.isArray(saved) ? [...new Set(saved)].sort() : null, assessment: answer2, attached: snapshot ? contentHash2(snapshot) : null });
|
|
53808
|
+
return contentHash2({ scope: scopeHash2, bought: Array.isArray(saved) ? [...new Set(saved)].sort() : null, assessment: answer2, attached: snapshot ? contentHash2(snapshot) : null, ...asks ? { asks } : {} });
|
|
53787
53809
|
}
|
|
53788
53810
|
function boughtBindingHash(scopeHash2, state2, rk) {
|
|
53789
53811
|
const saved = state2.bought?.[rk];
|
|
53790
|
-
|
|
53812
|
+
const asks = asksBound(state2, rk);
|
|
53813
|
+
return contentHash2({ scope: scopeHash2, bought: Array.isArray(saved) ? [...new Set(saved)].sort() : null, ...asks ? { asks } : {} });
|
|
53814
|
+
}
|
|
53815
|
+
function asksBound(state2, rk) {
|
|
53816
|
+
const a = state2.asks?.[rk];
|
|
53817
|
+
return a && Object.keys(a).length ? a : void 0;
|
|
53791
53818
|
}
|
|
53792
53819
|
function intakeChannelAnswers(answers) {
|
|
53793
53820
|
const get2 = (key2, label2) => {
|
|
@@ -53807,6 +53834,12 @@ function boughtJobs(ticks, offered, channels = {}) {
|
|
|
53807
53834
|
reason: item.kind === "not-in-plan" ? "Bought, but not built automatically yet. It is not in the plan for this client's industry." : `Bought, but not built automatically yet. ${OTHER_DELIVERY[item.id].job}`
|
|
53808
53835
|
}));
|
|
53809
53836
|
}
|
|
53837
|
+
function heldJobs(held2) {
|
|
53838
|
+
return held2.map((h) => {
|
|
53839
|
+
const at = h.line.indexOf(": ");
|
|
53840
|
+
return { ref: `bought:held-${h.id}`, type: BOUGHT_JOB_TYPE, name: h.line.slice(0, at), reason: h.line.slice(at + 2) };
|
|
53841
|
+
});
|
|
53842
|
+
}
|
|
53810
53843
|
function boughtButExcludedJobs(ticks, offered, scopeFeatures) {
|
|
53811
53844
|
if (scopeFeatures === "all") return [];
|
|
53812
53845
|
return boughtItemsFor(offered).filter((item) => ticks.includes(item.id) && item.kind === "built" && item.feature && !scopeFeatures.includes(item.feature)).map((item) => ({
|
|
@@ -53829,10 +53862,10 @@ function assessmentChoiceLine(answer2, attached = answer2 === "attached") {
|
|
|
53829
53862
|
if (answer2 === "run-first") return "The Assessment will be run first.";
|
|
53830
53863
|
return "Not answered yet: has the Assessment been run for this client?";
|
|
53831
53864
|
}
|
|
53832
|
-
function boughtViewFor(state2, rk, offered, wording = {}) {
|
|
53865
|
+
function boughtViewFor(state2, rk, offered, wording = {}, status) {
|
|
53833
53866
|
const { ticks, source } = ticksFor(state2, rk);
|
|
53834
53867
|
return {
|
|
53835
|
-
items: boughtItemsFor(offered, wording),
|
|
53868
|
+
items: boughtItemsFor(offered, wording, status),
|
|
53836
53869
|
ticks,
|
|
53837
53870
|
source,
|
|
53838
53871
|
answer: assessmentAnswerOf(state2, rk),
|
|
@@ -53841,17 +53874,20 @@ function boughtViewFor(state2, rk, offered, wording = {}) {
|
|
|
53841
53874
|
suggestions: source === "saved" ? claudeSuggestions(ticks) : []
|
|
53842
53875
|
};
|
|
53843
53876
|
}
|
|
53844
|
-
function boughtDocumentFor(state2, rk, offered, wording = {}) {
|
|
53877
|
+
function boughtDocumentFor(state2, rk, offered, wording = {}, status) {
|
|
53845
53878
|
const { ticks, source } = ticksFor(state2, rk);
|
|
53846
|
-
const items = boughtItemsFor(offered, wording);
|
|
53879
|
+
const items = boughtItemsFor(offered, wording, status);
|
|
53847
53880
|
const confirmed = source === "saved" || source === "scope";
|
|
53848
53881
|
const ticked = items.filter((i) => ticks.includes(i.id));
|
|
53849
53882
|
return {
|
|
53850
53883
|
confirmed,
|
|
53851
53884
|
assessment: assessmentChoiceLine(assessmentAnswerOf(state2, rk), Boolean(state2.assessmentAttachments?.[rk])),
|
|
53852
|
-
|
|
53885
|
+
// "No call goes unanswered" is a promise only an AI answering live keeps, and this version builds none: the client
|
|
53886
|
+
// reads what the build did for calls (promise-vs-build F4 wording, 2026-10-29).
|
|
53887
|
+
bought: ticked.map((i) => ({ name: i.name, means: i.id === "missed" ? status?.made?.has("missed_call") ? "Missed calls get a text back" : "How calls are answered" : i.means, delivery: clientDelivery(i) })),
|
|
53853
53888
|
// The client reads this: never the agency's job text, and never "with Claude" (review of ea3526f).
|
|
53854
53889
|
stillToDo: ticked.flatMap((i) => {
|
|
53890
|
+
if (HELD_LINE.test(i.delivery)) return [`${i.name}: ${clientDelivery(i).toLowerCase()}.`];
|
|
53855
53891
|
if (i.kind === "go-live") return [`${i.name}: being set up in your account.`];
|
|
53856
53892
|
if (i.kind === "separate") return [`${i.name}: the form is ready. The pages are a separate piece of work.`];
|
|
53857
53893
|
if (i.kind === "not-in-plan") return [`${i.name}: being set up separately from this build.`];
|
|
@@ -53861,13 +53897,15 @@ function boughtDocumentFor(state2, rk, offered, wording = {}) {
|
|
|
53861
53897
|
};
|
|
53862
53898
|
}
|
|
53863
53899
|
function clientDelivery(i) {
|
|
53900
|
+
const held2 = HELD_LINE.exec(i.delivery);
|
|
53901
|
+
if (held2) return held2[1] === "Waiting for your client's answer" ? "Waiting on your answer" : held2[1] === "Not built, your client said not yet" ? "Not set up, as you said not yet" : "Chosen, and coming in a later update";
|
|
53864
53902
|
if (i.kind === "go-live") return "Being set up in your account";
|
|
53865
53903
|
if (i.kind === "separate") return "The form is built; the pages are separate work";
|
|
53866
53904
|
if (i.kind === "not-in-plan") return "Not part of this build";
|
|
53867
53905
|
if (i.kind === "claude") return "A weekly report on what is working";
|
|
53868
53906
|
return i.delivery;
|
|
53869
53907
|
}
|
|
53870
|
-
var BOUGHT_IDS, BOUGHT_MEANS, FEATURE_FOR_BOUGHT, BUILT_LABEL, OTHER_DELIVERY, CALL_ANSWERING_NEXT, ASSESSMENT_CHOICES, ANSWER_THE_ASSESSMENT_QUESTION, ATTACH_THE_ASSESSMENT, RUN_THE_ASSESSMENT_FIRST, BOUGHT_JOB_TYPE;
|
|
53908
|
+
var BOUGHT_IDS, BOUGHT_MEANS, FEATURE_FOR_BOUGHT, BUILT_LABEL, OTHER_DELIVERY, CALL_ANSWERING_NEXT, ASSESSMENT_CHOICES, ANSWER_THE_ASSESSMENT_QUESTION, ATTACH_THE_ASSESSMENT, RUN_THE_ASSESSMENT_FIRST, BOUGHT_JOB_TYPE, HELD_LINE;
|
|
53871
53909
|
var init_bought_list = __esm({
|
|
53872
53910
|
"src/bought-list.ts"() {
|
|
53873
53911
|
"use strict";
|
|
@@ -53876,11 +53914,13 @@ var init_bought_list = __esm({
|
|
|
53876
53914
|
init_build_contract();
|
|
53877
53915
|
BOUGHT_IDS = ASSESSMENT_FEATURES.map((f) => f.id);
|
|
53878
53916
|
BOUGHT_MEANS = {
|
|
53879
|
-
|
|
53917
|
+
// rc.1 MED-1: true whatever the client chooses; nothing answers live until a voice agent is built.
|
|
53918
|
+
missed: "Callers get the answer your client chooses",
|
|
53880
53919
|
waited: "Every new lead hears back in seconds",
|
|
53881
53920
|
quotes: "No quote goes cold",
|
|
53882
53921
|
noshow: "Bookings confirmed, reminded and rescued",
|
|
53883
|
-
|
|
53922
|
+
// rc.1 MED-2: what Win-back reaches (leads who go cold from now on); an old list only once a campaign for it is built.
|
|
53923
|
+
reactivation: "Leads who go quiet are brought back",
|
|
53884
53924
|
nurture: "Leads kept warm until they are ready",
|
|
53885
53925
|
recall: "Clients come back on schedule",
|
|
53886
53926
|
deposits: "Fewer no-shows, cash up front",
|
|
@@ -53918,6 +53958,7 @@ var init_bought_list = __esm({
|
|
|
53918
53958
|
ATTACH_THE_ASSESSMENT = "Attach the Assessment in step 4, or choose Build without an Assessment there.";
|
|
53919
53959
|
RUN_THE_ASSESSMENT_FIRST = "You chose to run the Assessment first. Attach it in step 4 when it is done, or choose Build without an Assessment there.";
|
|
53920
53960
|
BOUGHT_JOB_TYPE = "bought-not-built";
|
|
53961
|
+
HELD_LINE = /^(Waiting for your client's answer|Not built, your client said not yet|Not built yet, chosen and coming in a later update): /;
|
|
53921
53962
|
}
|
|
53922
53963
|
});
|
|
53923
53964
|
|
|
@@ -53975,6 +54016,23 @@ var init_offer_normalize = __esm({
|
|
|
53975
54016
|
}
|
|
53976
54017
|
});
|
|
53977
54018
|
|
|
54019
|
+
// src/intake-to-build/texting.ts
|
|
54020
|
+
function textingAnswer(brief) {
|
|
54021
|
+
const channels = brief && typeof brief === "object" ? brief.channels : void 0;
|
|
54022
|
+
const sms = channels && typeof channels === "object" ? channels.sms : void 0;
|
|
54023
|
+
if (sms === true) return "yes";
|
|
54024
|
+
if (sms === false) return "not_yet";
|
|
54025
|
+
return "unanswered";
|
|
54026
|
+
}
|
|
54027
|
+
function smsOn(brief) {
|
|
54028
|
+
return textingAnswer(brief) === "yes";
|
|
54029
|
+
}
|
|
54030
|
+
var init_texting = __esm({
|
|
54031
|
+
"src/intake-to-build/texting.ts"() {
|
|
54032
|
+
"use strict";
|
|
54033
|
+
}
|
|
54034
|
+
});
|
|
54035
|
+
|
|
53978
54036
|
// src/intake-to-build/brief.ts
|
|
53979
54037
|
function norm4(s) {
|
|
53980
54038
|
return s.trim().toLowerCase();
|
|
@@ -54749,7 +54807,7 @@ function deriveFlags(brief) {
|
|
|
54749
54807
|
const channels = brief.channels ?? {};
|
|
54750
54808
|
const goal = brief.goal ?? {};
|
|
54751
54809
|
const a2p = typeof channels.a2pStatus === "string" ? channels.a2pStatus.toLowerCase() : "";
|
|
54752
|
-
if (
|
|
54810
|
+
if (smsOn(brief) && a2p !== "approved" && a2p !== "not needed") {
|
|
54753
54811
|
flags.push("needs_a2p");
|
|
54754
54812
|
}
|
|
54755
54813
|
if (channels.payment === "Stripe not connected" || channels.payment === void 0) flags.push("stripe_not_connected");
|
|
@@ -54757,7 +54815,7 @@ function deriveFlags(brief) {
|
|
|
54757
54815
|
flags.push("calendar_oauth_needed");
|
|
54758
54816
|
}
|
|
54759
54817
|
if (channels.email === false) flags.push("email_not_ready");
|
|
54760
|
-
if (
|
|
54818
|
+
if (smsOn(brief) && channels.hasPhoneNumber !== "yes") {
|
|
54761
54819
|
flags.push("phone_number_needed");
|
|
54762
54820
|
}
|
|
54763
54821
|
return flags;
|
|
@@ -54928,6 +54986,7 @@ var init_normalizer = __esm({
|
|
|
54928
54986
|
"src/intake-to-build/normalizer.ts"() {
|
|
54929
54987
|
"use strict";
|
|
54930
54988
|
init_offer_normalize();
|
|
54989
|
+
init_texting();
|
|
54931
54990
|
init_question_set();
|
|
54932
54991
|
init_brief2();
|
|
54933
54992
|
EMAIL_RE3 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -55508,7 +55567,8 @@ function context(src) {
|
|
|
55508
55567
|
const notify2 = clean3(src.roster?.notifyName) || intakeAnswer2(src.intake, "notify_name") || void 0;
|
|
55509
55568
|
const calls = clean3(src.roster?.callsName) || intakeAnswer2(src.intake, "calls_name") || void 0;
|
|
55510
55569
|
const notifyShown = notify2 ? teamNames(src).find((n) => sameName(n, notify2)) : void 0;
|
|
55511
|
-
|
|
55570
|
+
const callAnswering = src.recordKey && src.boughtState?.asks?.[src.recordKey]?.callAnswering || [];
|
|
55571
|
+
return { features, live: workflowsConfirmed(plan, src.lines), calendars, notify: notify2, ...notifyShown && { notifyShown }, calls, items, ticked: confirmedTicks ? ticks : [], callAnswering, confirmedTicks, programFollowUp: isProgramFollowUp(plan), review: reviewTimingOf(plan), noShowRescue: hasWorkflow(plan, "workflow.no_show"), reply: replyChannels(plan) };
|
|
55512
55572
|
}
|
|
55513
55573
|
function printableCalendars(ctx) {
|
|
55514
55574
|
return [...new Set(ctx.calendars.map((c) => c.name))].filter((n) => n !== "booking calendar");
|
|
@@ -55570,7 +55630,9 @@ function summaryOf2(ctx, plan) {
|
|
|
55570
55630
|
}
|
|
55571
55631
|
const extras = [
|
|
55572
55632
|
f("missed_call") ? "missed calls get a text back" : "",
|
|
55573
|
-
|
|
55633
|
+
// Win-back starts from a tag Lead Nurture or No-Show Rescue adds, so it reaches leads who go cold from now on,
|
|
55634
|
+
// not an old list (promise-vs-build F5, 2026-10-29).
|
|
55635
|
+
f("winback") ? "leads who go cold get a win-back message" : "",
|
|
55574
55636
|
f("quote_followup") ? ctx.programFollowUp ? PROGRAM_WORDS.quotes.summary : "every quote is followed up" : "",
|
|
55575
55637
|
f("review_requests") ? "happy clients are asked for a review" : ""
|
|
55576
55638
|
].filter(Boolean);
|
|
@@ -55687,7 +55749,9 @@ function boughtOf(ctx) {
|
|
|
55687
55749
|
if (built) working.push(boughtLabel(item, true, ctx));
|
|
55688
55750
|
else if (item.kind !== "built") next.push(boughtLabel(item, false, ctx));
|
|
55689
55751
|
else next.push(item.name);
|
|
55690
|
-
if (item.id === "missed" &&
|
|
55752
|
+
if (item.id === "missed" && ctx.ticked.includes("missed")) {
|
|
55753
|
+
for (const c of ctx.callAnswering) if (c !== "missed_text") next.push(c === "live_voice" ? "An AI that answers the phone live (chosen, coming in a later update)" : "An AI that replies by text or chat and books (chosen, coming in a later update)");
|
|
55754
|
+
}
|
|
55691
55755
|
}
|
|
55692
55756
|
return { workingLabel: ctx.live ? "Working now" : "Built, ready to switch on", working, next };
|
|
55693
55757
|
}
|
|
@@ -62762,6 +62826,7 @@ function evalCondition(cond, ctx) {
|
|
|
62762
62826
|
const [, pathExpr, op, rawRhs] = cmp;
|
|
62763
62827
|
let lhs = getPath(ctx.brief, pathExpr);
|
|
62764
62828
|
if (pathExpr === "goal.bookingNeeded") lhs = ctx.bookingWanted;
|
|
62829
|
+
if (pathExpr === "channels.sms") lhs = smsOn(ctx.brief);
|
|
62765
62830
|
const rhs = rawRhs.trim().replace(/^'(.*)'$/, "$1").replace(/^"(.*)"$/, "$1");
|
|
62766
62831
|
let equal;
|
|
62767
62832
|
if (rhs === "true" || rhs === "false") equal = lhs === (rhs === "true");
|
|
@@ -63631,7 +63696,7 @@ function planFromPreset(input) {
|
|
|
63631
63696
|
const skTemplates = sk.templates ?? {};
|
|
63632
63697
|
const emailTemplates = keptByCondition(skTemplates.emails, ctx).map((t) => fillNode(t, ctx));
|
|
63633
63698
|
const gsm7 = typeof p.conventions?.gsm7 === "string";
|
|
63634
|
-
const smsTemplates = keptByCondition(skTemplates.sms, ctx).map((t) => {
|
|
63699
|
+
const smsTemplates = (smsOn(input.brief) ? keptByCondition(skTemplates.sms, ctx) : []).map((t) => {
|
|
63635
63700
|
const filled2 = fillNode(t, ctx);
|
|
63636
63701
|
if (gsm7 && typeof filled2.body === "string") filled2.body = toGsm7(filled2.body);
|
|
63637
63702
|
return filled2;
|
|
@@ -64043,6 +64108,7 @@ var init_plan_from_preset = __esm({
|
|
|
64043
64108
|
init_sms_rules();
|
|
64044
64109
|
init_business_hours();
|
|
64045
64110
|
init_plan_guide();
|
|
64111
|
+
init_texting();
|
|
64046
64112
|
init_claims_guard();
|
|
64047
64113
|
PRESET_TOKEN_ROOTS = /* @__PURE__ */ new Set(["business", "offer", "audience", "voice", "goal", "proof"]);
|
|
64048
64114
|
GSM7_REPLACEMENTS = [
|
|
@@ -66869,7 +66935,7 @@ function compute(ans, skip, asm) {
|
|
|
66869
66935
|
add("missed", "Calls you never get back to",
|
|
66870
66936
|
[{ id: "calls_missed" }, { id: "missed_never_return", share: true }], null,
|
|
66871
66937
|
function () { return raw("calls_missed") * share("missed_never_return") * c * v; },
|
|
66872
|
-
"Call answering
|
|
66938
|
+
"Call answering");
|
|
66873
66939
|
|
|
66874
66940
|
add("waited", "Enquiries that waited",
|
|
66875
66941
|
[{ id: "waited_month" }], "response_penalty",
|
|
@@ -69652,6 +69718,14 @@ function reviewPrompt(input) {
|
|
|
69652
69718
|
"For each gap, state the cost using their own numbers when they gave any, and what we build to close it:",
|
|
69653
69719
|
...GAP_CHECKLIST.map((g) => ` \u2022 ${g}`),
|
|
69654
69720
|
"",
|
|
69721
|
+
...input.promises ? [
|
|
69722
|
+
"WHAT THIS BUILD MAKES. You do not list what we build or how each gap is fixed: the document takes that from this list itself. You write the diagnosis only. Your recommended offer and biggest lever may name a system or automation ONLY if it is on this list:",
|
|
69723
|
+
...input.promises.willBuild.length ? input.promises.willBuild.map((w) => ` \u2022 ${w}`) : [" \u2022 (nothing yet)"],
|
|
69724
|
+
...input.promises.held.length ? ["WAITING ON THE CLIENT'S ANSWER (never say we build these):", ...input.promises.held.map((h) => ` \u2022 ${h}`)] : [],
|
|
69725
|
+
'Never promise that calls are answered live, "day and night", "24/7" or "every call": nothing in this build answers the phone.',
|
|
69726
|
+
...input.promises.textingOn ? [] : ["This client is not texting: never promise a text, an SMS or a text-back anywhere."],
|
|
69727
|
+
""
|
|
69728
|
+
] : [],
|
|
69655
69729
|
"THE CLIENT'S INTAKE ANSWERS:",
|
|
69656
69730
|
answers || "(no answers provided)",
|
|
69657
69731
|
"",
|
|
@@ -69663,7 +69737,7 @@ function reviewPrompt(input) {
|
|
|
69663
69737
|
] : [],
|
|
69664
69738
|
'A gap about deposits or prepayment is never priority "now": deposits are set up later and never hold up sign-off.',
|
|
69665
69739
|
"Reply with ONE line of JSON and nothing else:",
|
|
69666
|
-
'RESULT: {"headline":"<one sentence a busy owner would nod at>","ica":{"who":"","painPoints":[""],"objections":[""],"buyingTrigger":"","whereTheyAre":""},"offer":{"score":0,"dreamOutcome":"","believability":"","speed":"","effort":"","biggestLever":"","recommendedOffer":""},"gaps":[{"title":"","cost":"","
|
|
69740
|
+
'RESULT: {"headline":"<one sentence a busy owner would nod at>","ica":{"who":"","painPoints":[""],"objections":[""],"buyingTrigger":"","whereTheyAre":""},"offer":{"score":0,"dreamOutcome":"","believability":"","speed":"","effort":"","biggestLever":"","recommendedOffer":""},"gaps":[{"title":"","cost":"","priority":"now"}],"unknowns":[""]}'
|
|
69667
69741
|
].join("\n");
|
|
69668
69742
|
}
|
|
69669
69743
|
function accountPrefixes(clientName) {
|
|
@@ -69719,6 +69793,97 @@ function strictRetryPrompt(input) {
|
|
|
69719
69793
|
"No preamble, no explanation, no markdown fences, no closing remarks."
|
|
69720
69794
|
].join("\n");
|
|
69721
69795
|
}
|
|
69796
|
+
function renderedLines(r) {
|
|
69797
|
+
const out = [];
|
|
69798
|
+
const add2 = (where, text3) => {
|
|
69799
|
+
if (typeof text3 === "string" && text3.trim()) out.push({ where, text: text3 });
|
|
69800
|
+
};
|
|
69801
|
+
add2("headline", r.headline);
|
|
69802
|
+
if (r.ica) {
|
|
69803
|
+
add2("ica.who", r.ica.who);
|
|
69804
|
+
add2("ica.buyingTrigger", r.ica.buyingTrigger);
|
|
69805
|
+
add2("ica.whereTheyAre", r.ica.whereTheyAre);
|
|
69806
|
+
(r.ica.painPoints ?? []).forEach((t, i) => add2(`ica.painPoints.${i}`, t));
|
|
69807
|
+
(r.ica.objections ?? []).forEach((t, i) => add2(`ica.objections.${i}`, t));
|
|
69808
|
+
}
|
|
69809
|
+
if (r.offer) for (const k of ["dreamOutcome", "believability", "speed", "effort", "biggestLever", "recommendedOffer"]) add2(`offer.${k}`, r.offer[k]);
|
|
69810
|
+
(r.gaps ?? []).forEach((g, i) => {
|
|
69811
|
+
add2(`gaps.${i}.title`, g.title);
|
|
69812
|
+
add2(`gaps.${i}.cost`, g.cost);
|
|
69813
|
+
});
|
|
69814
|
+
(r.unknowns ?? []).forEach((t, i) => add2(`unknowns.${i}`, t));
|
|
69815
|
+
return out;
|
|
69816
|
+
}
|
|
69817
|
+
function overPromises(r, p) {
|
|
69818
|
+
if (!p || !r.ok) return [];
|
|
69819
|
+
const made = p.willBuild.join(" | ");
|
|
69820
|
+
return renderedLines(r).filter((l) => LIVE_CALLS.test(l.text) || (!p.textingOn || p.held.some((h) => /texting/i.test(h))) && TEXTS.test(l.text) || TOPICS.some((t) => t.re.test(l.text) && !t.built.test(made))).map((l) => l.where);
|
|
69821
|
+
}
|
|
69822
|
+
function graderPrompt(r, p) {
|
|
69823
|
+
return [
|
|
69824
|
+
"You check a client document for promises. A line promises something if it says or implies that we will build, set up, run or deliver it for the client.",
|
|
69825
|
+
"WHAT THIS BUILD MAKES (the only things a line may promise):",
|
|
69826
|
+
...p.willBuild.length ? p.willBuild.map((w) => `- ${w}`) : ["- (nothing)"],
|
|
69827
|
+
...p.held.length ? ["NOT BUILT, WAITING ON THE CLIENT'S ANSWER (a line promising these is a violation):", ...p.held.map((h) => `- ${h}`)] : [],
|
|
69828
|
+
`Texting is ${p.textingOn ? "on" : "OFF: any promise of a text, SMS or text-back is a violation"}. Nothing in this build answers the phone live: any promise of answered calls is a violation.`,
|
|
69829
|
+
"Diagnosis, observations about the client and questions are not promises. Advice clearly marked as for later is not a promise.",
|
|
69830
|
+
"THE LINES (id: text):",
|
|
69831
|
+
...renderedLines(r).map((l) => `${l.where}: ${l.text}`),
|
|
69832
|
+
'Reply with ONE line of JSON and nothing else: RESULT: {"violations":["<id>", ...]}'
|
|
69833
|
+
].join("\n");
|
|
69834
|
+
}
|
|
69835
|
+
function parseGrader(text3, r) {
|
|
69836
|
+
const json3 = extractResultJson(text3);
|
|
69837
|
+
if (json3 === void 0) return null;
|
|
69838
|
+
try {
|
|
69839
|
+
const v = JSON.parse(json3).violations;
|
|
69840
|
+
if (!Array.isArray(v)) return null;
|
|
69841
|
+
const known = new Set(renderedLines(r).map((l) => l.where));
|
|
69842
|
+
return v.map(String).filter((x) => known.has(x));
|
|
69843
|
+
} catch {
|
|
69844
|
+
return null;
|
|
69845
|
+
}
|
|
69846
|
+
}
|
|
69847
|
+
function trimLines(r, bad) {
|
|
69848
|
+
if (!bad.length) return r;
|
|
69849
|
+
const set = new Set(bad);
|
|
69850
|
+
const trimmed = renderedLines(r).filter((l) => set.has(l.where)).map((l) => l.text);
|
|
69851
|
+
const keep = (where) => !set.has(where);
|
|
69852
|
+
const ica = r.ica && {
|
|
69853
|
+
...r.ica,
|
|
69854
|
+
who: keep("ica.who") ? r.ica.who : "",
|
|
69855
|
+
buyingTrigger: keep("ica.buyingTrigger") ? r.ica.buyingTrigger : "",
|
|
69856
|
+
whereTheyAre: keep("ica.whereTheyAre") ? r.ica.whereTheyAre : "",
|
|
69857
|
+
painPoints: (r.ica.painPoints ?? []).filter((_, i) => keep(`ica.painPoints.${i}`)),
|
|
69858
|
+
objections: (r.ica.objections ?? []).filter((_, i) => keep(`ica.objections.${i}`))
|
|
69859
|
+
};
|
|
69860
|
+
const offer = r.offer && Object.fromEntries(Object.entries(r.offer).map(([k, v]) => [k, typeof v === "string" && !keep(`offer.${k}`) ? "" : v]));
|
|
69861
|
+
return {
|
|
69862
|
+
...r,
|
|
69863
|
+
headline: keep("headline") ? r.headline : void 0,
|
|
69864
|
+
...ica ? { ica } : {},
|
|
69865
|
+
...offer ? { offer } : {},
|
|
69866
|
+
gaps: (r.gaps ?? []).filter((_, i) => keep(`gaps.${i}.title`) && keep(`gaps.${i}.cost`)),
|
|
69867
|
+
unknowns: (r.unknowns ?? []).filter((_, i) => keep(`unknowns.${i}`)),
|
|
69868
|
+
trimmed: [...r.trimmed ?? [], ...trimmed]
|
|
69869
|
+
};
|
|
69870
|
+
}
|
|
69871
|
+
async function violations(spawnClaude, r, p) {
|
|
69872
|
+
if (!p || !r.ok) return { ids: [], graded: true };
|
|
69873
|
+
const fast = overPromises(r, p);
|
|
69874
|
+
let found = null;
|
|
69875
|
+
for (let attempt2 = 0; attempt2 < 2 && found === null; attempt2++) {
|
|
69876
|
+
try {
|
|
69877
|
+
found = parseGrader(await spawnClaude(graderPrompt(r, p)), r);
|
|
69878
|
+
} catch {
|
|
69879
|
+
found = null;
|
|
69880
|
+
}
|
|
69881
|
+
}
|
|
69882
|
+
return { ids: [.../* @__PURE__ */ new Set([...fast, ...found ?? []])], graded: found !== null };
|
|
69883
|
+
}
|
|
69884
|
+
function structural(r, p) {
|
|
69885
|
+
return { ...r, build: [], gaps: (r.gaps ?? []).map((g) => ({ ...g, fix: "" })), ...p ? { promises: p } : {} };
|
|
69886
|
+
}
|
|
69722
69887
|
async function generateReview(spawnClaude, input) {
|
|
69723
69888
|
const answered = Object.values(input.answers).filter((v) => v && String(v).trim()).length;
|
|
69724
69889
|
if (answered < 5) {
|
|
@@ -69731,7 +69896,21 @@ async function generateReview(spawnClaude, input) {
|
|
|
69731
69896
|
lastRaw = raw;
|
|
69732
69897
|
const names = [input.clientName, ...Object.entries(input.answers).filter(([k]) => /business[ _]?name/i.test(k)).map(([, v]) => v)].join(" ");
|
|
69733
69898
|
const parsed2 = parseReview(raw, names);
|
|
69734
|
-
if (parsed2.ok)
|
|
69899
|
+
if (parsed2.ok) {
|
|
69900
|
+
const first2 = structural(parsed2, input.promises);
|
|
69901
|
+
const over2 = await violations(spawnClaude, first2, input.promises);
|
|
69902
|
+
if (!over2.ids.length) return { ...first2, graded: over2.graded };
|
|
69903
|
+
try {
|
|
69904
|
+
const again = parseReview(await spawnClaude([reviewPrompt(input), "", `IMPORTANT: your previous answer promised what this build does not make (${over2.ids.join(", ")}). Promise only what is listed under WHAT THIS BUILD MAKES.`].join("\n")), names);
|
|
69905
|
+
if (again.ok) {
|
|
69906
|
+
const second = structural(again, input.promises);
|
|
69907
|
+
const v = await violations(spawnClaude, second, input.promises);
|
|
69908
|
+
return { ...trimLines(second, v.ids), graded: v.graded };
|
|
69909
|
+
}
|
|
69910
|
+
} catch {
|
|
69911
|
+
}
|
|
69912
|
+
return { ...trimLines(first2, over2.ids), graded: over2.graded };
|
|
69913
|
+
}
|
|
69735
69914
|
} catch (e) {
|
|
69736
69915
|
lastRaw = e instanceof Error ? e.message : String(e);
|
|
69737
69916
|
}
|
|
@@ -69742,12 +69921,15 @@ async function generateReview(spawnClaude, input) {
|
|
|
69742
69921
|
raw: lastRaw.slice(-600)
|
|
69743
69922
|
};
|
|
69744
69923
|
}
|
|
69924
|
+
function clientHeldLine(line4) {
|
|
69925
|
+
return line4.replace(/^Waiting for your client's answer: /, "Waiting on your answer: ").replace(/^Not built, your client said not yet: /, "Not set up, as you said not yet: ").replace(/^Not built yet, chosen and coming in a later update: /, "Chosen, and coming in a later update: ");
|
|
69926
|
+
}
|
|
69745
69927
|
function reviewDocumentHtml(review, opts) {
|
|
69746
69928
|
const accent = opts.accent && /^#[0-9a-fA-F]{6}$/.test(opts.accent) ? opts.accent : "#B8934A";
|
|
69747
69929
|
const agency = opts.agencyName?.trim() || "";
|
|
69748
69930
|
const esc18 = (v) => String(v ?? "").replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" })[c]);
|
|
69749
69931
|
const date = (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, { year: "numeric", month: "long", day: "numeric" });
|
|
69750
|
-
const gap = (g) => `<div class="gap ${g.priority}"><div class="gt">${esc18(g.title)}<span class="pri">${g.priority === "now" ? "Fix first" : g.priority === "soon" ? "Next" : "Later"}</span></div>${g.cost ? `<div class="gc">${esc18(g.cost)}</div>` : ""}
|
|
69932
|
+
const gap = (g) => `<div class="gap ${g.priority}"><div class="gt">${esc18(g.title)}<span class="pri">${g.priority === "now" ? "Fix first" : g.priority === "soon" ? "Next" : "Later"}</span></div>${g.cost ? `<div class="gc">${esc18(g.cost)}</div>` : ""}</div>`;
|
|
69751
69933
|
const o = review.offer;
|
|
69752
69934
|
const confirmed = opts.scope && !opts.scope.wholePreset && !opts.scope.awaitingConfirmation ? opts.scope : void 0;
|
|
69753
69935
|
const purchased = confirmed || opts.purchasedFeatureIds ? [.../* @__PURE__ */ new Set([
|
|
@@ -69758,7 +69940,7 @@ function reviewDocumentHtml(review, opts) {
|
|
|
69758
69940
|
const represented = [...opts.scope?.built ?? [], ...unbuilt].map((f) => BUILD_ASSESSMENT_FEATURE[f.id]).concat(opts.scope && !opts.scope.awaitingConfirmation ? ["pipeline"] : []);
|
|
69759
69941
|
const assessmentNext = opts.assessment && purchased ? assessmentFeatureLines(opts.assessment, [...purchased, ...represented]) : [];
|
|
69760
69942
|
const available = unbuilt.map((f) => `<div class="build"><b>${esc18(f.name)}</b><span>${esc18(f.what)}${f.figure ? ` <b>${esc18(f.figure)}</b>` : ""}</span></div>`).join("") + assessmentNext.map((line4) => `<p>${esc18(line4)}</p>`).join("");
|
|
69761
|
-
return `<!doctype html><html><head><meta charset="utf-8"><title>${esc18(opts.clientName)}
|
|
69943
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${esc18(opts.clientName)}: Growth Blueprint</title><style>
|
|
69762
69944
|
@page{margin:0.6in}
|
|
69763
69945
|
*{box-sizing:border-box}
|
|
69764
69946
|
body{font-family:-apple-system,Segoe UI,Helvetica,sans-serif;color:#1f2430;margin:0;line-height:1.55}
|
|
@@ -69813,7 +69995,7 @@ function reviewDocumentHtml(review, opts) {
|
|
|
69813
69995
|
${o.biggestLever ? `<p class="kv"><b>Biggest lever:</b> ${esc18(o.biggestLever)}</p>` : ""}
|
|
69814
69996
|
${o.recommendedOffer ? `<div class="lead"><b>Run this instead:</b> ${esc18(o.recommendedOffer)}</div>` : ""}` : ""}
|
|
69815
69997
|
${review.gaps?.length ? `<div class="head">What's leaking today (${review.gaps.length})</div>${review.gaps.map(gap).join("")}` : ""}
|
|
69816
|
-
${review.
|
|
69998
|
+
${review.promises && (review.promises.willBuild.length || review.promises.held.length) ? `<div class="head">What we're building</div>${review.promises.willBuild.map((w) => `<div class="build"><b>${esc18(w)}</b></div>`).join("")}${review.promises.held.map((h) => `<div class="build"><b>${esc18(clientHeldLine(h))}</b></div>`).join("")}` : ""}
|
|
69817
69999
|
${opts.scope?.awaitingConfirmation ? `<div class="head">What this build includes</div>
|
|
69818
70000
|
<p>Not confirmed yet. The build waits until the features to build are confirmed.</p>
|
|
69819
70001
|
` : opts.scope ? `<div class="head">What this build includes</div>
|
|
@@ -69829,7 +70011,7 @@ function reviewDocumentHtml(review, opts) {
|
|
|
69829
70011
|
<footer><span>${agency ? esc18(agency) : "Growth Blueprint"}</span><span>${esc18(opts.clientName)} \xB7 ${date}</span></footer>
|
|
69830
70012
|
</div></body></html>`;
|
|
69831
70013
|
}
|
|
69832
|
-
var GAP_CHECKLIST;
|
|
70014
|
+
var GAP_CHECKLIST, LIVE_CALLS, TEXTS, TOPICS;
|
|
69833
70015
|
var init_blueprint_review = __esm({
|
|
69834
70016
|
"src/blueprint-review.ts"() {
|
|
69835
70017
|
"use strict";
|
|
@@ -69849,6 +70031,12 @@ var init_blueprint_review = __esm({
|
|
|
69849
70031
|
"SMS capability (A2P registered) if they want to text",
|
|
69850
70032
|
"Reporting they will actually look at"
|
|
69851
70033
|
];
|
|
70034
|
+
LIVE_CALLS = /(?:\bday\s*(?:and|&)\s*night\b|\b24\s*[\/\-\u2013]\s*7\b|\b24 hours\b|\b(?:a)?round[- ]the[- ]clock\b|\bafter[- ]hours?\b|\bevery (?:single )?(?:call|caller|enquiry|inquiry|ring)s?\b|\banswer(?:s|ed|ing)?\b[^.]{0,40}\b(?:phones?|calls?|rings?)\b|\b(?:phones?|calls?)\b[^.]{0,20}\banswer(?:s|ed|ing)?\b|\bcalls? (?:are |get )?answered\b|\bpick(?:s|ing)? up\b|\bsomeone rings\b|\bphones? (?:are |is )?covered\b|\b(?:call|phone) coverage\b|\bcoverage on (?:your |the )?phones?\b|\bno missed calls\b|\bnever miss(?:es)?\b[^.]{0,40}\b(?:calls?|leads?|phone)\b|\bno (?:call|caller) (?:goes|is) (?:unanswered|missed)\b|\blive (?:call |phone )?answering\b|\banswering 24\b|\b(?:ai|virtual) (?:receptionist|voice|phone)\b|\breceptionist\b)/i;
|
|
70035
|
+
TEXTS = /\b(?:texts?|texted|texting|sms|text-?backs?|whatsapp|by message on (?:their|your) (?:mobile|phone)|on their mobile|mobile messages?)\b/i;
|
|
70036
|
+
TOPICS = [
|
|
70037
|
+
{ re: /\b(?:missed[- ]calls?|text[- ]?backs?|callers?)\b/i, built: /missed-call text-back/i },
|
|
70038
|
+
{ re: /\b(?:reactivat\w*|win[- ]?backs?|past (?:clients|customers|patients)|old (?:leads|lists?|clients|customers|patients)|dormant|lapsed|bring (?:them|back)\b|brought back)\b/i, built: /win-back/i }
|
|
70039
|
+
];
|
|
69852
70040
|
}
|
|
69853
70041
|
});
|
|
69854
70042
|
|
|
@@ -71782,6 +71970,116 @@ var init_clinic_prefix_registry = __esm({
|
|
|
71782
71970
|
}
|
|
71783
71971
|
});
|
|
71784
71972
|
|
|
71973
|
+
// src/client-asks.ts
|
|
71974
|
+
function parseAsks(raw) {
|
|
71975
|
+
if (raw === void 0 || raw === null) return { ok: true, asks: {} };
|
|
71976
|
+
if (typeof raw !== "object" || Array.isArray(raw)) return { ok: false, error: "The answers could not be read. Choose them again, then save." };
|
|
71977
|
+
const r = raw;
|
|
71978
|
+
const out = {};
|
|
71979
|
+
if (r.texting !== void 0) {
|
|
71980
|
+
if (r.texting !== "yes" && r.texting !== "not_yet") return { ok: false, error: "Texting is Yes or Not yet." };
|
|
71981
|
+
out.texting = r.texting;
|
|
71982
|
+
}
|
|
71983
|
+
if (r.callAnswering !== void 0) {
|
|
71984
|
+
if (!Array.isArray(r.callAnswering) || !r.callAnswering.every((c) => CALL_ANSWERING_CHOICES.includes(c))) {
|
|
71985
|
+
return { ok: false, error: "Call answering choices could not be read. Choose them again, then save." };
|
|
71986
|
+
}
|
|
71987
|
+
const chosen = [...new Set(r.callAnswering)];
|
|
71988
|
+
if (chosen.length) out.callAnswering = CALL_ANSWERING_CHOICES.filter((c) => chosen.includes(c));
|
|
71989
|
+
}
|
|
71990
|
+
if (r.reactivation !== void 0) {
|
|
71991
|
+
const x = r.reactivation;
|
|
71992
|
+
if (!x || typeof x !== "object") return { ok: false, error: "Reactivation answers could not be read. Choose them again, then save." };
|
|
71993
|
+
if (!REACTIVATION_LISTS.includes(x.list)) return { ok: false, error: "Choose who reactivation should reach." };
|
|
71994
|
+
const offer = x.offer;
|
|
71995
|
+
const text3 = typeof offer?.text === "string" ? offer.text.trim() : "";
|
|
71996
|
+
if (!offer || offer.from !== "brief" && offer.from !== "typed" || !text3) return { ok: false, error: "Choose the offer that brings them back, or type one." };
|
|
71997
|
+
if (text3.length > MAX_OFFER) return { ok: false, error: `Keep the offer under ${MAX_OFFER} characters.` };
|
|
71998
|
+
if (x.channel !== "email" && x.channel !== "email_and_text") return { ok: false, error: "Choose Email, or Email and text." };
|
|
71999
|
+
const a = { list: x.list, offer: { from: offer.from, text: text3 }, channel: x.channel };
|
|
72000
|
+
if (a.list === "in_account_not_booked") {
|
|
72001
|
+
const d = Number(x.notBookedDays);
|
|
72002
|
+
if (!Number.isInteger(d) || d < 1 || d > 3650) return { ok: false, error: "Not booked in how many days? Use a whole number from 1 to 3650." };
|
|
72003
|
+
a.notBookedDays = d;
|
|
72004
|
+
}
|
|
72005
|
+
if (x.textConsentConfirmed === true) a.textConsentConfirmed = true;
|
|
72006
|
+
out.reactivation = a;
|
|
72007
|
+
}
|
|
72008
|
+
return { ok: true, asks: out };
|
|
72009
|
+
}
|
|
72010
|
+
function effectiveTexting(asks, intakeSms) {
|
|
72011
|
+
if (asks?.texting === "yes") return "yes";
|
|
72012
|
+
if (asks?.texting === "not_yet") return "not_yet";
|
|
72013
|
+
if (intakeSms === true) return "yes";
|
|
72014
|
+
if (intakeSms === false) return "not_yet";
|
|
72015
|
+
return "unanswered";
|
|
72016
|
+
}
|
|
72017
|
+
function asksContradiction(asks, texting) {
|
|
72018
|
+
if (texting !== "yes" && asks.callAnswering?.includes("missed_text")) return "A text back after a missed call needs Texting: Yes.";
|
|
72019
|
+
const re = asks.reactivation;
|
|
72020
|
+
if (re?.channel === "email_and_text") {
|
|
72021
|
+
if (texting !== "yes") return "Email and text needs Texting: Yes.";
|
|
72022
|
+
if (!re.textConsentConfirmed) return "Email and text needs your client to confirm these people agreed to get texts.";
|
|
72023
|
+
}
|
|
72024
|
+
return null;
|
|
72025
|
+
}
|
|
72026
|
+
function heldItems(ticks, asks, texting) {
|
|
72027
|
+
const out = [];
|
|
72028
|
+
const wait = (id2, name2) => ({ id: id2, name: name2, reason: "unanswered", line: `Waiting for your client's answer: ${name2}` });
|
|
72029
|
+
const textsMatter = ticks.includes("sms") || ticks.includes("missed") || ticks.includes("reactivation");
|
|
72030
|
+
if (textsMatter && texting === "unanswered") out.push(wait("texting", "Texting"));
|
|
72031
|
+
if (ticks.includes("sms") && texting === "not_yet") out.push({ id: "texting", name: "Texting", reason: "not_yet", line: "Not built, your client said not yet: Texting" });
|
|
72032
|
+
if (ticks.includes("missed")) {
|
|
72033
|
+
const c = asks?.callAnswering ?? [];
|
|
72034
|
+
if (!c.length) out.push(wait("call_answering", "Call answering"));
|
|
72035
|
+
else {
|
|
72036
|
+
const later = c.filter((x) => x === "live_voice" || x === "chat_bot");
|
|
72037
|
+
if (later.length) out.push({ id: "call_answering", name: "Call answering", reason: "later_update", line: `Not built yet, chosen and coming in a later update: ${later.map((x) => x === "live_voice" ? "an AI that answers the phone live" : "an AI that replies by text or chat and books").join(" and ")}` });
|
|
72038
|
+
if (c.includes("missed_text") && texting !== "yes") out.push(wait("texting", "Texting (the text back after a missed call needs it)"));
|
|
72039
|
+
}
|
|
72040
|
+
}
|
|
72041
|
+
if (ticks.includes("reactivation")) {
|
|
72042
|
+
const re = asks?.reactivation;
|
|
72043
|
+
if (!re) out.push(wait("reactivation", "Reactivation (who it reaches, which offer)"));
|
|
72044
|
+
else if (re.list !== "cold_from_now") out.push({ id: "reactivation", name: "Reactivation", reason: "later_update", line: `Not built yet, chosen and coming in a later update: reactivation for ${re.list === "import" ? "a list your client will give you" : `contacts here with no booking in ${re.notBookedDays} days`}` });
|
|
72045
|
+
}
|
|
72046
|
+
return out.filter((h, i) => out.findIndex((o) => o.id === h.id && o.reason === h.reason) === i);
|
|
72047
|
+
}
|
|
72048
|
+
function featuresAllowedByAsks(features, ticks, asks) {
|
|
72049
|
+
return features.filter((f) => {
|
|
72050
|
+
if (f === "missed_call" && ticks.includes("missed")) return asks?.callAnswering?.includes("missed_text") === true;
|
|
72051
|
+
if (f === "winback" && ticks.includes("reactivation")) return asks?.reactivation?.list === "cold_from_now";
|
|
72052
|
+
return true;
|
|
72053
|
+
});
|
|
72054
|
+
}
|
|
72055
|
+
function briefWithAsks(brief, asks, ticks, presetWorkflows) {
|
|
72056
|
+
const ch = brief.channels && typeof brief.channels === "object" ? brief.channels : {};
|
|
72057
|
+
const texting = effectiveTexting(asks, typeof ch.sms === "boolean" ? ch.sms : void 0);
|
|
72058
|
+
const { sms: _was, ...rest } = ch;
|
|
72059
|
+
let out = { ...brief, channels: { ...rest, ...texting === "unanswered" ? {} : { sms: texting === "yes" } } };
|
|
72060
|
+
if (!ticks) return out;
|
|
72061
|
+
const scope = out.scope;
|
|
72062
|
+
const offered = [...new Set(presetWorkflows.map((w) => featureOfWorkflow(String(w.ref ?? ""), w.feature)).filter((f) => f !== null))];
|
|
72063
|
+
const current = scope ? Array.isArray(scope.features) ? scope.features.map(String) : [] : offered;
|
|
72064
|
+
const allowed = featuresAllowedByAsks(current, ticks, asks);
|
|
72065
|
+
if (allowed.length !== current.length) out = { ...out, scope: { ...scope?.package ? { package: scope.package } : {}, features: allowed } };
|
|
72066
|
+
return out;
|
|
72067
|
+
}
|
|
72068
|
+
function asksBindingHash(asks, ticks) {
|
|
72069
|
+
return contentHash2({ asks: asks && Object.keys(asks).length ? asks : null, ticks: ticks ? [...new Set(ticks)].sort() : null });
|
|
72070
|
+
}
|
|
72071
|
+
var CALL_ANSWERING_CHOICES, REACTIVATION_LISTS, MAX_OFFER;
|
|
72072
|
+
var init_client_asks = __esm({
|
|
72073
|
+
"src/client-asks.ts"() {
|
|
72074
|
+
"use strict";
|
|
72075
|
+
init_features();
|
|
72076
|
+
init_build_contract();
|
|
72077
|
+
CALL_ANSWERING_CHOICES = ["live_voice", "chat_bot", "missed_text"];
|
|
72078
|
+
REACTIVATION_LISTS = ["import", "in_account_not_booked", "cold_from_now"];
|
|
72079
|
+
MAX_OFFER = 300;
|
|
72080
|
+
}
|
|
72081
|
+
});
|
|
72082
|
+
|
|
71785
72083
|
// src/dashboard.ts
|
|
71786
72084
|
var dashboard_exports = {};
|
|
71787
72085
|
__export(dashboard_exports, {
|
|
@@ -71796,6 +72094,7 @@ __export(dashboard_exports, {
|
|
|
71796
72094
|
ASSESSMENT_CHANGED_SINCE_APPROVAL: () => ASSESSMENT_CHANGED_SINCE_APPROVAL,
|
|
71797
72095
|
ASSESSMENT_PIPELINE_MISSING: () => ASSESSMENT_PIPELINE_MISSING,
|
|
71798
72096
|
BRIEF_NEEDS_INTAKE: () => BRIEF_NEEDS_INTAKE2,
|
|
72097
|
+
BUILD_WHAT_WAS_WAITING: () => BUILD_WHAT_WAS_WAITING,
|
|
71799
72098
|
CHOOSE_BEFORE_BUILD: () => CHOOSE_BEFORE_BUILD,
|
|
71800
72099
|
CHOOSE_CLIENT: () => CHOOSE_CLIENT,
|
|
71801
72100
|
CHOOSE_WHAT_THEY_BOUGHT: () => CHOOSE_WHAT_THEY_BOUGHT,
|
|
@@ -71852,6 +72151,7 @@ __export(dashboard_exports, {
|
|
|
71852
72151
|
brandPagesForPro: () => brandPagesForPro,
|
|
71853
72152
|
briefReach: () => briefReach,
|
|
71854
72153
|
buildPreviewsFor: () => buildPreviewsFor,
|
|
72154
|
+
buildStatusFor: () => buildStatusFor,
|
|
71855
72155
|
callClaudeRecorded: () => callClaudeRecorded,
|
|
71856
72156
|
canonicalIndustry: () => canonicalIndustry,
|
|
71857
72157
|
capabilityJobs: () => capabilityJobs,
|
|
@@ -71893,6 +72193,7 @@ __export(dashboard_exports, {
|
|
|
71893
72193
|
guarded: () => guarded,
|
|
71894
72194
|
hasChosenScope: () => hasChosenScope,
|
|
71895
72195
|
insideDir: () => insideDir,
|
|
72196
|
+
intakeAskFacts: () => intakeAskFacts,
|
|
71896
72197
|
intakeErrorDetail: () => intakeErrorDetail,
|
|
71897
72198
|
intakeInput: () => intakeInput,
|
|
71898
72199
|
intakeMissingMessage: () => intakeMissingMessage,
|
|
@@ -71948,15 +72249,20 @@ __export(dashboard_exports, {
|
|
|
71948
72249
|
rereadQueueIntakes: () => rereadQueueIntakes,
|
|
71949
72250
|
resetCockpitStateRecovery: () => resetCockpitStateRecovery,
|
|
71950
72251
|
resolvePlan: () => resolvePlan2,
|
|
72252
|
+
reviewIsStale: () => reviewIsStale,
|
|
72253
|
+
reviewPromisesFor: () => reviewPromisesFor,
|
|
72254
|
+
reviewPromisesHash: () => reviewPromisesHash,
|
|
71951
72255
|
routeGate: () => routeGate,
|
|
71952
72256
|
runDashboard: () => runDashboard,
|
|
71953
72257
|
runForTests: () => runForTests,
|
|
71954
72258
|
runRefusal: () => runRefusal,
|
|
71955
72259
|
runStopReason: () => runStopReason,
|
|
71956
72260
|
safeDocName: () => safeDocName,
|
|
72261
|
+
savedPlanFitsAnswers: () => savedPlanFitsAnswers,
|
|
71957
72262
|
scanGate: () => scanGate,
|
|
71958
72263
|
scopeHashOf: () => scopeHashOf,
|
|
71959
72264
|
scopePrefillFor: () => scopePrefillFor,
|
|
72265
|
+
scopeWithAsks: () => scopeWithAsks,
|
|
71960
72266
|
secondClientInputHash: () => secondClientInputHash,
|
|
71961
72267
|
setAccountIndustry: () => setAccountIndustry,
|
|
71962
72268
|
setAccountType: () => setAccountType,
|
|
@@ -72147,7 +72453,7 @@ function withdrawIfStaffChanged(state2, locationId2) {
|
|
|
72147
72453
|
return { state: { ...setBuildApproval(state2, locationId2, null), planApprovals: approvals }, withdrawn: true, message: ap.staffHash === void 0 ? APPROVAL_WITHDRAWN_NO_TEAM : APPROVAL_WITHDRAWN_FOR_STAFF };
|
|
72148
72454
|
}
|
|
72149
72455
|
function withBoughtJobs(tasks2, lines) {
|
|
72150
|
-
const kept = tasks2.filter((t) => !SELF_REPORTED.test(t.text));
|
|
72456
|
+
const kept = tasks2.filter((t) => !SELF_REPORTED.test(t.text) && !HELD_JOB.test(t.text));
|
|
72151
72457
|
const prior = new Map(tasks2.map((t) => [t.id, t]));
|
|
72152
72458
|
return [...kept, ...lines.map((line4) => {
|
|
72153
72459
|
if (typeof line4 === "string") return prior.get(taskId(line4)) ?? { id: taskId(line4), text: line4 };
|
|
@@ -72247,6 +72553,71 @@ function presetWorkflowsFor(state2, recordKey) {
|
|
|
72247
72553
|
}
|
|
72248
72554
|
return null;
|
|
72249
72555
|
}
|
|
72556
|
+
function scopeWithAsks(state2, rk) {
|
|
72557
|
+
const current = state2.scope?.[rk];
|
|
72558
|
+
const ticks = state2.bought?.[rk];
|
|
72559
|
+
if (!current || !Array.isArray(ticks)) return current;
|
|
72560
|
+
const base = current.wholePreset ? offeredFeaturesFor(state2, rk) : current.features;
|
|
72561
|
+
const allowed = featuresAllowedByAsks(base, ticks, state2.asks?.[rk]);
|
|
72562
|
+
return allowed.length === base.length ? current : { features: allowed, setAt: current.setAt };
|
|
72563
|
+
}
|
|
72564
|
+
function intakeAskFacts(answers) {
|
|
72565
|
+
const get2 = (key2, label2) => {
|
|
72566
|
+
const v = answers?.[label2] ?? answers?.[key2];
|
|
72567
|
+
return typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
72568
|
+
};
|
|
72569
|
+
const { sms } = intakeChannelAnswers(answers);
|
|
72570
|
+
const phone = get2("has_phone_number", "Do you already have a phone number in GoHighLevel?")?.toLowerCase();
|
|
72571
|
+
const offer = get2("core_offer", "Core Offer");
|
|
72572
|
+
return { ...sms !== void 0 ? { sms } : {}, ...phone === "yes" || phone === "no" || phone === "unsure" ? { phone } : {}, ...offer ? { offer: offer.slice(0, 300) } : {} };
|
|
72573
|
+
}
|
|
72574
|
+
function buildStatusFor(st, rk, locationId2) {
|
|
72575
|
+
const ticks = st.bought?.[rk] ?? [];
|
|
72576
|
+
const intake = intakeChannelAnswers(st.intakeCompleted?.[locationId2]?.answers ?? st.intakeInputs?.[locationId2]?.answers);
|
|
72577
|
+
const held2 = {};
|
|
72578
|
+
const item = { texting: "sms", call_answering: "missed", reactivation: "reactivation" };
|
|
72579
|
+
for (const h of heldItems(ticks, st.asks?.[rk], effectiveTexting(st.asks?.[rk], intake.sms))) held2[item[h.id]] ??= h.line;
|
|
72580
|
+
const sp = readSavedPlan(rk);
|
|
72581
|
+
const ran = st.outcomes?.[`${rk}:3`]?.ok === true || (sp?.built?.length ?? 0) > 0;
|
|
72582
|
+
if (!ran) return { held: held2 };
|
|
72583
|
+
const featureOf = (ref) => {
|
|
72584
|
+
try {
|
|
72585
|
+
return featureOfWorkflow(ref);
|
|
72586
|
+
} catch {
|
|
72587
|
+
return null;
|
|
72588
|
+
}
|
|
72589
|
+
};
|
|
72590
|
+
const made = /* @__PURE__ */ new Set();
|
|
72591
|
+
for (const o of sp?.planId ? madeByBuild(locationId2, sp.planId) : []) if (o.type === "workflow") {
|
|
72592
|
+
const f = featureOf(o.ref);
|
|
72593
|
+
if (f) made.add(f);
|
|
72594
|
+
}
|
|
72595
|
+
for (const b of sp?.built ?? []) if (b.type === "workflow" && b.realId) {
|
|
72596
|
+
const f = featureOf(String(b.ref));
|
|
72597
|
+
if (f) made.add(f);
|
|
72598
|
+
}
|
|
72599
|
+
return { made, held: held2 };
|
|
72600
|
+
}
|
|
72601
|
+
function reviewPromisesFor(st, rk, locationId2) {
|
|
72602
|
+
const ticks = new Set(st.bought?.[rk] ?? []);
|
|
72603
|
+
const items = boughtItemsFor(offeredFeaturesFor(st, rk), boughtWordingFor(st, rk), buildStatusFor(st, rk, locationId2)).filter((i) => ticks.has(i.id));
|
|
72604
|
+
const willBuild = items.map((i) => /^(?:Will build|Built|Always built): (.+)$/.exec(i.delivery)?.[1]).filter((x) => Boolean(x));
|
|
72605
|
+
const held2 = items.map((i) => i.delivery).filter((d) => HELD_JOB.test(d));
|
|
72606
|
+
const intake = intakeChannelAnswers(st.intakeCompleted?.[locationId2]?.answers ?? st.intakeInputs?.[locationId2]?.answers);
|
|
72607
|
+
return { willBuild, held: held2, textingOn: effectiveTexting(st.asks?.[rk], intake.sms) === "yes" };
|
|
72608
|
+
}
|
|
72609
|
+
function reviewPromisesHash(st, rk, locationId2 = rk) {
|
|
72610
|
+
return contentHash2(reviewPromisesFor(st, rk, locationId2));
|
|
72611
|
+
}
|
|
72612
|
+
function reviewIsStale(st, rk, review) {
|
|
72613
|
+
return Boolean(review?.ok) && review.promisesHash !== reviewPromisesHash(st, rk);
|
|
72614
|
+
}
|
|
72615
|
+
function savedPlanFitsAnswers(sp, state2, rk) {
|
|
72616
|
+
if (!sp || !savedPlanMatchesScope(sp, scopeWithAsks(state2, rk))) return false;
|
|
72617
|
+
const asks = state2.asks?.[rk];
|
|
72618
|
+
if (!sp.asksHash) return !(asks && Object.keys(asks).length);
|
|
72619
|
+
return sp.asksHash === asksBindingHash(asks, state2.bought?.[rk]);
|
|
72620
|
+
}
|
|
72250
72621
|
function offeredFeaturesFor(state2, recordKey) {
|
|
72251
72622
|
const workflows = presetWorkflowsFor(state2, recordKey);
|
|
72252
72623
|
return workflows ? featuresInPreset(workflows) : FEATURE_IDS;
|
|
@@ -72456,7 +72827,7 @@ function startRun(locationId2, stage, onDone, meta = {}) {
|
|
|
72456
72827
|
// or before Your team decided) is made again, never built with someone the member was not shown.
|
|
72457
72828
|
savedPlan: (() => {
|
|
72458
72829
|
const sp = stage === 3 ? readSavedPlan(rk) : null;
|
|
72459
|
-
return sp && sp.inputHash === meta.inputHash &&
|
|
72830
|
+
return sp && sp.inputHash === meta.inputHash && savedPlanFitsAnswers(sp, state2, rk) && savedPlanTeamIsCurrent(sp, meta.normalizedAnswers, composed) ? { planId: sp.planId, savedAt: sp.savedAt, summary: planSummary(sp) } : void 0;
|
|
72460
72831
|
})(),
|
|
72461
72832
|
textSnippets: Boolean(state2.textSnippets?.[rk])
|
|
72462
72833
|
};
|
|
@@ -74203,6 +74574,60 @@ async function profLoad(){if(profLoading)return;profLoading=true;try{profCache=(
|
|
|
74203
74574
|
waits for the Assessment question above the list. */
|
|
74204
74575
|
/* The ticks and package chosen but not yet saved, kept outside the block so a redraw keeps them (review of c09e946). */
|
|
74205
74576
|
let scopeDraft={},boughtNotice={};
|
|
74577
|
+
/* Ask first (Jerry 2026-10-29): texting, call answering and reactivation are asked of the client before Build. The
|
|
74578
|
+
answers save with "Save what they bought" (server: parseAsks, asksContradiction). Nothing is pre-selected: the
|
|
74579
|
+
client's own form answer shows with a Change link; a phone number is only a hint. Blank = held, never built. */
|
|
74580
|
+
let asksDraft={};
|
|
74581
|
+
function asksClean(a){const o={};if(a.texting)o.texting=a.texting;if(a.callAnswering&&a.callAnswering.length)o.callAnswering=[...a.callAnswering];
|
|
74582
|
+
/* W44: typed text counts whenever no other offer was chosen, whatever was clicked first. */
|
|
74583
|
+
const r=a.reactivation;if(r&&r.list){const off=r.offer&&r.offer.text?r.offer:(r._typed?{from:"typed",text:r._typed}:{});const x={list:r.list,offer:off,channel:r.channel};if(r.list==="in_account_not_booked")x.notBookedDays=Number(r.notBookedDays);if(r.textConsentConfirmed)x.textConsentConfirmed=true;o.reactivation=x}return o}
|
|
74584
|
+
function asksBox(id,v,getTicked,keep){
|
|
74585
|
+
const box=mk("div","cds-stack ask-first");box.id="asks-"+id;const ia=v.intakeAsks||{};const savedJson=JSON.stringify(v.asks||{});
|
|
74586
|
+
if(!asksDraft[id]||asksDraft[id].base!==savedJson)asksDraft[id]={base:savedJson,a:JSON.parse(savedJson),change:false};
|
|
74587
|
+
const d=asksDraft[id],a=d.a;
|
|
74588
|
+
const radio=(name,val,lab,on,dis,why,act)=>{const l=mk("label","cds-check");const r=document.createElement("input");r.type="radio";r.name=name;r.checked=on;r.disabled=!!dis;r.onchange=act;l.appendChild(r);const s=mk("span","");s.appendChild(mk("b","",lab));if(why)s.appendChild(mk("small","muted",why));l.appendChild(s);return l};
|
|
74589
|
+
function draw(){box.innerHTML="";const t=getTicked();
|
|
74590
|
+
if(!(t.has("sms")||t.has("missed")||t.has("reactivation"))){box.hidden=true;return}box.hidden=false;
|
|
74591
|
+
box.appendChild(mk("h4","cds-panel-label","Confirm with your client before Build"));
|
|
74592
|
+
box.appendChild(mk("p","","Ask your client these, then choose. Nothing is built for a question left blank; it waits for your client's answer."));
|
|
74593
|
+
box.appendChild(mk("p","muted","The answers save with Save what they bought. Changing one after the plan is approved means approving it again."));
|
|
74594
|
+
const texting=a.texting||(ia.sms===true?"yes":ia.sms===false?"not_yet":"");
|
|
74595
|
+
const tx=mk("div","cds-stack");tx.appendChild(mk("b","","Texting"));tx.appendChild(mk("p","","Can this business text leads?"));
|
|
74596
|
+
if(!a.texting&&ia.sms!==undefined&&!d.change){const p=mk("p","","Your client's form said: "+(ia.sms?"Yes":"Not yet")+" ");const ch=mk("button","cds-link","Change");ch.type="button";ch.onclick=()=>{d.change=true;draw()};p.appendChild(ch);tx.appendChild(p)}
|
|
74597
|
+
else{const row=mk("div","cds-row");row.appendChild(radio("tx-"+id,"yes","Yes",a.texting==="yes",false,"",()=>{a.texting="yes";keep();draw()}));row.appendChild(radio("tx-"+id,"not_yet","Not yet",a.texting==="not_yet",false,"",()=>{a.texting="not_yet";keep();draw()}));tx.appendChild(row)}
|
|
74598
|
+
if(ia.phone==="yes")tx.appendChild(mk("small","muted","Hint: your client's form said they have a phone number in GoHighLevel."));
|
|
74599
|
+
box.appendChild(tx);
|
|
74600
|
+
if(t.has("missed")){const ca=mk("div","cds-stack");ca.appendChild(mk("b","","Call answering"));ca.appendChild(mk("p","","What did your client pay for? Choose all that apply."));
|
|
74601
|
+
const cur=new Set(a.callAnswering||[]);
|
|
74602
|
+
const opt=(val,lab,sub,why,greyed)=>{const l=mk("label","cds-check");const c=document.createElement("input");c.type="checkbox";c.checked=cur.has(val);c.disabled=greyed&&!cur.has(val);c.onchange=()=>{if(c.checked)cur.add(val);else cur.delete(val);a.callAnswering=[...cur];keep();draw()};l.appendChild(c);const s=mk("span","");s.appendChild(mk("b","",lab));if(sub)s.appendChild(mk("small","",sub));if(why)s.appendChild(mk("small","muted",why));l.appendChild(s);ca.appendChild(l)};
|
|
74603
|
+
opt("live_voice","An AI answers the phone live","Your client connects their number to it afterwards. Once it is connected, GoHighLevel charges for every minute it talks, usually from your agency's wallet.","",false);
|
|
74604
|
+
opt("chat_bot","An AI replies by text or chat and books the visit","",texting==="yes"?"":"Texting replies need Texting: Yes.",false);
|
|
74605
|
+
opt("missed_text","A text back after a missed call","",texting==="yes"?"":"Needs texting: Yes",texting!=="yes");
|
|
74606
|
+
box.appendChild(ca)}
|
|
74607
|
+
if(t.has("reactivation")){const re=a.reactivation||(a.reactivation={});const rv=mk("div","cds-stack");rv.appendChild(mk("b","","Reactivation"));
|
|
74608
|
+
rv.appendChild(mk("p","","Who should it reach?"));
|
|
74609
|
+
rv.appendChild(radio("rl-"+id,"import","A list your client will give you to import",re.list==="import",false,"",()=>{re.list="import";keep();draw()}));
|
|
74610
|
+
/* rc.3 c6 hole 4: the number is in the sentence as text, and its box sits on its own labelled line. */
|
|
74611
|
+
const nDays=re.notBookedDays||90;
|
|
74612
|
+
rv.appendChild(radio("rl-"+id,"in_account_not_booked","Contacts already here who have not booked in "+nDays+" days",re.list==="in_account_not_booked",false,"",()=>{re.list="in_account_not_booked";if(!re.notBookedDays)re.notBookedDays=90;keep();draw()}));
|
|
74613
|
+
if(re.list==="in_account_not_booked"){const row=mk("label","cds-row");row.appendChild(mk("span","","Days with no booking: "));const days=document.createElement("input");days.type="number";days.min="1";days.max="3650";days.className="cds-input";days.style.width="6em";days.value=String(nDays);days.onchange=()=>{const n=Math.round(Number(days.value));if(n>=1&&n<=3650){re.notBookedDays=n;keep();draw()}else days.value=String(nDays)};row.appendChild(days);rv.appendChild(row);
|
|
74614
|
+
rv.appendChild(mk("small","muted","You will see how many people this reaches and approve it before anyone gets a message."))}
|
|
74615
|
+
rv.appendChild(radio("rl-"+id,"cold_from_now","Only leads who go cold from now on",re.list==="cold_from_now",false,"",()=>{re.list="cold_from_now";keep();draw()}));
|
|
74616
|
+
rv.appendChild(mk("p","","What offer brings them back?"));
|
|
74617
|
+
/* W44: with no offer in the brief, typing one is the only choice, so it starts chosen. */
|
|
74618
|
+
if(!ia.offer&&!re.offer)re.offer={from:"typed",text:""};
|
|
74619
|
+
if(ia.offer)rv.appendChild(radio("ro-"+id,"brief","From the brief: \\u201c"+ia.offer+"\\u201d",re.offer&&re.offer.from==="brief",false,"",()=>{re.offer={from:"brief",text:ia.offer};keep();draw()}));
|
|
74620
|
+
const typed=radio("ro-"+id,"typed","Type one:",re.offer&&re.offer.from==="typed",false,"",()=>{re.offer={from:"typed",text:ot.value.trim()};keep();draw()});
|
|
74621
|
+
/* W45 (Jerry, Safari): inside the label the box drew as a tiny oval. It is its own full-width field under its choice. */
|
|
74622
|
+
const ot=document.createElement("textarea");ot.rows=2;ot.maxLength=300;ot.className="cds-input ask-offer";ot.style.width="100%";ot.style.boxSizing="border-box";ot.placeholder="e.g. a free skin check";ot.value=re.offer&&re.offer.from==="typed"?re.offer.text:"";ot.setAttribute("aria-label","The offer, in your client's words");ot.oninput=()=>{re._typed=ot.value.trim();re.offer={from:"typed",text:re._typed};keep();typed.querySelector("input").checked=true};
|
|
74623
|
+
rv.appendChild(typed);rv.appendChild(ot);
|
|
74624
|
+
rv.appendChild(mk("p","","How?"));
|
|
74625
|
+
const canText=texting==="yes"&&re.textConsentConfirmed===true;
|
|
74626
|
+
rv.appendChild(radio("rc-"+id,"email","Email",re.channel==="email",false,"",()=>{re.channel="email";keep();draw()}));
|
|
74627
|
+
rv.appendChild(radio("rc-"+id,"email_and_text","Email and text",re.channel==="email_and_text",!canText&&re.channel!=="email_and_text",texting!=="yes"?"Needs texting: Yes":re.textConsentConfirmed?"":"Needs your client to confirm these people agreed to get texts",()=>{re.channel="email_and_text";keep();draw()}));
|
|
74628
|
+
const cl=mk("label","cds-check");const cc=document.createElement("input");cc.type="checkbox";cc.checked=re.textConsentConfirmed===true;cc.onchange=()=>{if(cc.checked)re.textConsentConfirmed=true;else{delete re.textConsentConfirmed;if(re.channel==="email_and_text")re.channel="email"}keep();draw()};cl.appendChild(cc);cl.appendChild(mk("span","","My client confirmed these people agreed to get texts"));rv.appendChild(cl);
|
|
74629
|
+
box.appendChild(rv)}}
|
|
74630
|
+
draw();box.redraw=draw;return box}
|
|
74206
74631
|
const BOUGHT_ITEMS=${scriptJson(boughtItemsFor(FEATURE_IDS))};
|
|
74207
74632
|
function scopeChosen(state,id){const s=(state.scope||{})[id];const v=(state.boughtViews||{})[id];return Boolean(s&&(s.wholePreset||(Array.isArray(s.features)&&s.features.length)))&&!(v&&v.ready===false)}
|
|
74208
74633
|
function openWhatTheyBought(id){rOpen[id]=true;rSeen[id]=true;const go=()=>{render(lastState,lastRun);requestAnimationFrame(()=>{const t=document.getElementById("wtb-"+id);if(t){t.scrollIntoView({behavior:"smooth",block:"start"});const f=t.querySelector("button,input,select");if(f)f.focus({preventScroll:true})}})};if(!rData[id])rLoad(id).then(go,go);else go()}
|
|
@@ -74225,7 +74650,7 @@ function scopeBlock(id,state){
|
|
|
74225
74650
|
const v=boughtView(state,id);
|
|
74226
74651
|
if(!bsHideAssess)sc.appendChild(assessmentQuestion(id,v));
|
|
74227
74652
|
sc.appendChild(mk("h4","cds-panel-label","What they bought"));
|
|
74228
|
-
sc.appendChild(mk("p","",v.source==="all"?"Everything starts ticked. Untick what this client did not buy.":v.source==="assessment"?"Ticked from what the Assessment recorded as bought. Untick anything they did not buy.":"Tick what the client bought. Only the items marked
|
|
74653
|
+
sc.appendChild(mk("p","",v.source==="all"?"Everything starts ticked. Untick what this client did not buy.":v.source==="assessment"?"Ticked from what the Assessment recorded as bought. Untick anything they did not buy.":"Tick what the client bought. Only the items marked Will build get built."));
|
|
74229
74654
|
if(!profCache){profLoad();const p=document.createElement("p");p.className="muted";p.textContent="Loading your packages\u2026";sc.appendChild(p);return sc}
|
|
74230
74655
|
const saved=(state.scope||{})[id]||null;const pk=(profCache.packages||[]);
|
|
74231
74656
|
const sel=document.createElement("select");sel.className="cds-select";sel.setAttribute("aria-label","Package they bought");
|
|
@@ -74236,17 +74661,17 @@ function scopeBlock(id,state){
|
|
|
74236
74661
|
const keepScope=()=>{scopeDraft[id]={ticked:[...ticked],pkg:sel.value,base:base};unsaved.hidden=false};
|
|
74237
74662
|
const everything=document.createElement("button");everything.className="cds-btn ghost";everything.textContent="Tick everything";
|
|
74238
74663
|
everything.title="Ticks all fourteen. Nothing is saved until you press Save what they bought.";
|
|
74239
|
-
everything.onclick=()=>{ticked=new Set(v.items.map(i=>i.id));keepScope();drawBoxes();drawFigs()};
|
|
74664
|
+
everything.onclick=()=>{ticked=new Set(v.items.map(i=>i.id));keepScope();drawBoxes();drawFigs();if(askEl)askEl.redraw()};
|
|
74240
74665
|
sc.appendChild(everything);
|
|
74241
74666
|
const boxes=document.createElement("div");boxes.className="cds-stack";
|
|
74242
74667
|
function drawBoxes(){boxes.innerHTML="";for(const it of v.items){const l=document.createElement("label");l.className="cds-check";
|
|
74243
|
-
l.dataset.k=it.id;const c=document.createElement("input");c.type="checkbox";c.checked=ticked.has(it.id);c.onchange=()=>{if(c.checked)ticked.add(it.id);else ticked.delete(it.id);keepScope();drawFigs()};l.appendChild(c);
|
|
74668
|
+
l.dataset.k=it.id;const c=document.createElement("input");c.type="checkbox";c.checked=ticked.has(it.id);c.onchange=()=>{if(c.checked)ticked.add(it.id);else ticked.delete(it.id);keepScope();drawFigs();if(askEl)askEl.redraw()};l.appendChild(c);
|
|
74244
74669
|
const t=document.createElement("span");const tb=document.createElement("b");tb.textContent=it.name;t.appendChild(tb);t.appendChild(mk("small","",it.means));
|
|
74245
|
-
t.appendChild(mk("small","bought-delivery",it.delivery));if(it.next)t.appendChild(mk("small","bought-next",it.next));l.appendChild(t);boxes.appendChild(l)}}
|
|
74670
|
+
t.appendChild(mk("small","bought-delivery",it.delivery));if(it.next&&!(((asksDraft[id]&&asksDraft[id].a.callAnswering)||(v.asks||{}).callAnswering||[]).includes("live_voice")))t.appendChild(mk("small","bought-next",it.next));l.appendChild(t);boxes.appendChild(l)}}
|
|
74246
74671
|
const figs=((state.assessmentFigures||{})[id])||{};
|
|
74247
74672
|
function drawFigs(){for(const l of boxes.querySelectorAll("label")){const it=v.items.find(i=>i.id===l.dataset.k);const old=l.querySelector("em");if(old)old.remove();if(it&&it.feature&&figs[it.feature]){const e=document.createElement("em");e.className="cds-chip"+(ticked.has(it.id)?"":" warn");e.textContent="Assessment: "+figs[it.feature]+(ticked.has(it.id)?".":". Not ticked.");l.querySelector("span").appendChild(e)}}}
|
|
74248
74673
|
/* A package names build features: it ticks their items and unticks the other built ones; the rest stay as they are. */
|
|
74249
|
-
sel.onchange=()=>{const p=pk.find(x=>x.id===sel.value);if(p)for(const it of v.items){if(!it.feature)continue;if((p.features||[]).includes(it.feature))ticked.add(it.id);else ticked.delete(it.id)}keepScope();drawBoxes();drawFigs()};drawBoxes();drawFigs();
|
|
74674
|
+
sel.onchange=()=>{const p=pk.find(x=>x.id===sel.value);if(p)for(const it of v.items){if(!it.feature)continue;if((p.features||[]).includes(it.feature))ticked.add(it.id);else ticked.delete(it.id)}keepScope();drawBoxes();drawFigs();if(askEl)askEl.redraw()};let askEl=null;drawBoxes();drawFigs();askEl=asksBox(id,v,()=>ticked,keepScope);
|
|
74250
74675
|
const note=document.createElement("p");note.className="muted";note.textContent="Always built, whatever is ticked: the pipeline and its stages, the fields, tags and custom values, the intake form, the calendar, and the reply routing. A ticked item that is not built automatically becomes one of your jobs in the account.";
|
|
74251
74676
|
const sv=document.createElement("button");sv.className="cds-btn ghost";sv.textContent="Save what they bought";
|
|
74252
74677
|
const status=document.createElement("p");status.className="muted";status.setAttribute("role","status");
|
|
@@ -74254,14 +74679,15 @@ function scopeBlock(id,state){
|
|
|
74254
74679
|
if(!v.items.some(it=>it.kind==="built"&&ticked.has(it.id))){status.textContent="None of the ticked items is built for this client's industry. Tick at least one item marked Built.";return}
|
|
74255
74680
|
/* Run 8 finding 9: about 5 seconds with nothing showing it was working. */
|
|
74256
74681
|
sv.disabled=true;const was=sv.textContent;sv.textContent="Saving\u2026";status.textContent="Saving what they bought\u2026";
|
|
74257
|
-
const body={locationId:id,bought:[...ticked]};if(sel.value)body.package=sel.value;const r=await postJSON("/api/scope",body);
|
|
74258
|
-
if(r){unsaved.hidden=true;if(r.approvalWithdrawn)boughtNotice[id]=r.message;else delete boughtNotice[id];delete scopeDraft[id];delete assessmentCards[id];status.textContent="Saved.";load()}
|
|
74682
|
+
const body={locationId:id,bought:[...ticked]};if(sel.value)body.package=sel.value;if(asksDraft[id])body.asks=asksClean(asksDraft[id].a);const r=await postJSON("/api/scope",body);
|
|
74683
|
+
if(r){unsaved.hidden=true;delete asksDraft[id];if(r.approvalWithdrawn)boughtNotice[id]=r.message;else delete boughtNotice[id];delete scopeDraft[id];delete assessmentCards[id];status.textContent="Saved.";load()}
|
|
74259
74684
|
if(sv.isConnected){sv.disabled=false;sv.textContent=was}};
|
|
74260
74685
|
/* Audit 2026-10-28 #9: the always-built pipeline counts as built ("2 bought, 1 built" while it is always built). */
|
|
74261
|
-
|
|
74262
|
-
|
|
74686
|
+
/* rc.3 c6 hole 2: the count follows the labels: what will be built (or was) and what waits on your client's answer. */
|
|
74687
|
+
const tickedItems=v.items.filter(it=>v.ticks.includes(it.id));const nBought=v.ticks.length,nBuilt=tickedItems.filter(it=>/^(Will build|Built|Always built): /.test(it.delivery)).length,nHeld=tickedItems.filter(it=>/^(Waiting for your client's answer|Not built, your client said not yet|Not built yet, chosen and coming in a later update): /.test(it.delivery)).length;
|
|
74688
|
+
status.textContent=saved&&v.source==="saved"?"Saved: "+nBought+" of "+v.items.length+" bought. "+nBuilt+(nBuilt===1?" is":" are")+" built automatically"+(nHeld?", "+nHeld+(nHeld===1?" waits":" wait")+" for your client's answer":"")+(saved.package?(pk.find(x=>x.id===saved.package)?", from the "+pk.find(x=>x.id===saved.package).name+" package":", from a package no longer on Your agency"):"")+". The build follows this.":saved?(saved.wholePreset?"Saved: everything in the plan. The build follows this.":"Saved before this list: "+saved.features.length+" features. Save again to record what they bought."):"Not saved yet. Save what they bought before approving.";
|
|
74263
74689
|
const unsaved=mk("span","cds-chip warn","Not saved yet");unsaved.hidden=!scopeDraft[id];
|
|
74264
|
-
sc.appendChild(sel);sc.appendChild(boxes);sc.appendChild(note);sc.appendChild(sv);sc.appendChild(unsaved);sc.appendChild(status);
|
|
74690
|
+
sc.appendChild(sel);sc.appendChild(boxes);sc.appendChild(askEl);sc.appendChild(note);sc.appendChild(sv);sc.appendChild(unsaved);sc.appendChild(status);
|
|
74265
74691
|
if(boughtNotice[id]){const n=mk("p","cds-callout warn",boughtNotice[id]);n.setAttribute("role","alert");sc.appendChild(n)}
|
|
74266
74692
|
return sc}
|
|
74267
74693
|
async function rLoad(id){const tok=clientToken();const r=await(await fetch("/api/review?locationId="+id)).json();rData[id]=r;renderFor(tok)}
|
|
@@ -74290,6 +74716,10 @@ function reviewPanel(id,name,state,run){const r=rData[id];const d=document.creat
|
|
|
74290
74716
|
rBusy[id]=false;load()};
|
|
74291
74717
|
if(busy){const p=document.createElement("p");p.className="muted";
|
|
74292
74718
|
p.textContent="Reading their answers and writing the review, usually 1 to 2 minutes. This panel updates itself when it is ready.";d.appendChild(p)}
|
|
74719
|
+
/* rc.3 c6 hole 1: a review written before the answers changed may promise what the build will not make. */
|
|
74720
|
+
if(r&&r.ok&&r.stale&&!busy){const w=mk("p","cds-callout warn","This review was written before your client's answers or what they bought changed, so it may promise what this build will not make. Press Regenerate before you share it; the client document waits for the new one.");w.setAttribute("role","alert");d.appendChild(w)}
|
|
74721
|
+
if(r&&r.ok&&!r.stale&&r.graded!==true&&!busy){const w=mk("p","cds-callout warn","Not checked for over-promises. Regenerate.");w.setAttribute("role","alert");d.appendChild(w)}
|
|
74722
|
+
if(r&&r.ok&&Array.isArray(r.trimmed)&&r.trimmed.length&&!busy)d.appendChild(mk("p","muted","Taken out because they promised more than this build makes: "+r.trimmed.join("; ")+"."));
|
|
74293
74723
|
if(!r||!r.ok){if(!busy){const p=document.createElement("p");if(r&&r.error&&r.error!=="not generated yet"){p.className="cds-callout warn";p.setAttribute("role","alert")}else p.className="muted";
|
|
74294
74724
|
p.textContent=r&&r.error?r.error:"Turn their intake answers into strategy: who they really sell to, whether the offer holds up, what the account is missing, and what to build first.";
|
|
74295
74725
|
d.appendChild(p)}
|
|
@@ -74316,15 +74746,16 @@ function reviewPanel(id,name,state,run){const r=rData[id];const d=document.creat
|
|
|
74316
74746
|
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);
|
|
74317
74747
|
const gl=mk("ul","cds-list gaps");
|
|
74318
74748
|
for(const g of r.gaps){const gg=mk("li","gap "+(g.priority||"soon"));
|
|
74319
|
-
const pri=g.priority||"soon";const gt=mk("span","ttl",String(g.title||""));gt.appendChild(mk("span","sub",moneyText(
|
|
74749
|
+
const pri=g.priority||"soon";const gt=mk("span","ttl",String(g.title||""));gt.appendChild(mk("span","sub",moneyText(String(g.cost||""))));/* rc.5: the fix is never the model's words; what we build is listed below, from the build */gg.appendChild(gt);
|
|
74320
74750
|
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)}
|
|
74321
74751
|
s3.appendChild(gl);
|
|
74322
74752
|
d.appendChild(s3)}
|
|
74323
|
-
|
|
74753
|
+
/* rc.5 (c6 ruling): What we build is the build's own will-build list and held lines, never the model's prose. */
|
|
74754
|
+
{const pr=r.promises;if(pr&&((pr.willBuild||[]).length||(pr.held||[]).length)){const s4=document.createElement("div");s4.className="cds-stack";s4.innerHTML="<h4 class='cds-panel-label'>What we build</h4>";
|
|
74324
74755
|
const bl=document.createElement("ul");bl.className="cds-list";
|
|
74325
|
-
for(const
|
|
74326
|
-
|
|
74327
|
-
s4.appendChild(bl);d.appendChild(s4)}
|
|
74756
|
+
for(const w of (pr.willBuild||[])){const bb=document.createElement("li");bb.className="buildline";bb.appendChild(mk("span","ttl",String(w)));bl.appendChild(bb)}
|
|
74757
|
+
for(const h of (pr.held||[])){const bb=document.createElement("li");bb.className="buildline";bb.appendChild(mk("span","ttl muted",String(h)));bl.appendChild(bb)}
|
|
74758
|
+
s4.appendChild(bl);d.appendChild(s4)}}
|
|
74328
74759
|
if((r.unknowns||[]).length){const s5=document.createElement("div");s5.className="cds-stack";s5.innerHTML="<h4 class='cds-panel-label'>Ask them next</h4>";
|
|
74329
74760
|
const p=document.createElement("p");p.textContent=r.unknowns.join(" \xB7 ");s5.appendChild(p);d.appendChild(s5)}
|
|
74330
74761
|
d.appendChild(scopeBlock(id,state));
|
|
@@ -75407,7 +75838,9 @@ function renderCapability(m,state,run){
|
|
|
75407
75838
|
if(CAP.key==="intake"){const go=mk("a","cds-btn cap-build-account","Build this client's account");end.appendChild(asClientLink(go,"/new",c.locationId))}
|
|
75408
75839
|
m.appendChild(screenHead(CAP.label,CAP.what,CAP.groupLabel,end));
|
|
75409
75840
|
const sc=document.getElementById("side-client");if(sc)sc.textContent=c.name;
|
|
75410
|
-
|
|
75841
|
+
/* W43 (Jerry's watched walk, 2026-10-29): he saw no Build button on the page Your clients leads to. The header carries
|
|
75842
|
+
one; this puts the next step at the top of the page itself, where nobody can miss it. */
|
|
75843
|
+
if(CAP.key==="intake"){const nx=mk("div","cds-callout cap-next-build");nx.appendChild(mk("span","ttl","Next step: build this client's account"));nx.appendChild(mk("span","","Answers, approval and the build itself are on the build page."));const go=mk("a","cds-btn cap-build-account-main","Build this client's account");nx.appendChild(asClientLink(go,"/new",c.locationId));m.appendChild(nx);renderIntakeCapability(m,c);return}
|
|
75411
75844
|
const built=capBuiltItems(view,CAP.key);
|
|
75412
75845
|
const toCheck=built.filter(x=>x.status==="uncertain");
|
|
75413
75846
|
const health=HEALTH_PAGES.includes(CAP.key);
|
|
@@ -75448,7 +75881,25 @@ function renderCapability(m,state,run){
|
|
|
75448
75881
|
if(health)m.appendChild(capOwedPanel(owed,(state.jobCounts||{})[c.locationId],((view||{}).handoff||{}).lines))}
|
|
75449
75882
|
/* What this page reads live: one list for one read. With several reads, a read of 3 or fewer shows its rows under its own label,
|
|
75450
75883
|
and a longer one folds under a closed "See the N \u2026". A count read is one row: the number. */
|
|
75884
|
+
/* Sign-off and handoff (command-center 2026-10-29): read from this client's own build record, the same one the build
|
|
75885
|
+
steps show (F2): what is Built, what waits on the client (the held lines), and the jobs still open. Never GoHighLevel's
|
|
75886
|
+
words for it, and never a claim the record does not hold. */
|
|
75887
|
+
const HELD_RE=/^(Waiting for your client's answer|Not built, your client said not yet|Not built yet, chosen and coming in a later update): /;
|
|
75888
|
+
function capHandoffRecord(card,c){
|
|
75889
|
+
const v=((lastState&&lastState.boughtViews)||{})[c.locationId];
|
|
75890
|
+
if(!v){card.appendChild(mk("p","muted","Nothing is on record for this client yet."));return}
|
|
75891
|
+
const ticked=new Set(v.ticks||[]);const items=(v.items||[]).filter(i=>ticked.has(i.id));
|
|
75892
|
+
const built=items.filter(i=>/^Built: /.test(i.delivery)).map(i=>i.delivery.replace(/^Built: /,""));
|
|
75893
|
+
const waiting=items.filter(i=>HELD_RE.test(i.delivery)).map(i=>i.delivery);
|
|
75894
|
+
const open=(((lastState&&lastState.manualTasks)||{})[c.locationId]||[]).filter(t=>!t.claimed&&!HELD_RE.test(String(t.text))).map(t=>String(t.text));
|
|
75895
|
+
const sec=(title,list,none)=>{const b=mk("div","cap-read");b.appendChild(mk("b","",title));if(!list.length)b.appendChild(mk("p","muted",none));else{const u=mk("ul","");for(const x of list.slice(0,8))u.appendChild(mk("li","",x));if(list.length>8)u.appendChild(mk("li","muted","and "+(list.length-8)+" more"));b.appendChild(u)}card.appendChild(b)};
|
|
75896
|
+
card.appendChild(mk("p","muted","From this client's build record on this computer, the same record the build steps show."));
|
|
75897
|
+
sec("Built",built,"Nothing is recorded as built yet.");
|
|
75898
|
+
sec("Waiting on your client",waiting,"Nothing is waiting on your client.");
|
|
75899
|
+
sec("Still open",open,"No open jobs.");
|
|
75900
|
+
}
|
|
75451
75901
|
function capReadsInto(card,c,hasBuild,foldAll){
|
|
75902
|
+
if(CAP.key==="handoff"){capHandoffRecord(card,c);return}
|
|
75452
75903
|
if(!CAP.reads.length){card.appendChild(capEmpty("This page does not read the account yet","Say the words below to Claude, and it reads what this account has for this job, live."));return}
|
|
75453
75904
|
if(capReadsFor!==c.locationId){card.appendChild(mk("p","muted","Reading this account\u2026"));loadCapReads(c.locationId);return}
|
|
75454
75905
|
if(!capReads){card.appendChild(mk("p","muted","Reading this account\u2026"));return}
|
|
@@ -76995,10 +77446,11 @@ async function runDashboard(argv) {
|
|
|
76995
77446
|
const hit = await pickIntakeContact(c, locationId2, byId2, snap.contactId, INTAKE_FORM_NAME2);
|
|
76996
77447
|
const completed = st0.intakeCompleted?.[locationId2]?.answers;
|
|
76997
77448
|
const answers = Object.fromEntries(Object.entries(completed ?? normalizedIntake({ answers: hit?.answers ?? {}, answeredAt: snap.answeredAt }, st0.intakeDrafts?.[locationId2])).map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : v]));
|
|
76998
|
-
|
|
77449
|
+
const review = await generateReview(
|
|
76999
77450
|
(prompt) => callClaudeRecorded(claudeBin(), ["-p", prompt, "--allowedTools", "", "--disallowedTools", `Bash,Write,Edit,WebFetch,WebSearch,Task,Read,Glob,Grep,${MCP_DENY}`, "--max-turns", "6", ...NO_MCP_ARGS], { locationId: locationId2, stage: 1, kind: "review" }),
|
|
77000
|
-
{ clientName, industry: st0.accounts?.[locationId2]?.industry, answers, ...st0.assessmentAttachments?.[locationId2] ? { assessmentLines: findingLines(st0.assessmentAttachments[locationId2]) } : {} }
|
|
77451
|
+
{ clientName, industry: st0.accounts?.[locationId2]?.industry, answers, ...st0.assessmentAttachments?.[locationId2] ? { assessmentLines: findingLines(st0.assessmentAttachments[locationId2]) } : {}, promises: reviewPromisesFor(st0, locationId2, locationId2) }
|
|
77001
77452
|
);
|
|
77453
|
+
return review.ok ? { ...review, promisesHash: contentHash2(review.promises ?? reviewPromisesFor(st0, locationId2, locationId2)) } : review;
|
|
77002
77454
|
};
|
|
77003
77455
|
const currentRoster = () => {
|
|
77004
77456
|
const registered = registry2.listLocations();
|
|
@@ -77652,7 +78104,12 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
77652
78104
|
const assessmentFigures = {};
|
|
77653
78105
|
for (const loc of Object.keys(st.assessmentAttachments ?? {})) assessmentFigures[loc] = assessmentFiguresFor(st, loc);
|
|
77654
78106
|
const scopeOffered = Object.fromEntries(rosterNow.map((c) => [c.locationId, offeredFeaturesFor(st, c.locationId)]));
|
|
77655
|
-
const boughtViews = Object.fromEntries(rosterNow.map((c) => [c.locationId,
|
|
78107
|
+
const boughtViews = Object.fromEntries(rosterNow.map((c) => [c.locationId, {
|
|
78108
|
+
...boughtViewFor(st, c.locationId, scopeOffered[c.locationId], boughtWordingFor(st, c.locationId), buildStatusFor(st, c.locationId, c.locationId)),
|
|
78109
|
+
// Ask first (2026-10-29): the saved answers and what the client's own form said, for the questions box.
|
|
78110
|
+
asks: st.asks?.[c.locationId] ?? {},
|
|
78111
|
+
intakeAsks: intakeAskFacts(st.intakeCompleted?.[c.locationId]?.answers ?? st.intakeInputs?.[c.locationId]?.answers)
|
|
78112
|
+
}]));
|
|
77656
78113
|
const buildViews = Object.fromEntries(rosterNow.map((c) => {
|
|
77657
78114
|
const saved = readSavedPlan(c.locationId);
|
|
77658
78115
|
const approval = st.planApprovals?.[c.locationId];
|
|
@@ -77926,7 +78383,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
77926
78383
|
if (stage === 3) {
|
|
77927
78384
|
assertStaffUnchanged(st0.planApprovals?.[rk], st0.staffRoster?.[locationId2]);
|
|
77928
78385
|
const sp = readSavedPlan(rk);
|
|
77929
|
-
if (!sp || !
|
|
78386
|
+
if (!sp || !savedPlanFitsAnswers(sp, st0, rk)) throw new Error("The features selected for this client, or the answers about them, changed after its plan was prepared. Prepare the plan again, approve it, then build.");
|
|
77930
78387
|
}
|
|
77931
78388
|
if (stage === 4) {
|
|
77932
78389
|
const build = st0.outcomes?.[`${rk}:3`];
|
|
@@ -78651,9 +79108,19 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
78651
79108
|
res.end("No review generated for this client yet.");
|
|
78652
79109
|
return;
|
|
78653
79110
|
}
|
|
79111
|
+
if (reviewIsStale(st, locationId2, review)) {
|
|
79112
|
+
res.writeHead(409, { "Content-Type": "text/plain; charset=utf-8" });
|
|
79113
|
+
res.end("This client's Blueprint Review was written before their answers or what they bought changed. Press Regenerate on the review, then open the client document again.");
|
|
79114
|
+
return;
|
|
79115
|
+
}
|
|
79116
|
+
if (review.graded !== true) {
|
|
79117
|
+
res.writeHead(409, { "Content-Type": "text/plain; charset=utf-8" });
|
|
79118
|
+
res.end("This client's Blueprint Review was not checked for over-promises. Press Regenerate on the review, then open the client document again.");
|
|
79119
|
+
return;
|
|
79120
|
+
}
|
|
78654
79121
|
const name2 = clientDisplayName(st, locationId2, currentRoster().find((r) => r.locationId === locationId2)?.name || locationId2);
|
|
78655
79122
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
78656
|
-
res.end(reviewDocumentHtml(review, { scope: documentScopeFor(st, locationId2, readSavedPlan(locationId2)), assessment: st.assessmentAttachments?.[locationId2], purchasedFeatureIds: assessmentPurchasesFor(st, locationId2), bought: boughtDocumentFor(st, locationId2, offeredFeaturesFor(st, locationId2), boughtWordingFor(st, locationId2)), planLines: readSavedPlan(locationId2) ? executablePlanLines(readSavedPlan(locationId2)) : [], clientName: name2, agencyName: q3.get("agency") || st.agencyName, accent: q3.get("accent") || st.agencyAccent }));
|
|
79123
|
+
res.end(reviewDocumentHtml(review, { scope: documentScopeFor(st, locationId2, readSavedPlan(locationId2)), assessment: st.assessmentAttachments?.[locationId2], purchasedFeatureIds: assessmentPurchasesFor(st, locationId2), bought: boughtDocumentFor(st, locationId2, offeredFeaturesFor(st, locationId2), boughtWordingFor(st, locationId2), buildStatusFor(st, locationId2, locationId2)), planLines: readSavedPlan(locationId2) ? executablePlanLines(readSavedPlan(locationId2)) : [], clientName: name2, agencyName: q3.get("agency") || st.agencyName, accent: q3.get("accent") || st.agencyAccent }));
|
|
78657
79124
|
return;
|
|
78658
79125
|
}
|
|
78659
79126
|
if (req.method === "GET" && url === "/api/review") {
|
|
@@ -78661,7 +79128,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
78661
79128
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
78662
79129
|
const stR = readCockpitState();
|
|
78663
79130
|
const businessName = clientDisplayName(stR, locationId2, currentRoster().find((r) => r.locationId === locationId2)?.name ?? "");
|
|
78664
|
-
|
|
79131
|
+
const savedReview = stR.reviews?.[locationId2];
|
|
79132
|
+
res.end(JSON.stringify({ ...savedReview ?? { ok: false, error: "not generated yet" }, ...businessName ? { businessName } : {}, ...reviewIsStale(stR, locationId2, savedReview) ? { stale: true } : {} }));
|
|
78665
79133
|
return;
|
|
78666
79134
|
}
|
|
78667
79135
|
if (req.method === "POST" && url === "/api/shared-adopt") {
|
|
@@ -79205,7 +79673,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
79205
79673
|
if (req.method === "POST" && url === "/api/scope") {
|
|
79206
79674
|
readBody(req, res, 1e4, (body) => {
|
|
79207
79675
|
try {
|
|
79208
|
-
const { locationId: locationId2, clientKey, package: pkg2, features, bought } = JSON.parse(body);
|
|
79676
|
+
const { locationId: locationId2, clientKey, package: pkg2, features, bought, asks: asksIn } = JSON.parse(body);
|
|
79209
79677
|
const who2 = cockpitClient(readCockpitState(), currentRoster(), locationId2, clientKey);
|
|
79210
79678
|
let ticks;
|
|
79211
79679
|
let derived;
|
|
@@ -79229,21 +79697,36 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
79229
79697
|
else ticks = savedTicks[rk];
|
|
79230
79698
|
nextState = { ...nextState, bought: savedTicks };
|
|
79231
79699
|
if (!Object.keys(savedTicks).length) delete nextState.bought;
|
|
79700
|
+
const intakeSms = intakeChannelAnswers(nextState.intakeCompleted?.[who2.locationId]?.answers ?? nextState.intakeInputs?.[who2.locationId]?.answers).sms;
|
|
79701
|
+
if (asksIn !== void 0) {
|
|
79702
|
+
const parsed2 = parseAsks(asksIn);
|
|
79703
|
+
if (!parsed2.ok) throw new Error(parsed2.error);
|
|
79704
|
+
const clash = asksContradiction(parsed2.asks, effectiveTexting(parsed2.asks, intakeSms));
|
|
79705
|
+
if (clash) throw new Error(clash);
|
|
79706
|
+
const all = { ...nextState.asks ?? {} };
|
|
79707
|
+
if (Object.keys(parsed2.asks).length) all[rk] = parsed2.asks;
|
|
79708
|
+
else delete all[rk];
|
|
79709
|
+
nextState = { ...nextState, asks: all };
|
|
79710
|
+
if (!Object.keys(all).length) delete nextState.asks;
|
|
79711
|
+
}
|
|
79232
79712
|
const { state: bound, withdrawn } = withdrawIfBindingChanged(nextState, rk);
|
|
79233
79713
|
nextState = bound;
|
|
79234
79714
|
{
|
|
79235
79715
|
const offered = offeredFeaturesFor(nextState, rk);
|
|
79236
79716
|
const channels = intakeChannelAnswers(nextState.intakeCompleted?.[who2.locationId]?.answers ?? nextState.intakeInputs?.[who2.locationId]?.answers);
|
|
79717
|
+
const texting = effectiveTexting(nextState.asks?.[rk], channels.sms);
|
|
79237
79718
|
const scopeNow = nextState.scope?.[rk];
|
|
79238
79719
|
const excluded = ticks ? boughtButExcludedJobs(ticks, offered, !scopeNow || scopeNow.wholePreset ? "all" : scopeNow.features ?? []) : [];
|
|
79239
|
-
const
|
|
79720
|
+
const held2 = ticks ? heldJobs(heldItems(ticks, nextState.asks?.[rk], texting)) : [];
|
|
79721
|
+
const boughtList = ticks ? [...boughtJobs(ticks, offered, { ...channels, sms: texting === "yes" }), ...excluded, ...held2] : [];
|
|
79240
79722
|
replaceBuildObligationsOfType(rk, BOUGHT_JOB_TYPE, boughtList);
|
|
79241
79723
|
const tasks2 = withBoughtJobs(nextState.manualTasks?.[rk] ?? [], ticks ? catalogJobs({ handoffs: [], obligations: boughtList }) : []);
|
|
79242
79724
|
if (tasks2.length || nextState.manualTasks?.[rk]) nextState = { ...nextState, manualTasks: { ...nextState.manualTasks, [rk]: tasks2 } };
|
|
79243
79725
|
}
|
|
79244
79726
|
writeCockpitState(nextState);
|
|
79245
79727
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
79246
|
-
|
|
79728
|
+
const builtBefore = nextState.outcomes?.[`${rk}:3`]?.ok === true;
|
|
79729
|
+
res.end(JSON.stringify({ ok: true, scope: readCockpitState().scope?.[rk] ?? null, ...withdrawn ? { approvalWithdrawn: true, message: builtBefore && asksIn !== void 0 ? BUILD_WHAT_WAS_WAITING : APPROVAL_WITHDRAWN_FOR_SCOPE } : {} }));
|
|
79247
79730
|
} catch (e) {
|
|
79248
79731
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
79249
79732
|
res.end(JSON.stringify({ ok: false, error: e instanceof Error ? e.message : "bad request" }));
|
|
@@ -79434,7 +79917,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
79434
79917
|
process.on("SIGTERM", stop3);
|
|
79435
79918
|
});
|
|
79436
79919
|
}
|
|
79437
|
-
var import_zod94, import_node_util5, fs53, path53, http2, import_child_process4, import_child_process5, osmod, VIEWER_COCKPIT_MSG, STAGES2, ACCOUNT_TYPES, SECOND_CLIENT_NO_INTAKE, stateRecovery, stateSaveFs, CHOOSE_WHAT_THEY_BOUGHT, CHOOSE_BEFORE_BUILD, TEXT_SNIPPETS_LOCKED, SCOPE_REFUSAL, APPROVAL_WITHDRAWN_FOR_STAFF, APPROVAL_WITHDRAWN_NO_TEAM, HANDOFF_WORKFLOW, NOTHING_BOUGHT, NOTHING_BUILT_FROM_TICKS, ASSESSMENT_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_ASSESSMENT, APPROVE_NEEDS_INTAKE, APPROVE_NEEDS_BRIEF, INDUSTRY_FIRST, NOTHING_TICKED_IN_PLAN, SCOPE_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_SCOPE, INACTIVE, OFF_LICENSE, LICENSE_RECHECK_MS, activeRun2, runStarting, buildOneTime, oneTimeView, runForTests, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, queueScanWatch, queueScanOnce, ASSESSMENT_PIPELINE_MISSING, GATE_BODY, GATE_BODY_MAX, DOC_NOT_SAVED, NO_KEY_ON_FILE, PARKED_NOT_BUILT, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_READ_TRANSIENT, INTAKE_REFRESH_RETRYING, ACCOUNT_CHANGED_DURING_READ, commitWithoutRead, SNAPSHOTS_CHANGED_DURING_READ, IntakeColdError, 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, NO_SUCH_PAGE, CHOOSE_CLIENT, ONE_CLIENT_ONLY, anthropicKeyWrites, SEAT_TOKEN_HEADER, notSignedInMsg, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS, COCKPIT_AFTER_CANCEL_PATHS, licenceRefusedHooks, ConnectionRequest, DisconnectRequest, ReportBrandSaveRequest;
|
|
79920
|
+
var import_zod94, import_node_util5, fs53, path53, http2, import_child_process4, import_child_process5, osmod, VIEWER_COCKPIT_MSG, STAGES2, ACCOUNT_TYPES, SECOND_CLIENT_NO_INTAKE, stateRecovery, stateSaveFs, CHOOSE_WHAT_THEY_BOUGHT, CHOOSE_BEFORE_BUILD, TEXT_SNIPPETS_LOCKED, SCOPE_REFUSAL, APPROVAL_WITHDRAWN_FOR_STAFF, APPROVAL_WITHDRAWN_NO_TEAM, HANDOFF_WORKFLOW, NOTHING_BOUGHT, NOTHING_BUILT_FROM_TICKS, ASSESSMENT_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_ASSESSMENT, APPROVE_NEEDS_INTAKE, APPROVE_NEEDS_BRIEF, INDUSTRY_FIRST, NOTHING_TICKED_IN_PLAN, SCOPE_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_SCOPE, BUILD_WHAT_WAS_WAITING, INACTIVE, OFF_LICENSE, LICENSE_RECHECK_MS, activeRun2, runStarting, buildOneTime, oneTimeView, runForTests, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, queueScanWatch, queueScanOnce, ASSESSMENT_PIPELINE_MISSING, GATE_BODY, GATE_BODY_MAX, DOC_NOT_SAVED, NO_KEY_ON_FILE, PARKED_NOT_BUILT, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_READ_TRANSIENT, INTAKE_REFRESH_RETRYING, ACCOUNT_CHANGED_DURING_READ, commitWithoutRead, SNAPSHOTS_CHANGED_DURING_READ, IntakeColdError, 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, NO_SUCH_PAGE, CHOOSE_CLIENT, ONE_CLIENT_ONLY, anthropicKeyWrites, SEAT_TOKEN_HEADER, notSignedInMsg, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS, COCKPIT_AFTER_CANCEL_PATHS, licenceRefusedHooks, ConnectionRequest, DisconnectRequest, ReportBrandSaveRequest;
|
|
79438
79921
|
var init_dashboard = __esm({
|
|
79439
79922
|
"src/dashboard.ts"() {
|
|
79440
79923
|
"use strict";
|
|
@@ -79574,6 +80057,7 @@ var init_dashboard = __esm({
|
|
|
79574
80057
|
init_daily_brief2();
|
|
79575
80058
|
init_daily_brief_collect();
|
|
79576
80059
|
init_brief_deadline();
|
|
80060
|
+
init_client_asks();
|
|
79577
80061
|
VIEWER_COCKPIT_MSG = "Your team login on this license is view only, and Command OS runs work that changes accounts, so it did not open. Ask the license owner to make you a team member, then open Command OS again.";
|
|
79578
80062
|
STAGES2 = [
|
|
79579
80063
|
"Intake",
|
|
@@ -79608,6 +80092,7 @@ var init_dashboard = __esm({
|
|
|
79608
80092
|
NOTHING_TICKED_IN_PLAN = "None of the ticked features is in the plan for this client's industry, so nothing was saved. Tick features from the list, or choose Everything in the plan.";
|
|
79609
80093
|
SCOPE_CHANGED_SINCE_APPROVAL = "What they bought changed after the build was approved, so Build did not start. Approve this plan again, then build.";
|
|
79610
80094
|
APPROVAL_WITHDRAWN_FOR_SCOPE = "What they bought changed, so the build approval was withdrawn. Approve this plan again before building.";
|
|
80095
|
+
BUILD_WHAT_WAS_WAITING = "Your client's answers changed what gets built, so the build approval was withdrawn. Approve the updated plan, then run Build: it adds only what was waiting and makes nothing twice.";
|
|
79611
80096
|
INACTIVE = "License is not active. Contact support@ghlcommand.com.";
|
|
79612
80097
|
OFF_LICENSE = "This computer is no longer on your license. Free a device on your license, then restart Command OS.";
|
|
79613
80098
|
LICENSE_RECHECK_MS = 4 * 60 * 60 * 1e3;
|
|
@@ -91514,18 +91999,22 @@ function claimBuildLock(locationId2, base) {
|
|
|
91514
91999
|
}
|
|
91515
92000
|
return null;
|
|
91516
92001
|
}
|
|
91517
|
-
function takeOver(lock, seen) {
|
|
92002
|
+
function takeOver(lock, seen, retried = false) {
|
|
91518
92003
|
const mutex = `${lock}.takeover`;
|
|
91519
92004
|
let fd;
|
|
91520
92005
|
try {
|
|
91521
92006
|
fd = fs63.openSync(mutex, "wx", 384);
|
|
91522
92007
|
} catch (e) {
|
|
91523
92008
|
if (e.code !== "EEXIST") throw e;
|
|
92009
|
+
let aged = false;
|
|
91524
92010
|
try {
|
|
91525
|
-
if (Date.now() - fs63.statSync(mutex).mtimeMs > TAKEOVER_STALE_MS)
|
|
92011
|
+
if (Date.now() - fs63.statSync(mutex).mtimeMs > TAKEOVER_STALE_MS) {
|
|
92012
|
+
fs63.unlinkSync(mutex);
|
|
92013
|
+
aged = true;
|
|
92014
|
+
}
|
|
91526
92015
|
} catch {
|
|
91527
92016
|
}
|
|
91528
|
-
return false;
|
|
92017
|
+
return aged && !retried ? takeOver(lock, seen, true) : false;
|
|
91529
92018
|
}
|
|
91530
92019
|
try {
|
|
91531
92020
|
try {
|
|
@@ -94598,6 +95087,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
94598
95087
|
const recordKey = clientRecordKey(activeLocation, clientKey);
|
|
94599
95088
|
let planArg = planArgIn;
|
|
94600
95089
|
let composed;
|
|
95090
|
+
let asksHash;
|
|
94601
95091
|
if (fromPreset) {
|
|
94602
95092
|
if (planArg) {
|
|
94603
95093
|
return jsonResponse({ ok: false, phase: "compose", error: "fromPreset composes the plan itself \u2014 pass brief (and optionally preset / copy), not a plan. To run a hand-authored plan, drop fromPreset." });
|
|
@@ -94655,6 +95145,12 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
94655
95145
|
if (!chosen.ok) {
|
|
94656
95146
|
return jsonResponse({ ok: false, phase: "compose", error: chosen.error });
|
|
94657
95147
|
}
|
|
95148
|
+
{
|
|
95149
|
+
const board = readBoardState();
|
|
95150
|
+
const ticks = board.bought?.[recordKey];
|
|
95151
|
+
briefForCompose = briefWithAsks(briefForCompose, board.asks?.[recordKey], Array.isArray(ticks) ? ticks : void 0, chosen.preset.skeleton?.workflows ?? []);
|
|
95152
|
+
asksHash = asksBindingHash(board.asks?.[recordKey], Array.isArray(ticks) ? ticks : void 0);
|
|
95153
|
+
}
|
|
94658
95154
|
const composition = planFromPreset({ preset: chosen.preset, brief: briefForCompose, locationId: activeLocation, copy: copyArg });
|
|
94659
95155
|
if (!composition.ok) {
|
|
94660
95156
|
return jsonResponse({
|
|
@@ -94785,7 +95281,7 @@ function registerIntakeToBuildTools(server2, client, builderClient, registry2) {
|
|
|
94785
95281
|
const { name: locationName, timeZone: locationTimeZone } = await readLocationBasics(client, activeLocation);
|
|
94786
95282
|
const { assets, warnings: scanWarnings } = await scanExistingAssets(client, activeLocation, registry2);
|
|
94787
95283
|
const result = resolvePlan(typedPlan, assets, { metHandoffs });
|
|
94788
|
-
const clientFields = { ...clientKey ? { clientKey } : {}, ...clinicPrefix ? { clinicPrefix } : {}, ...locationTimeZone ? { locationTimeZone } : {} };
|
|
95284
|
+
const clientFields = { ...clientKey ? { clientKey } : {}, ...asksHash ? { asksHash } : {}, ...clinicPrefix ? { clinicPrefix } : {}, ...locationTimeZone ? { locationTimeZone } : {} };
|
|
94789
95285
|
if (clinicPrefix && clinicScoping) {
|
|
94790
95286
|
let checked = false;
|
|
94791
95287
|
let findings = [];
|
|
@@ -95749,6 +96245,7 @@ var init_intake_to_build = __esm({
|
|
|
95749
96245
|
init_claims_guard();
|
|
95750
96246
|
init_plan_from_preset();
|
|
95751
96247
|
init_board_roster();
|
|
96248
|
+
init_client_asks();
|
|
95752
96249
|
init_features();
|
|
95753
96250
|
init_executor();
|
|
95754
96251
|
init_build_confirm();
|