@elitedcs/ghl-mcp 3.89.0-beta.2 → 3.89.0-beta.3
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 +16 -0
- package/README.md +1 -1
- package/dist/index.js +645 -149
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -25006,7 +25006,7 @@ var init_question_set = __esm({
|
|
|
25006
25006
|
field: { kind: "custom", dataType: "LARGE_TEXT" },
|
|
25007
25007
|
placeholder: "The problems they come to you with (one per line)",
|
|
25008
25008
|
briefPath: "audience.painPoints",
|
|
25009
|
-
briefKind: "
|
|
25009
|
+
briefKind: "textList"
|
|
25010
25010
|
},
|
|
25011
25011
|
{
|
|
25012
25012
|
key: "objections",
|
|
@@ -25017,7 +25017,7 @@ var init_question_set = __esm({
|
|
|
25017
25017
|
field: { kind: "custom", dataType: "LARGE_TEXT" },
|
|
25018
25018
|
placeholder: "Why prospects hesitate (one per line)",
|
|
25019
25019
|
briefPath: "audience.objections",
|
|
25020
|
-
briefKind: "
|
|
25020
|
+
briefKind: "textList"
|
|
25021
25021
|
},
|
|
25022
25022
|
// ── Section D: Goal and sales process ──
|
|
25023
25023
|
{
|
|
@@ -30420,6 +30420,9 @@ function findingsFor(outcome) {
|
|
|
30420
30420
|
const list4 = outcome.plain?.issues?.length ? outcome.plain.issues : outcome.issues;
|
|
30421
30421
|
return (list4 ?? []).map((s) => String(s).trim()).filter(Boolean);
|
|
30422
30422
|
}
|
|
30423
|
+
function openTasks(tasks2) {
|
|
30424
|
+
return tasks2.filter((t) => !(t.claimed === true && typeof t.text === "string" && SELF_REPORTED.test(t.text)));
|
|
30425
|
+
}
|
|
30423
30426
|
function mergeTasks(previous, findings, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
30424
30427
|
const before = new Map(previous.map((t) => [t.id, t]));
|
|
30425
30428
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -30429,7 +30432,9 @@ function mergeTasks(previous, findings, now = (/* @__PURE__ */ new Date()).toISO
|
|
|
30429
30432
|
if (seen.has(id)) continue;
|
|
30430
30433
|
seen.add(id);
|
|
30431
30434
|
const prior = before.get(id);
|
|
30432
|
-
if (prior?.claimed) {
|
|
30435
|
+
if (prior?.claimed && SELF_REPORTED.test(text2)) {
|
|
30436
|
+
out.push(prior);
|
|
30437
|
+
} else if (prior?.claimed) {
|
|
30433
30438
|
out.push({ id, text: text2, reappeared: true });
|
|
30434
30439
|
} else {
|
|
30435
30440
|
out.push({ id, text: text2, ...prior?.reappeared ? { reappeared: true } : {} });
|
|
@@ -30447,7 +30452,7 @@ function manualProgress(tasks2, verifyHasRun) {
|
|
|
30447
30452
|
const total = tasks2.length;
|
|
30448
30453
|
const claimed = tasks2.filter((t) => t.claimed).length;
|
|
30449
30454
|
const reappeared = tasks2.filter((t) => t.reappeared).length;
|
|
30450
|
-
const clear = verifyHasRun &&
|
|
30455
|
+
const clear = verifyHasRun && openTasks(tasks2).length === 0;
|
|
30451
30456
|
let line3;
|
|
30452
30457
|
if (!verifyHasRun) {
|
|
30453
30458
|
line3 = "Run the check first. It writes this list for you.";
|
|
@@ -30481,9 +30486,11 @@ function manualSignOffRefusal(tasks2, verifyHasRun) {
|
|
|
30481
30486
|
}
|
|
30482
30487
|
return "Everything is ticked, but the check has not confirmed it yet. Run the check again and this will clear on its own.";
|
|
30483
30488
|
}
|
|
30489
|
+
var SELF_REPORTED;
|
|
30484
30490
|
var init_manual_tasks = __esm({
|
|
30485
30491
|
"src/manual-tasks.ts"() {
|
|
30486
30492
|
"use strict";
|
|
30493
|
+
SELF_REPORTED = /^[^:]+: Bought, but not built automatically yet\./;
|
|
30487
30494
|
}
|
|
30488
30495
|
});
|
|
30489
30496
|
|
|
@@ -30665,7 +30672,7 @@ function deriveQueue(input) {
|
|
|
30665
30672
|
if (stages.every((s) => s === "done")) {
|
|
30666
30673
|
const proof2 = lastProof(state, locationId2, QUEUE_STEPS.length);
|
|
30667
30674
|
const claim = liveClaim(true, state.goLive?.[locationId2], state.outcomes?.[`${locationId2}:3`]?.at);
|
|
30668
|
-
const signedLive = claim.live && state.outcomes?.[`${locationId2}:4`]?.ok === true && !state.outcomes?.[`${locationId2}:4`]?.issues?.length && !state.manualTasks?.[locationId2]
|
|
30675
|
+
const signedLive = claim.live && state.outcomes?.[`${locationId2}:4`]?.ok === true && !state.outcomes?.[`${locationId2}:4`]?.issues?.length && !openTasks(state.manualTasks?.[locationId2] ?? []).length && !state.staleClients?.includes(locationId2);
|
|
30669
30676
|
finished.push({
|
|
30670
30677
|
locationId: locationId2,
|
|
30671
30678
|
clientName: name,
|
|
@@ -30799,6 +30806,7 @@ var init_work_queue = __esm({
|
|
|
30799
30806
|
"use strict";
|
|
30800
30807
|
init_stage_runner();
|
|
30801
30808
|
init_plain_outcome();
|
|
30809
|
+
init_manual_tasks();
|
|
30802
30810
|
init_go_live();
|
|
30803
30811
|
QUEUE_STEPS = [
|
|
30804
30812
|
{ label: "Intake", todo: "Set up the intake questions for {client}", why: "Command OS puts the questionnaire into their account." },
|
|
@@ -32494,7 +32502,7 @@ var require_package = __commonJS({
|
|
|
32494
32502
|
"package.json"(exports2, module2) {
|
|
32495
32503
|
module2.exports = {
|
|
32496
32504
|
name: "@elitedcs/ghl-mcp",
|
|
32497
|
-
version: "3.89.0-beta.
|
|
32505
|
+
version: "3.89.0-beta.3",
|
|
32498
32506
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
32499
32507
|
description: "GoHighLevel MCP Server for Claude. 250 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
|
|
32500
32508
|
main: "dist/index.js",
|
|
@@ -38538,6 +38546,13 @@ function saveBuildObligations(id, items) {
|
|
|
38538
38546
|
fs30.writeFileSync(file2 + ".tmp", JSON.stringify([...merged.values()], null, 2), { mode: 384 });
|
|
38539
38547
|
fs30.renameSync(file2 + ".tmp", file2);
|
|
38540
38548
|
}
|
|
38549
|
+
function replaceBuildObligationsOfType(id, type, items) {
|
|
38550
|
+
const kept = readBuildObligations(id).filter((m) => m.type !== type);
|
|
38551
|
+
const file2 = fileFor(id);
|
|
38552
|
+
fs30.mkdirSync(path28.dirname(file2), { recursive: true });
|
|
38553
|
+
fs30.writeFileSync(file2 + ".tmp", JSON.stringify([...kept, ...items.map((i) => ({ ...i, type }))], null, 2), { mode: 384 });
|
|
38554
|
+
fs30.renameSync(file2 + ".tmp", file2);
|
|
38555
|
+
}
|
|
38541
38556
|
function planObjectRefs(plan) {
|
|
38542
38557
|
const refs2 = /* @__PURE__ */ new Set();
|
|
38543
38558
|
const visit = (value) => {
|
|
@@ -38556,7 +38571,7 @@ function planObjectRefs(plan) {
|
|
|
38556
38571
|
function reconcileBuildObligations(id, checks, currentPlan) {
|
|
38557
38572
|
const refs2 = currentPlan && typeof currentPlan === "object" ? planObjectRefs(currentPlan) : void 0;
|
|
38558
38573
|
const workflows = currentPlan?.workflows;
|
|
38559
|
-
const stillPlanned = (item) => !refs2 || refs2.has(item.ref) || // The producer keys each manual job by workflow name and local job index.
|
|
38574
|
+
const stillPlanned = (item) => !refs2 || refs2.has(item.ref) || NOT_FROM_PLAN.has(item.type) || // The producer keys each manual job by workflow name and local job index.
|
|
38560
38575
|
// Its index can change or disappear from a later preview without completion.
|
|
38561
38576
|
item.type === "workflow-manual" && Array.isArray(workflows) && workflows.some((workflow) => item.ref.startsWith(`${workflow.name}:`) && /^\d+$/.test(item.ref.slice(workflow.name.length + 1)));
|
|
38562
38577
|
const remaining = readBuildObligations(id).filter((item) => stillPlanned(item) && !checks.some((c) => c.ref === item.ref && c.type === item.type && c.ok === true && typeof c.evidence === "string" && c.evidence.trim()));
|
|
@@ -38565,13 +38580,14 @@ function reconcileBuildObligations(id, checks, currentPlan) {
|
|
|
38565
38580
|
fs30.writeFileSync(file2 + ".tmp", JSON.stringify(remaining, null, 2), { mode: 384 });
|
|
38566
38581
|
fs30.renameSync(file2 + ".tmp", file2);
|
|
38567
38582
|
}
|
|
38568
|
-
var fs30, path28;
|
|
38583
|
+
var fs30, path28, NOT_FROM_PLAN;
|
|
38569
38584
|
var init_build_obligations = __esm({
|
|
38570
38585
|
"src/build-obligations.ts"() {
|
|
38571
38586
|
"use strict";
|
|
38572
38587
|
fs30 = __toESM(require("node:fs"));
|
|
38573
38588
|
path28 = __toESM(require("node:path"));
|
|
38574
38589
|
init_credentials_store();
|
|
38590
|
+
NOT_FROM_PLAN = /* @__PURE__ */ new Set(["bought-not-built"]);
|
|
38575
38591
|
}
|
|
38576
38592
|
});
|
|
38577
38593
|
|
|
@@ -39674,8 +39690,12 @@ function firstString(v) {
|
|
|
39674
39690
|
}
|
|
39675
39691
|
return void 0;
|
|
39676
39692
|
}
|
|
39677
|
-
function splitList(text2) {
|
|
39678
|
-
|
|
39693
|
+
function splitList(text2, opts = {}) {
|
|
39694
|
+
let lines = text2.split(/\r?\n/).filter((l) => l.trim());
|
|
39695
|
+
if (lines.length > 1 && /:\s*$/.test(lines[0])) lines = lines.slice(1);
|
|
39696
|
+
const oneLine = (line3) => line3.split(/;|•/).flatMap((part) => opts.commas !== false && part.includes(",") && !/\d,\d/.test(part) ? part.split(",") : [part]);
|
|
39697
|
+
const items = lines.length > 1 ? lines : lines.flatMap(oneLine);
|
|
39698
|
+
return items.map((s) => s.replace(/^[\s\-*•\d.)]+/, "").replace(/[\s,;]+$/, "").trim()).filter(Boolean);
|
|
39679
39699
|
}
|
|
39680
39700
|
function parsePricePoints(text2) {
|
|
39681
39701
|
const out = [];
|
|
@@ -39688,10 +39708,13 @@ function parsePricePoints(text2) {
|
|
|
39688
39708
|
const t = segment.trim().replace(/\.$/, "");
|
|
39689
39709
|
if (!t) continue;
|
|
39690
39710
|
const m = t.match(ENTRY);
|
|
39711
|
+
const withTerms = m ? null : t.match(/^(.*?)\s*[-–—:|]?\s*\$\s*([\d,]+(?:\.\d+)?)\s*(.+)$/);
|
|
39691
39712
|
if (m && m[2]) {
|
|
39692
39713
|
const name = clean3(m[1]);
|
|
39693
39714
|
const price = (m[3] ? `${num3(m[2])}-${num3(m[3])}` : num3(m[2])) + recur(m[4]);
|
|
39694
39715
|
out.push({ name: name || t, price });
|
|
39716
|
+
} else if (withTerms && clean3(withTerms[1])) {
|
|
39717
|
+
out.push({ name: clean3(withTerms[1]), price: num3(withTerms[2]) + (withTerms[3].trim().startsWith("/") ? "" : " ") + withTerms[3].trim() });
|
|
39695
39718
|
} else {
|
|
39696
39719
|
out.push({ name: t, price: "" });
|
|
39697
39720
|
}
|
|
@@ -39707,7 +39730,16 @@ function cleanLines(text2) {
|
|
|
39707
39730
|
function parseStaffLines(text2) {
|
|
39708
39731
|
const staff = [];
|
|
39709
39732
|
const warnings = [];
|
|
39710
|
-
|
|
39733
|
+
const emailsIn = (t) => splitFields(t).filter((f) => EMAIL_RE2.test(f.replace(/^e-?mail\s*[:=]\s*/i, ""))).length;
|
|
39734
|
+
const lines = cleanLines(text2).flatMap((line3) => {
|
|
39735
|
+
if (!line3.includes(";") || emailsIn(line3) < 2) return [line3];
|
|
39736
|
+
const pieces = line3.split(";").map((p) => p.trim()).filter(Boolean);
|
|
39737
|
+
return pieces.every((p) => emailsIn(p) === 1) ? pieces : [line3];
|
|
39738
|
+
});
|
|
39739
|
+
lines.forEach((line3, i) => {
|
|
39740
|
+
if (emailsIn(line3) > 1) {
|
|
39741
|
+
warnings.push(`Staff line ${i + 1} ("${line3}") lists more than one email address, so only the first person was read. Put one person per line.`);
|
|
39742
|
+
}
|
|
39711
39743
|
const fields = splitFields(line3);
|
|
39712
39744
|
let email;
|
|
39713
39745
|
let mobile;
|
|
@@ -39845,12 +39877,29 @@ function cleanPersonCandidate(s) {
|
|
|
39845
39877
|
if (durationMinutesFromWords(t) !== void 0) return void 0;
|
|
39846
39878
|
return t;
|
|
39847
39879
|
}
|
|
39848
|
-
function parseCalendarEntry(entry, index, calendars, warnings, knownStaffNames) {
|
|
39849
|
-
|
|
39880
|
+
function parseCalendarEntry(entry, index, calendars, warnings, knownStaffNames, sharedPeople = []) {
|
|
39881
|
+
let bracketMinutes;
|
|
39882
|
+
const bracketNotes = [];
|
|
39883
|
+
const bracketPeople = [];
|
|
39884
|
+
const staffIn = (part) => knownStaffNames.length ? matchesStaffName(part.replace(/^(?:with|by)\s+/i, ""), knownStaffNames) : void 0;
|
|
39885
|
+
const unbracketed = entry.replace(/\s*\(([^()]*)\)/g, (whole, inner) => {
|
|
39886
|
+
const hasLength = countDurations(inner) > 0;
|
|
39887
|
+
if (!hasLength && !/[,$;]/.test(inner) && !staffIn(inner)) return whole;
|
|
39888
|
+
const parts = inner.split(/[,;]/).map((x) => x.trim()).filter(Boolean);
|
|
39889
|
+
for (const part of parts) {
|
|
39890
|
+
const d = durationInProse(part);
|
|
39891
|
+
const person = staffIn(part);
|
|
39892
|
+
if (d && bracketMinutes === void 0 && !d.rest.replace(/[\s.,;:-]+/g, "")) bracketMinutes = d.minutes;
|
|
39893
|
+
else if (person) bracketPeople.push(person);
|
|
39894
|
+
else bracketNotes.push(part);
|
|
39895
|
+
}
|
|
39896
|
+
return " ";
|
|
39897
|
+
}).replace(/\s{2,}/g, " ").trim();
|
|
39898
|
+
const fields = splitFields(unbracketed);
|
|
39850
39899
|
let nameField = fields.shift() ?? "";
|
|
39851
39900
|
let type;
|
|
39852
|
-
let durationMinutes;
|
|
39853
|
-
const candidates = [];
|
|
39901
|
+
let durationMinutes = bracketMinutes;
|
|
39902
|
+
const candidates = [...sharedPeople, ...bracketPeople];
|
|
39854
39903
|
let proseName = false;
|
|
39855
39904
|
const inName = durationInProse(nameField);
|
|
39856
39905
|
if (inName) {
|
|
@@ -39911,6 +39960,9 @@ function parseCalendarEntry(entry, index, calendars, warnings, knownStaffNames)
|
|
|
39911
39960
|
warnings.push(`Calendar line ${index + 1} ("${entry}") was skipped: no calendar name found.`);
|
|
39912
39961
|
return;
|
|
39913
39962
|
}
|
|
39963
|
+
if (bracketNotes.length) {
|
|
39964
|
+
warnings.push(`Calendar "${name}": Review the details "${bracketNotes.join(", ")}". They were kept as a note, not as calendar settings.`);
|
|
39965
|
+
}
|
|
39914
39966
|
const people = [];
|
|
39915
39967
|
for (const c of candidates) {
|
|
39916
39968
|
const cleaned = cleanPersonCandidate(c);
|
|
@@ -39953,10 +40005,18 @@ function parseCalendarLines(text2, knownStaffNames = []) {
|
|
|
39953
40005
|
const calendars = [];
|
|
39954
40006
|
const warnings = [];
|
|
39955
40007
|
const entries = [];
|
|
40008
|
+
const sharedPeople = [];
|
|
40009
|
+
const SHARED = /(?:^|[.;]\s*)(?:both|all|each)(?:\s+(?:of\s+them|calendars?|are|is))*\s+(?:with|by|for)\s+([^.;\n]+)[.;]?\s*$/i;
|
|
39956
40010
|
for (const rawLine of text2.split(/\r?\n/)) {
|
|
39957
40011
|
let line3 = rawLine.replace(/^[\s\-*•]+/, "").trim();
|
|
39958
40012
|
if (!line3) continue;
|
|
39959
|
-
|
|
40013
|
+
const shared = SHARED.exec(line3);
|
|
40014
|
+
if (shared) {
|
|
40015
|
+
sharedPeople.push(...splitNames(shared[1]));
|
|
40016
|
+
line3 = line3.slice(0, shared.index).trim();
|
|
40017
|
+
if (!line3) continue;
|
|
40018
|
+
}
|
|
40019
|
+
line3 = line3.replace(LEADING_COUNT_WITH_NOUN, "").replace(LEADING_COUNT_BEFORE_MARKER, "").replace(LEADING_BARE_COUNT, "");
|
|
39960
40020
|
for (const marked3 of splitOnMarkers(line3)) {
|
|
39961
40021
|
for (const clause of splitOnAnd(marked3)) {
|
|
39962
40022
|
const cleaned = clause.replace(/^\(?\d{1,2}[).]\s+/, "").replace(/^[\s\-*•]+/, "").trim();
|
|
@@ -39964,7 +40024,7 @@ function parseCalendarLines(text2, knownStaffNames = []) {
|
|
|
39964
40024
|
}
|
|
39965
40025
|
}
|
|
39966
40026
|
}
|
|
39967
|
-
entries.forEach((entry, i) => parseCalendarEntry(entry, i, calendars, warnings, knownStaffNames));
|
|
40027
|
+
entries.forEach((entry, i) => parseCalendarEntry(entry, i, calendars, warnings, knownStaffNames, sharedPeople));
|
|
39968
40028
|
return { calendars, warnings };
|
|
39969
40029
|
}
|
|
39970
40030
|
function coerceTriState(v) {
|
|
@@ -39992,6 +40052,10 @@ function coerceValue(raw, kind, warnings) {
|
|
|
39992
40052
|
const s = firstString(raw);
|
|
39993
40053
|
return s ? splitList(s) : void 0;
|
|
39994
40054
|
}
|
|
40055
|
+
case "textList": {
|
|
40056
|
+
const s = firstString(raw);
|
|
40057
|
+
return s ? splitList(s, { commas: false }) : void 0;
|
|
40058
|
+
}
|
|
39995
40059
|
case "pricePoints": {
|
|
39996
40060
|
const s = firstString(raw);
|
|
39997
40061
|
return s ? parsePricePoints(s) : void 0;
|
|
@@ -40179,7 +40243,7 @@ function normalizeSubmissionToBrief(opts) {
|
|
|
40179
40243
|
brief.flags = deriveFlags(brief);
|
|
40180
40244
|
return brief;
|
|
40181
40245
|
}
|
|
40182
|
-
var EMAIL_RE2, PHONE_RE, CALENDAR_TYPE_WORDS, DURATION_IN_PROSE, COUNT_WORD, LEADING_COUNT_BEFORE_MARKER, LEADING_COUNT_WITH_NOUN, ASSIGNMENT_SLOTS;
|
|
40246
|
+
var EMAIL_RE2, PHONE_RE, CALENDAR_TYPE_WORDS, DURATION_IN_PROSE, COUNT_WORD, LEADING_COUNT_BEFORE_MARKER, LEADING_COUNT_WITH_NOUN, LEADING_BARE_COUNT, ASSIGNMENT_SLOTS;
|
|
40183
40247
|
var init_normalizer = __esm({
|
|
40184
40248
|
"src/intake-to-build/normalizer.ts"() {
|
|
40185
40249
|
"use strict";
|
|
@@ -40198,6 +40262,7 @@ var init_normalizer = __esm({
|
|
|
40198
40262
|
`^\\s*(?:(?:we|i)(?:'d| would| will)?\\s*(?:need|want|like|use)\\s+)?${COUNT_WORD}\\s+(?:booking\\s+)?calendars?\\s*[.:;,\u2013\u2014-]+\\s*`,
|
|
40199
40263
|
"i"
|
|
40200
40264
|
);
|
|
40265
|
+
LEADING_BARE_COUNT = new RegExp(`^\\s*${COUNT_WORD}\\s*:\\s+`, "i");
|
|
40201
40266
|
ASSIGNMENT_SLOTS = [
|
|
40202
40267
|
{ key: "notifyName", label: "who should be notified about new leads" },
|
|
40203
40268
|
{ key: "callsName", label: "who takes booking and follow-up calls" }
|
|
@@ -41884,6 +41949,193 @@ var init_attachment = __esm({
|
|
|
41884
41949
|
}
|
|
41885
41950
|
});
|
|
41886
41951
|
|
|
41952
|
+
// src/bought-list.ts
|
|
41953
|
+
function isBoughtId(x) {
|
|
41954
|
+
return typeof x === "string" && BOUGHT_IDS.includes(x);
|
|
41955
|
+
}
|
|
41956
|
+
function boughtItemsFor(offered) {
|
|
41957
|
+
return ASSESSMENT_FEATURES.map((f) => {
|
|
41958
|
+
const id = f.id;
|
|
41959
|
+
const base = { id, name: f.name, means: BOUGHT_MEANS[id] };
|
|
41960
|
+
const feature = FEATURE_FOR_BOUGHT[id];
|
|
41961
|
+
if (feature) {
|
|
41962
|
+
const next = id === "missed" ? { next: CALL_ANSWERING_NEXT } : {};
|
|
41963
|
+
if (!offered.includes(feature)) {
|
|
41964
|
+
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.` };
|
|
41965
|
+
}
|
|
41966
|
+
return { ...base, kind: "built", feature, ...next, delivery: BUILT_LABEL[id] };
|
|
41967
|
+
}
|
|
41968
|
+
const other = OTHER_DELIVERY[id];
|
|
41969
|
+
return { ...base, kind: other.kind, delivery: other.text };
|
|
41970
|
+
});
|
|
41971
|
+
}
|
|
41972
|
+
function featuresFromBought(ticks, offered) {
|
|
41973
|
+
return FEATURE_IDS.filter((f) => ticks.includes(BUILD_ASSESSMENT_FEATURE[f]) && offered.includes(f));
|
|
41974
|
+
}
|
|
41975
|
+
function scopeFromBought(ticks, offered, pkg2) {
|
|
41976
|
+
const features = featuresFromBought(ticks, offered);
|
|
41977
|
+
if (!pkg2 && offered.length && offered.every((f) => features.includes(f))) return { wholePreset: true };
|
|
41978
|
+
return { ...pkg2 ? { package: pkg2 } : {}, features };
|
|
41979
|
+
}
|
|
41980
|
+
function isAssessmentChoice(x) {
|
|
41981
|
+
return typeof x === "string" && ASSESSMENT_CHOICES.includes(x);
|
|
41982
|
+
}
|
|
41983
|
+
function ticksFor(state, rk) {
|
|
41984
|
+
const saved = state.bought?.[rk];
|
|
41985
|
+
if (Array.isArray(saved)) return { ticks: BOUGHT_IDS.filter((id) => saved.includes(id)), source: "saved" };
|
|
41986
|
+
const sc = state.scope?.[rk];
|
|
41987
|
+
if (sc && !sc.wholePreset && Array.isArray(sc.features)) {
|
|
41988
|
+
const chosen = sc.features.filter(isFeatureId).map((f) => BUILD_ASSESSMENT_FEATURE[f]);
|
|
41989
|
+
return { ticks: BOUGHT_IDS.filter((id) => !FEATURE_FOR_BOUGHT[id] || chosen.includes(id)), source: "scope" };
|
|
41990
|
+
}
|
|
41991
|
+
if (sc) return { ticks: [...BOUGHT_IDS], source: "scope" };
|
|
41992
|
+
const purchases = state.assessmentPurchases?.[rk];
|
|
41993
|
+
if (Array.isArray(purchases)) return { ticks: BOUGHT_IDS.filter((id) => purchases.includes(id)), source: "assessment" };
|
|
41994
|
+
return { ticks: [...BOUGHT_IDS], source: "all" };
|
|
41995
|
+
}
|
|
41996
|
+
function assessmentAnswerOf(state, rk) {
|
|
41997
|
+
if (state.assessmentAttachments?.[rk]) return "attached";
|
|
41998
|
+
const c = state.assessmentChoice?.[rk]?.choice;
|
|
41999
|
+
return isAssessmentChoice(c) ? c : null;
|
|
42000
|
+
}
|
|
42001
|
+
function secondClinicAssessmentMessage(clientKey, name) {
|
|
42002
|
+
return `Nobody has answered the Assessment question for ${name ? name : `the second clinic "${clientKey}"`}, so nothing was approved or built. For a second clinic in a sub-account the only answer available for now is Build without an Assessment. Send the cockpit POST /api/assessment-choice with this sub-account's locationId, clientKey "${clientKey}" and choice "without". Then approve and build again.`;
|
|
42003
|
+
}
|
|
42004
|
+
function assessmentGateRefusal(state, rk, clientKey, name) {
|
|
42005
|
+
const answer2 = assessmentAnswerOf(state, rk);
|
|
42006
|
+
if (state.assessmentAttachments?.[rk] || answer2 === "without") return null;
|
|
42007
|
+
if (clientKey) return secondClinicAssessmentMessage(clientKey, name);
|
|
42008
|
+
if (answer2 === "attached") return ATTACH_THE_ASSESSMENT;
|
|
42009
|
+
if (answer2 === "run-first") return RUN_THE_ASSESSMENT_FIRST;
|
|
42010
|
+
return ANSWER_THE_ASSESSMENT_QUESTION;
|
|
42011
|
+
}
|
|
42012
|
+
function approvalBindingHash(scopeHash, state, rk) {
|
|
42013
|
+
const saved = state.bought?.[rk];
|
|
42014
|
+
const answer2 = assessmentAnswerOf(state, rk);
|
|
42015
|
+
if (!Array.isArray(saved) && !answer2) return scopeHash;
|
|
42016
|
+
const snapshot = state.assessmentAttachments?.[rk];
|
|
42017
|
+
return contentHash({ scope: scopeHash, bought: Array.isArray(saved) ? [...new Set(saved)].sort() : null, assessment: answer2, attached: snapshot ? contentHash(snapshot) : null });
|
|
42018
|
+
}
|
|
42019
|
+
function boughtBindingHash(scopeHash, state, rk) {
|
|
42020
|
+
const saved = state.bought?.[rk];
|
|
42021
|
+
return contentHash({ scope: scopeHash, bought: Array.isArray(saved) ? [...new Set(saved)].sort() : null });
|
|
42022
|
+
}
|
|
42023
|
+
function boughtJobs(ticks, offered) {
|
|
42024
|
+
return boughtItemsFor(offered).filter((item) => ticks.includes(item.id) && (item.kind === "go-live" || item.kind === "separate" || item.kind === "not-in-plan")).map((item) => ({
|
|
42025
|
+
ref: `bought:${item.id}`,
|
|
42026
|
+
type: BOUGHT_JOB_TYPE,
|
|
42027
|
+
name: item.name,
|
|
42028
|
+
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}`
|
|
42029
|
+
}));
|
|
42030
|
+
}
|
|
42031
|
+
function boughtJobLines(ticks, offered) {
|
|
42032
|
+
return boughtJobs(ticks, offered).map((j) => `${j.name}: ${j.reason}`);
|
|
42033
|
+
}
|
|
42034
|
+
function claudeSuggestions(ticks) {
|
|
42035
|
+
const out = [];
|
|
42036
|
+
if (ticks.includes("missed")) out.push("Call answering: next step with Claude, an AI voice agent to answer live.");
|
|
42037
|
+
if (ticks.includes("reporting")) out.push("Reporting: from Claude, a weekly client report. No account setup.");
|
|
42038
|
+
return out;
|
|
42039
|
+
}
|
|
42040
|
+
function assessmentChoiceLine(answer2, attached = answer2 === "attached") {
|
|
42041
|
+
if (attached) return "The Assessment was run and is attached.";
|
|
42042
|
+
if (answer2 === "attached") return "The Assessment was run. It is not attached yet.";
|
|
42043
|
+
if (answer2 === "without") return "No Assessment was run for this client.";
|
|
42044
|
+
if (answer2 === "run-first") return "The Assessment will be run first.";
|
|
42045
|
+
return "Not answered yet: has the Assessment been run for this client?";
|
|
42046
|
+
}
|
|
42047
|
+
function boughtViewFor(state, rk, offered) {
|
|
42048
|
+
const { ticks, source } = ticksFor(state, rk);
|
|
42049
|
+
return {
|
|
42050
|
+
items: boughtItemsFor(offered),
|
|
42051
|
+
ticks,
|
|
42052
|
+
source,
|
|
42053
|
+
answer: assessmentAnswerOf(state, rk),
|
|
42054
|
+
attached: Boolean(state.assessmentAttachments?.[rk]),
|
|
42055
|
+
ready: assessmentGateRefusal(state, rk) === null,
|
|
42056
|
+
suggestions: source === "saved" ? claudeSuggestions(ticks) : []
|
|
42057
|
+
};
|
|
42058
|
+
}
|
|
42059
|
+
function boughtDocumentFor(state, rk, offered) {
|
|
42060
|
+
const { ticks, source } = ticksFor(state, rk);
|
|
42061
|
+
const items = boughtItemsFor(offered);
|
|
42062
|
+
const confirmed = source === "saved" || source === "scope";
|
|
42063
|
+
const ticked = items.filter((i) => ticks.includes(i.id));
|
|
42064
|
+
return {
|
|
42065
|
+
confirmed,
|
|
42066
|
+
assessment: assessmentChoiceLine(assessmentAnswerOf(state, rk), Boolean(state.assessmentAttachments?.[rk])),
|
|
42067
|
+
bought: ticked.map((i) => ({ name: i.name, means: i.means, delivery: clientDelivery(i) })),
|
|
42068
|
+
// The client reads this: never the agency's job text, and never "with Claude" (review of ea3526f).
|
|
42069
|
+
stillToDo: ticked.flatMap((i) => {
|
|
42070
|
+
if (i.kind === "go-live") return [`${i.name}: being set up in your account.`];
|
|
42071
|
+
if (i.kind === "separate") return [`${i.name}: the form is ready. The pages are a separate piece of work.`];
|
|
42072
|
+
if (i.kind === "not-in-plan") return [`${i.name}: being set up separately from this build.`];
|
|
42073
|
+
if (i.kind === "claude") return [`${i.name}: a weekly report on what is working, sent to you.`];
|
|
42074
|
+
return [];
|
|
42075
|
+
}).concat(ticks.includes("missed") ? ["Call answering: live call answering by an AI voice agent is a possible next step."] : [])
|
|
42076
|
+
};
|
|
42077
|
+
}
|
|
42078
|
+
function clientDelivery(i) {
|
|
42079
|
+
if (i.kind === "go-live") return "Being set up in your account";
|
|
42080
|
+
if (i.kind === "separate") return "The form is built; the pages are separate work";
|
|
42081
|
+
if (i.kind === "not-in-plan") return "Not part of this build";
|
|
42082
|
+
if (i.kind === "claude") return "A weekly report on what is working";
|
|
42083
|
+
return i.delivery;
|
|
42084
|
+
}
|
|
42085
|
+
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;
|
|
42086
|
+
var init_bought_list = __esm({
|
|
42087
|
+
"src/bought-list.ts"() {
|
|
42088
|
+
"use strict";
|
|
42089
|
+
init_attachment();
|
|
42090
|
+
init_features();
|
|
42091
|
+
init_build_contract();
|
|
42092
|
+
BOUGHT_IDS = ASSESSMENT_FEATURES.map((f) => f.id);
|
|
42093
|
+
BOUGHT_MEANS = {
|
|
42094
|
+
missed: "No call goes unanswered",
|
|
42095
|
+
waited: "Every new lead hears back in seconds",
|
|
42096
|
+
quotes: "No quote goes cold",
|
|
42097
|
+
noshow: "Bookings confirmed, reminded and rescued",
|
|
42098
|
+
reactivation: "Old leads and past clients brought back",
|
|
42099
|
+
nurture: "Leads kept warm until they are ready",
|
|
42100
|
+
recall: "Clients come back on schedule",
|
|
42101
|
+
deposits: "Fewer no-shows, cash up front",
|
|
42102
|
+
reviews: "More five-star reviews",
|
|
42103
|
+
referrals: "Happy clients send friends",
|
|
42104
|
+
lead_magnet: "A reason to give you their details",
|
|
42105
|
+
sms: "Texts that actually get delivered",
|
|
42106
|
+
pipeline: "Every lead tracked to a sale",
|
|
42107
|
+
reporting: "The owner sees what is working"
|
|
42108
|
+
};
|
|
42109
|
+
FEATURE_FOR_BOUGHT = Object.fromEntries(
|
|
42110
|
+
FEATURE_IDS.map((f) => [BUILD_ASSESSMENT_FEATURE[f], f])
|
|
42111
|
+
);
|
|
42112
|
+
BUILT_LABEL = {
|
|
42113
|
+
missed: "Built: missed-call text-back",
|
|
42114
|
+
waited: "Built: Speed to Lead",
|
|
42115
|
+
quotes: "Built: Quote follow-up",
|
|
42116
|
+
noshow: "Built: booking, reminders and no-show rescue",
|
|
42117
|
+
reactivation: "Built: Win-back",
|
|
42118
|
+
nurture: "Built: 30-day lead nurture",
|
|
42119
|
+
reviews: "Built: Review requests"
|
|
42120
|
+
};
|
|
42121
|
+
OTHER_DELIVERY = {
|
|
42122
|
+
pipeline: { kind: "always", text: "Always built: Sales pipeline" },
|
|
42123
|
+
recall: { kind: "go-live", text: "Go-live job for now: Rebooking and recall", job: "Set up rebooking and recall in the account." },
|
|
42124
|
+
referrals: { kind: "go-live", text: "Go-live job for now: Referral requests", job: "Set up referral requests in the account." },
|
|
42125
|
+
deposits: { kind: "go-live", text: "Go-live job: Deposits and prepayment (payment setup is done in the account)", job: "Payment setup is done in the account." },
|
|
42126
|
+
sms: { kind: "go-live", text: "Go-live job: SMS capability (phone number and text registration)", job: "Get the phone number and finish text registration." },
|
|
42127
|
+
lead_magnet: { kind: "separate", text: "Separate job: Lead magnet (the form is built; pages are separate work)", job: "The form is built. The pages are separate work." },
|
|
42128
|
+
reporting: { kind: "claude", text: "From Claude: Reporting (weekly client report, no account setup)" }
|
|
42129
|
+
};
|
|
42130
|
+
CALL_ANSWERING_NEXT = "Next step with Claude: an AI voice agent to answer live";
|
|
42131
|
+
ASSESSMENT_CHOICES = ["attached", "run-first", "without"];
|
|
42132
|
+
ANSWER_THE_ASSESSMENT_QUESTION = "Answer the Assessment question before approving: Blueprint Review, then Have you run the Assessment for this client.";
|
|
42133
|
+
ATTACH_THE_ASSESSMENT = "Attach the Assessment before approving, or choose Build without an Assessment.";
|
|
42134
|
+
RUN_THE_ASSESSMENT_FIRST = "You chose to run the Assessment first. Attach it when it is done, or choose Build without an Assessment.";
|
|
42135
|
+
BOUGHT_JOB_TYPE = "bought-not-built";
|
|
42136
|
+
}
|
|
42137
|
+
});
|
|
42138
|
+
|
|
41887
42139
|
// src/intake-contact.ts
|
|
41888
42140
|
async function pickIntakeContact(client, locationId2, fieldIdToLabel, knownContactId, formName) {
|
|
41889
42141
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -42282,6 +42534,21 @@ function prefillFormUrl(formUrl, queryKeys, answers, contact = {}, altQueryKeys
|
|
|
42282
42534
|
const qs = p.toString();
|
|
42283
42535
|
return { url: qs ? `${formUrl}?${qs}` : formUrl, leftovers };
|
|
42284
42536
|
}
|
|
42537
|
+
function isAuthFailure(error) {
|
|
42538
|
+
const text2 = error instanceof Error ? error.message : String(error ?? "");
|
|
42539
|
+
return /GHL API Error (?:401|403)\b/.test(text2);
|
|
42540
|
+
}
|
|
42541
|
+
function isRequestFailure(error) {
|
|
42542
|
+
if (isGHLRequestError(error)) return true;
|
|
42543
|
+
const text2 = error instanceof Error ? error.message : String(error ?? "");
|
|
42544
|
+
return /GHL API Error \d{3}|Request timeout|timed? ?out|fetch failed|ECONN|ETIMEDOUT|socket hang up|network/i.test(text2);
|
|
42545
|
+
}
|
|
42546
|
+
function unavailable(empty, reason3) {
|
|
42547
|
+
return { ...empty, status: "unavailable", unavailableReason: reason3 };
|
|
42548
|
+
}
|
|
42549
|
+
function unavailableFrom(empty, error) {
|
|
42550
|
+
return unavailable(empty, isAuthFailure(error) ? "auth" : isRequestFailure(error) ? "transient" : "error");
|
|
42551
|
+
}
|
|
42285
42552
|
async function readIntakeSnapshot(client, locationId2, sentMarker) {
|
|
42286
42553
|
const empty = { status: "not_installed", fields: {}, queryKeys: {}, submissions: 0 };
|
|
42287
42554
|
try {
|
|
@@ -42333,16 +42600,16 @@ async function readIntakeSnapshot(client, locationId2, sentMarker) {
|
|
|
42333
42600
|
contactId = hit2.id;
|
|
42334
42601
|
channel = isFormSubmission(hit2) ? "form" : "cockpit";
|
|
42335
42602
|
}
|
|
42336
|
-
} catch {
|
|
42337
|
-
return
|
|
42603
|
+
} catch (e) {
|
|
42604
|
+
return unavailableFrom(empty, e);
|
|
42338
42605
|
}
|
|
42339
42606
|
}
|
|
42340
42607
|
const hit = await pickIntakeContact(client, locationId2, Object.fromEntries(Object.entries(fields).map(([label2, id]) => [id, label2])), contactId, INTAKE_FORM_NAME2);
|
|
42341
|
-
if (submissions && !hit) return
|
|
42608
|
+
if (submissions && !hit) return unavailable(empty, "missing");
|
|
42342
42609
|
const status = submissions > 0 ? "received" : sentMarker ? "sent" : "installed";
|
|
42343
42610
|
return { status, answers: hit?.answers ?? {}, formId: form.id, formUrl: publicFormUrl(form.id), fields, queryKeys, altQueryKeys, multiFields, submissions, answeredAt, channel, contactId };
|
|
42344
|
-
} catch {
|
|
42345
|
-
return
|
|
42611
|
+
} catch (e) {
|
|
42612
|
+
return unavailableFrom(empty, e);
|
|
42346
42613
|
}
|
|
42347
42614
|
}
|
|
42348
42615
|
function recorderAllowPatterns(mcpListOutput) {
|
|
@@ -42472,6 +42739,7 @@ var init_client_intake = __esm({
|
|
|
42472
42739
|
"src/client-intake.ts"() {
|
|
42473
42740
|
"use strict";
|
|
42474
42741
|
init_intake_contact();
|
|
42742
|
+
init_ghl_client();
|
|
42475
42743
|
init_question_set();
|
|
42476
42744
|
init_form_template();
|
|
42477
42745
|
init_transcript();
|
|
@@ -43338,23 +43606,17 @@ async function assessmentRequest(id,action,extra={}){
|
|
|
43338
43606
|
const response=await fetch('/api/client-assessment',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({clientLocationId:id,action,...extra})});
|
|
43339
43607
|
const result=await response.json();if(!response.ok||!result.ok)throw new Error(result.error||'The assessment action did not finish.');
|
|
43340
43608
|
if(action==='list'){p.list=result.assessments;p.listed=true;p.selected='';if(!p.list.length)p.notice='There are no assessments in this account’s Assessments pipeline.'}
|
|
43341
|
-
else{p.data=result;p.
|
|
43342
|
-
if(action==='purchases')await load();
|
|
43609
|
+
else{p.data=result;p.notice=result.message||'';if(action==='fill'){p.fill=result;delete iData[id];if(typeof iOpen!=='undefined'&&iOpen[id])await iLoad(id)}}
|
|
43610
|
+
if(action==='purchases'||action==='attach'||action==='detach')await load();
|
|
43343
43611
|
}catch(e){p.notice=e.message||'The assessment could not be read. Try again.'}
|
|
43344
43612
|
p.busy=false;render(lastState,lastRun);
|
|
43345
43613
|
}
|
|
43346
43614
|
function assessmentPanel(id){
|
|
43347
|
-
const fresh=!assessmentCards[id];const p=assessmentCards[id]||(assessmentCards[id]={busy:false,data:null,list:[],source:'',selected:''
|
|
43615
|
+
const fresh=!assessmentCards[id];const p=assessmentCards[id]||(assessmentCards[id]={busy:false,data:null,list:[],source:'',selected:''});
|
|
43348
43616
|
const box=document.createElement('section');box.className='cds-stack';
|
|
43349
43617
|
const text=(tag,value)=>{const el=document.createElement(tag);if(tag==='h4')el.className='cds-panel-label';el.textContent=value;box.appendChild(el);return el};
|
|
43350
43618
|
const button=(label,run,disabled=false)=>{const b=text('button',label);b.className='cds-btn ghost';b.disabled=p.busy||disabled;b.onclick=run;return b};
|
|
43351
|
-
|
|
43352
|
-
text('p','Record what they bought in your sales conversation. The What they bought list further down is ticked from this, and only that list decides what gets built.');
|
|
43353
|
-
if(p.data){
|
|
43354
|
-
for(const feature of p.data.features){const label=text('label','');label.className='cds-check';const cb=document.createElement('input');cb.type='checkbox';cb.checked=p.choices.includes(feature.id);cb.disabled=p.busy;cb.onchange=()=>{p.choices=cb.checked?[...p.choices,feature.id]:p.choices.filter(v=>v!==feature.id)};label.appendChild(cb);const span=document.createElement('span');span.textContent=feature.name;label.appendChild(span)}
|
|
43355
|
-
button('Save the Assessment record',()=>assessmentRequest(id,'purchases',{purchasedFeatureIds:p.choices}));
|
|
43356
|
-
if(p.data.purchasedFeatureIds===null)text('p','Nothing is recorded from the Assessment yet.');
|
|
43357
|
-
}
|
|
43619
|
+
/* What they bought is ticked on the one list below this panel (2026-09-29); this panel attaches the Assessment. */
|
|
43358
43620
|
text('h4','Assessment');
|
|
43359
43621
|
text('p','The numbers from your sales conversation follow the client into the intake, the Review and their Blueprint.');
|
|
43360
43622
|
const select=text('select','');select.className='cds-select';select.setAttribute('aria-label','Your account where the assessment was saved');select.disabled=p.busy;
|
|
@@ -46201,6 +46463,10 @@ function reviewDocumentHtml(review, opts) {
|
|
|
46201
46463
|
` : opts.scope ? `<div class="head">What this build includes</div>
|
|
46202
46464
|
<p>${opts.scope.package ? `Built to your <b>${esc14(opts.scope.package)}</b> package: ` : ""}${opts.scope.built.length ? esc14(opts.scope.built.map((b) => b.name).join(", ")) : "the foundation only"}. Always included: the pipeline and its stages, the fields, tags and custom values, the intake form, the calendar, and the reply routing.</p>
|
|
46203
46465
|
` : ""}
|
|
46466
|
+
${opts.bought ? `<div class="head">What you bought</div>
|
|
46467
|
+
<p>${esc14(opts.bought.assessment)}</p>
|
|
46468
|
+
${opts.bought.confirmed ? opts.bought.bought.map((b) => `<div class="build"><b>${esc14(b.name)}</b><span>${esc14(b.means)}. ${esc14(b.delivery)}.</span></div>`).join("") : "<p>Not confirmed yet. The build waits until what you bought is saved.</p>"}
|
|
46469
|
+
${opts.bought.confirmed && opts.bought.stillToDo.length ? `<div class="head">Still to do</div><div class="ask"><ul>${opts.bought.stillToDo.map((line3) => `<li>${esc14(line3)}</li>`).join("")}</ul></div>` : ""}` : ""}
|
|
46204
46470
|
${review.unknowns?.length ? `<div class="head">What we still need from you</div><div class="ask"><ul>${review.unknowns.map((u) => `<li>${esc14(u)}</li>`).join("")}</ul></div>` : ""}
|
|
46205
46471
|
${opts.assessment ? `<div class="head">Assessment</div><p>Snapshot attached on ${esc14(opts.assessment.attachedAt.slice(0, 10))}.</p>${assessmentFeatureLines(opts.assessment, [], false).filter((line3) => line3.includes("by your own numbers")).map((line3) => `<p>${esc14(line3)}</p>`).join("")}` : ""}
|
|
46206
46472
|
${available ? `<div class="head">Available next</div>${available}` : opts.assessment ? `<div class="head">Available next</div><p>${purchased ? "Every listed feature is included in what you bought." : "What you bought has not been recorded yet."}</p>` : ""}
|
|
@@ -47779,14 +48045,22 @@ var init_clinic_prefix_registry = __esm({
|
|
|
47779
48045
|
var dashboard_exports = {};
|
|
47780
48046
|
__export(dashboard_exports, {
|
|
47781
48047
|
ACCOUNT_TYPES: () => ACCOUNT_TYPES,
|
|
48048
|
+
APPROVAL_WITHDRAWN_FOR_ASSESSMENT: () => APPROVAL_WITHDRAWN_FOR_ASSESSMENT,
|
|
47782
48049
|
APPROVAL_WITHDRAWN_FOR_SCOPE: () => APPROVAL_WITHDRAWN_FOR_SCOPE,
|
|
48050
|
+
ASSESSMENT_CHANGED_SINCE_APPROVAL: () => ASSESSMENT_CHANGED_SINCE_APPROVAL,
|
|
47783
48051
|
CHOOSE_BEFORE_BUILD: () => CHOOSE_BEFORE_BUILD,
|
|
47784
48052
|
CHOOSE_WHAT_THEY_BOUGHT: () => CHOOSE_WHAT_THEY_BOUGHT,
|
|
47785
48053
|
CLAUDE_REPAIR_MESSAGES: () => CLAUDE_REPAIR_MESSAGES,
|
|
48054
|
+
INTAKE_ANSWERS_MISSING: () => INTAKE_ANSWERS_MISSING,
|
|
47786
48055
|
INTAKE_CONNECT_REQUIRED: () => INTAKE_CONNECT_REQUIRED,
|
|
47787
48056
|
INTAKE_FORM_REQUIRED: () => INTAKE_FORM_REQUIRED,
|
|
47788
48057
|
INTAKE_OWN_MESSAGES: () => INTAKE_OWN_MESSAGES,
|
|
48058
|
+
INTAKE_READ_FAILED: () => INTAKE_READ_FAILED,
|
|
48059
|
+
INTAKE_READ_TRANSIENT: () => INTAKE_READ_TRANSIENT,
|
|
48060
|
+
INTAKE_REFRESH_RETRYING: () => INTAKE_REFRESH_RETRYING,
|
|
47789
48061
|
INTAKE_UNREADABLE: () => INTAKE_UNREADABLE,
|
|
48062
|
+
NOTHING_BOUGHT: () => NOTHING_BOUGHT,
|
|
48063
|
+
NOTHING_BUILT_FROM_TICKS: () => NOTHING_BUILT_FROM_TICKS,
|
|
47790
48064
|
NOTHING_TICKED_IN_PLAN: () => NOTHING_TICKED_IN_PLAN,
|
|
47791
48065
|
NOT_SIGNED_IN_MSG: () => NOT_SIGNED_IN_MSG,
|
|
47792
48066
|
PARKED_NOT_BUILT: () => PARKED_NOT_BUILT,
|
|
@@ -47800,8 +48074,11 @@ __export(dashboard_exports, {
|
|
|
47800
48074
|
STAGES: () => STAGES2,
|
|
47801
48075
|
accountTypeOf: () => accountTypeOf,
|
|
47802
48076
|
agencyConnectionReady: () => agencyConnectionReady,
|
|
48077
|
+
approvalBoughtHash: () => approvalBoughtHash,
|
|
48078
|
+
approvalScopeHash: () => approvalScopeHash,
|
|
47803
48079
|
assessmentAgencyTag: () => assessmentAgencyTag,
|
|
47804
48080
|
assessmentFiguresFor: () => assessmentFiguresFor,
|
|
48081
|
+
boughtUnchangedSinceApproval: () => boughtUnchangedSinceApproval,
|
|
47805
48082
|
callClaudeRecorded: () => callClaudeRecorded,
|
|
47806
48083
|
canonicalIndustry: () => canonicalIndustry,
|
|
47807
48084
|
checkMutationGuard: () => checkMutationGuard,
|
|
@@ -47823,6 +48100,7 @@ __export(dashboard_exports, {
|
|
|
47823
48100
|
insideDir: () => insideDir,
|
|
47824
48101
|
intakeInput: () => intakeInput,
|
|
47825
48102
|
intakeRequestError: () => intakeRequestError,
|
|
48103
|
+
intakeUnavailableError: () => intakeUnavailableError,
|
|
47826
48104
|
isAllowedCockpitHost: () => isAllowedCockpitHost,
|
|
47827
48105
|
isLoopback: () => isLoopback,
|
|
47828
48106
|
isUpstreamFailure: () => isUpstreamFailure,
|
|
@@ -47865,7 +48143,9 @@ __export(dashboard_exports, {
|
|
|
47865
48143
|
uniqueDocName: () => uniqueDocName,
|
|
47866
48144
|
unknownCapabilityHtml: () => unknownCapabilityHtml,
|
|
47867
48145
|
verifyPanelOutcome: () => verifyPanelOutcome,
|
|
48146
|
+
withBoughtJobs: () => withBoughtJobs,
|
|
47868
48147
|
withCockpitShell: () => withCockpitShell,
|
|
48148
|
+
withdrawIfBindingChanged: () => withdrawIfBindingChanged,
|
|
47869
48149
|
writeCockpitState: () => writeCockpitState,
|
|
47870
48150
|
writeDocAtomically: () => writeDocAtomically,
|
|
47871
48151
|
writeOverlay: () => writeOverlay
|
|
@@ -47940,6 +48220,28 @@ function scopeHashOf(sel) {
|
|
|
47940
48220
|
if (sel.wholePreset) return contentHash({ wholePreset: true });
|
|
47941
48221
|
return contentHash({ package: sel.package ?? "", features: [...new Set((sel.features ?? []).filter(isFeatureId))].sort() });
|
|
47942
48222
|
}
|
|
48223
|
+
function approvalScopeHash(state, recordKey) {
|
|
48224
|
+
return approvalBindingHash(scopeHashOf(state.scope?.[recordKey]), state, recordKey);
|
|
48225
|
+
}
|
|
48226
|
+
function withdrawIfBindingChanged(state, recordKey) {
|
|
48227
|
+
const ap = state.planApprovals?.[recordKey];
|
|
48228
|
+
if (!ap || ap.scopeHash === approvalScopeHash(state, recordKey)) return { state, withdrawn: false };
|
|
48229
|
+
const approvals = { ...state.planApprovals };
|
|
48230
|
+
delete approvals[recordKey];
|
|
48231
|
+
return { state: { ...setBuildApproval(state, recordKey, null), planApprovals: approvals }, withdrawn: true };
|
|
48232
|
+
}
|
|
48233
|
+
function withBoughtJobs(tasks2, lines) {
|
|
48234
|
+
const kept = tasks2.filter((t) => !SELF_REPORTED.test(t.text));
|
|
48235
|
+
const prior = new Map(tasks2.map((t) => [t.id, t]));
|
|
48236
|
+
return [...kept, ...lines.map((text2) => prior.get(taskId(text2)) ?? { id: taskId(text2), text: text2 })];
|
|
48237
|
+
}
|
|
48238
|
+
function approvalBoughtHash(state, recordKey) {
|
|
48239
|
+
return boughtBindingHash(scopeHashOf(state.scope?.[recordKey]), state, recordKey);
|
|
48240
|
+
}
|
|
48241
|
+
function boughtUnchangedSinceApproval(ap, state, recordKey) {
|
|
48242
|
+
if (ap.boughtHash) return ap.boughtHash === approvalBoughtHash(state, recordKey);
|
|
48243
|
+
return ap.scopeHash === scopeHashOf(state.scope?.[recordKey]) && !Array.isArray(state.bought?.[recordKey]);
|
|
48244
|
+
}
|
|
47943
48245
|
function hasChosenScope(state, recordKey) {
|
|
47944
48246
|
const sel = state.scope?.[recordKey];
|
|
47945
48247
|
return Boolean(sel && (sel.wholePreset || Array.isArray(sel.features) && sel.features.some(isFeatureId)));
|
|
@@ -48076,7 +48378,10 @@ function startRun(locationId2, stage, onDone, meta2 = {}) {
|
|
|
48076
48378
|
normalizedAnswers: meta2.normalizedAnswers,
|
|
48077
48379
|
dependencies: verificationPlan ? verificationPlan.dependencies ?? planDependencies(verificationPlan.plan) : readSavedPlan(rk)?.dependencies,
|
|
48078
48380
|
industry,
|
|
48079
|
-
|
|
48381
|
+
// Bought-but-not-built jobs are the member's to confirm: no check can read them, so
|
|
48382
|
+
// Verify never reports them as findings (review of ea3526f). They reach the card's
|
|
48383
|
+
// jobs from the obligations file directly.
|
|
48384
|
+
manualObligations: stage === 4 ? readBuildObligations(rk).filter((o) => o.type !== BOUGHT_JOB_TYPE) : void 0,
|
|
48080
48385
|
intakeSummary: describeComposition({ industry, agency }),
|
|
48081
48386
|
customLabels: composed.filter((q3) => {
|
|
48082
48387
|
const base = INTAKE_QUESTIONS.find((b) => b.key === q3.key);
|
|
@@ -48523,6 +48828,10 @@ function readableAccount(locationId2, roster = currentRosterRef(), state = readC
|
|
|
48523
48828
|
const t = state.accounts?.[locationId2]?.type;
|
|
48524
48829
|
return t !== "hidden" && t !== "template";
|
|
48525
48830
|
}
|
|
48831
|
+
function intakeUnavailableError(snap) {
|
|
48832
|
+
const reason3 = snap.unavailableReason;
|
|
48833
|
+
return new Error(reason3 === "auth" ? INTAKE_UNREADABLE : reason3 === "missing" ? INTAKE_ANSWERS_MISSING : reason3 === "error" ? INTAKE_READ_FAILED : INTAKE_READ_TRANSIENT);
|
|
48834
|
+
}
|
|
48526
48835
|
function intakeRequestError(e) {
|
|
48527
48836
|
const text2 = e instanceof Error ? e.message : "";
|
|
48528
48837
|
return INTAKE_OWN_MESSAGES.includes(text2) ? text2 : "The intake request did not finish. Retry this step.";
|
|
@@ -48563,6 +48872,9 @@ function html(roster, seatToken, screen = "board", cap) {
|
|
|
48563
48872
|
/* Board: four doors in one row (DoorTile), then the two-column grid */
|
|
48564
48873
|
.board{display:grid;gap:var(--space-6)}
|
|
48565
48874
|
.txt > .cds-chip{justify-self:start}
|
|
48875
|
+
/* A class that sets display (.cds-chip is inline-flex) beats the browser's own [hidden] rule, so
|
|
48876
|
+
"Not saved yet" stayed on screen after a save (rehearsal 2, finding 12). Hidden means hidden. */
|
|
48877
|
+
[hidden]{display:none!important}
|
|
48566
48878
|
.cds-door > .cds-callout{display:grid;gap:var(--space-2)}
|
|
48567
48879
|
|
|
48568
48880
|
/* Toolbars and rows of controls */
|
|
@@ -48663,7 +48975,6 @@ const SCREEN=document.body.getAttribute("data-screen")||"board";
|
|
|
48663
48975
|
const CAP=${scriptJson(capForPage)};const HUB=${scriptJson(HUB_URL)};const HEADS=${scriptJson(SCREEN_HEADS)};
|
|
48664
48976
|
const INDUSTRIES=${scriptJson([...INDUSTRY_PACKS.map((p) => ({ slug: p.slug, label: p.label })), { slug: "other", label: "Other" }])};
|
|
48665
48977
|
const INDUSTRIES2=${scriptJson(INDUSTRY_PACKS.map((p) => ({ slug: p.slug, label: p.label })))};
|
|
48666
|
-
const FEATS=${scriptJson(FEATURES)};
|
|
48667
48978
|
/* One account at a time (Jerry, 2026-08-26: "way too busy\u2026 tempting to work on multiple
|
|
48668
48979
|
accounts at once"). One card is ever expanded; every other account is a one-line row. */
|
|
48669
48980
|
let newClientId=new URLSearchParams(location.hash.slice(1)).get("client"),existingClientsOpen=false;
|
|
@@ -48672,7 +48983,7 @@ let registerNotice="",registerFailed=false,registerBusy=false;
|
|
|
48672
48983
|
let activeId=null;try{activeId=localStorage.getItem("cmdos-active")||null}catch(e){}
|
|
48673
48984
|
{const linkedClient=new URLSearchParams(location.hash.slice(1)).get("client");if(linkedClient)rememberClient(linkedClient)}
|
|
48674
48985
|
function rememberClient(id){activeId=id;try{localStorage.setItem("cmdos-active",id)}catch(e){};document.querySelectorAll(".cds-side a[href]").forEach(a=>{const href=a.getAttribute("href");if(href&&href.startsWith("/")&&!href.startsWith("/api/")&&!href.includes("?")){a.href=href.split("#")[0]+"#client="+encodeURIComponent(id)}})}
|
|
48675
|
-
function setActive(id){rememberClient(id);render(lastState,lastRun)}
|
|
48986
|
+
function setActive(id){if(typeof iRetryStopExcept==="function")iRetryStopExcept(id);rememberClient(id);render(lastState,lastRun)}
|
|
48676
48987
|
function initials(name){const p=String(name||"").trim().split(/\\s+/);return ((p[0]||"?")[0]+(p[1]?p[1][0]:"")).toUpperCase()}
|
|
48677
48988
|
/* Command Design System pieces, built as text (never markup from data). */
|
|
48678
48989
|
function mk(tag,cls,text){const e=document.createElement(tag);if(cls)e.className=cls;if(text!=null)e.textContent=text;return e}
|
|
@@ -48726,11 +49037,14 @@ let lastStatusOkAt=0,redrawPending=false,pointerIsDown=false,flushTimer=0;
|
|
|
48726
49037
|
then would re-enable its button and drop its message, so it waits like a focused field. */
|
|
48727
49038
|
let iBusy={};
|
|
48728
49039
|
function intakeWorkBusy(){try{return Object.values(iBusy).some(n=>n>0)}catch(e){return false}}
|
|
49040
|
+
/* Inline confirm panels (see icPanel): one waiting on the server holds the redraw like intake work. */
|
|
49041
|
+
let icOpen={};
|
|
49042
|
+
function icBusy(){try{return Object.values(icOpen).some(x=>x&&x.busy)}catch(e){return false}}
|
|
48729
49043
|
/* A counter, so two requests for one client overlapping never clear each other. */
|
|
48730
49044
|
async function iWork(id,fn){iBusy[id]=(iBusy[id]||0)+1;try{return await fn()}finally{iBusy[id]=Math.max(0,(iBusy[id]||0)-1);if(redrawPending)scheduleFlush()}}
|
|
48731
49045
|
function inputFocused(){const a=document.activeElement;return Boolean(a&&/^(INPUT|TEXTAREA|SELECT)$/.test(a.tagName))}
|
|
48732
49046
|
/* Holds back the full redraw, never the status fetch. */
|
|
48733
|
-
function userIsBusy(){if(SCREEN==="new")return true;return inputFocused()||intakeWorkBusy()}
|
|
49047
|
+
function userIsBusy(){if(SCREEN==="new")return true;return inputFocused()||intakeWorkBusy()||icBusy()}
|
|
48734
49048
|
function intakeIsOpen(){try{return Object.values(iOpen||{}).some(Boolean)}catch(e){return false}}
|
|
48735
49049
|
async function fetchRunStatus(){const rr=await(await fetch("/api/run/status")).json();lastStatusOkAt=Date.now();return rr}
|
|
48736
49050
|
function statusIsStale(){return !lastStatusOkAt||Date.now()-lastStatusOkAt>15000}
|
|
@@ -48861,7 +49175,33 @@ function costLabel(kind){return ({stage:"Stage",review:"Client review","plain re
|
|
|
48861
49175
|
function iContact(id,questions){const out={};const keys={first_name:"firstName",last_name:"lastName",email:"email",phone:"phone"};for(const q of questions){if(keys[q.contactTag])out[keys[q.contactTag]]=String((iAnswers[id]||{})[q.label]||"")}return out}
|
|
48862
49176
|
function iEdit(id,label,value){(iAnswers[id]||(iAnswers[id]={}))[label]=value;(iDirty[id]||(iDirty[id]=new Set())).add(label)}
|
|
48863
49177
|
function iApplyProposals(id,proposals){const applied=[];const answers=iAnswers[id]||(iAnswers[id]={});for(const [label,value] of Object.entries(proposals||{})){if(answers[label]&&JSON.stringify(answers[label])!==JSON.stringify(value)){if(!confirm(label+String.fromCharCode(10)+"Current: "+answers[label]+String.fromCharCode(10)+"Proposed: "+value+String.fromCharCode(10)+"Replace this answer?"))continue}iEdit(id,label,value);applied.push(label)}return applied}
|
|
48864
|
-
|
|
49178
|
+
/* Rehearsal 2 (2026-09-29): a read that failed for a passing reason (a timeout, a rate limit) is not a broken
|
|
49179
|
+
key. The panel keeps what it last read, says so quietly, and retries; the next good read clears the note.
|
|
49180
|
+
Only a real refusal of the key (401/403) still says to paste it again. */
|
|
49181
|
+
/* Review of d407f04: the retry is a refresh only. It never changes the client on screen (iLoad does, for a
|
|
49182
|
+
click), it stops when the member moves to another client or closes the intake, a throw schedules the next
|
|
49183
|
+
try instead of ending them, and after I_RETRY_MAX tries it stops and says so. */
|
|
49184
|
+
let iRetry={};const I_RETRY_MAX=10;
|
|
49185
|
+
const I_RETRY_NOTE="Could not refresh just now; retrying.",I_RETRY_GAVE_UP="Could not refresh. Refresh status to try again.";
|
|
49186
|
+
function iRetryNote(id){const n=iRetry[id];if(!n)return null;const p=mk("p","muted iretry",n.gaveUp?I_RETRY_GAVE_UP:(n.note||I_RETRY_NOTE));p.setAttribute("role","status");
|
|
49187
|
+
if(n.gaveUp){const b=mk("button","cds-link","Refresh status");b.type="button";b.onclick=()=>iLoad(id);p.appendChild(document.createTextNode(" "));p.appendChild(b)}return p}
|
|
49188
|
+
function iRetryStop(id){const n=iRetry[id];if(n){if(n.t)clearTimeout(n.t);delete iRetry[id]}}
|
|
49189
|
+
function iRetryStopExcept(id){for(const k of Object.keys(iRetry))if(k!==id)iRetryStop(k)}
|
|
49190
|
+
/* Still worth refreshing: the client is the one on screen. */
|
|
49191
|
+
function iShown(id){return activeId===id}
|
|
49192
|
+
function iScheduleRetry(id,note){const n=((iRetry[id]&&iRetry[id].n)||0)+1;if(iRetry[id]&&iRetry[id].t)clearTimeout(iRetry[id].t);
|
|
49193
|
+
if(n>I_RETRY_MAX){iRetry[id]={n:n,gaveUp:true,t:0};return}
|
|
49194
|
+
iRetry[id]={n:n,note:note||I_RETRY_NOTE,t:setTimeout(()=>{const cur=iRetry[id];if(!cur||cur.gaveUp)return;cur.t=0;if(!iShown(id)){iRetryStop(id);return}iRefresh(id)},Math.min(30000,3000*n))}}
|
|
49195
|
+
/* The background read: no rememberClient, no change of client. */
|
|
49196
|
+
async function iRefresh(id){try{const res=await fetch("/api/client-intake?locationId="+encodeURIComponent(id));const r=await res.json();if(!res.ok&&r&&r.ok!==false)r.ok=false;iApply(id,r)}
|
|
49197
|
+
catch(e){if(!iShown(id)){iRetryStop(id);return}iScheduleRetry(id,I_RETRY_NOTE);try{render(lastState,lastRun)}catch(_){}}}
|
|
49198
|
+
async function iLoad(id){rememberClient(id);iRetryStop(id);const r=await(await clientPageRequest("/api/client-intake?locationId="+id)).json();iApply(id,r)}
|
|
49199
|
+
function iApply(id,r){
|
|
49200
|
+
if(r&&r.ok===false&&r.transient){iScheduleRetry(id,r.error);
|
|
49201
|
+
if(!iData[id]||!iData[id].ok)iData[id]={ok:false,transient:true,error:r.error};
|
|
49202
|
+
render(lastState,lastRun);return}
|
|
49203
|
+
iRetryStop(id);
|
|
49204
|
+
iData[id]=r;
|
|
48865
49205
|
// Only unsaved edits override the newest server submission, including edits made before Connect.
|
|
48866
49206
|
if(r.ok){const edits={};for(const label of iDirty[id]||[])edits[label]=(iAnswers[id]||{})[label];iAnswers[id]={...(r.answers||{}),...edits};if(r.status==="received")delete iPrepared[id]}render(lastState,lastRun)}
|
|
48867
49207
|
let iFieldN=0;
|
|
@@ -48888,8 +49228,10 @@ function iField(id,q,val,prefilled){const w=document.createElement("div");w.clas
|
|
|
48888
49228
|
w.appendChild(inp);return w}
|
|
48889
49229
|
function intakeWorkspace(id,name){const d=iData[id];const box=document.createElement("div");box.className="cds-panel ipanel";
|
|
48890
49230
|
if(!d){box.textContent="Loading intake\u2026";return box}
|
|
49231
|
+
if(!d.ok&&d.transient){box.innerHTML="<h3 class='cds-panel-title'>Intake</h3>";box.appendChild(iRetryNote(id)||mk("p","muted",d.error||"Could not refresh just now; retrying."));return box}
|
|
48891
49232
|
if(!d.ok){box.innerHTML="<h3 class='cds-panel-title'>Intake</h3><p role='alert'>"+escT(clientPageError(d.error))+"</p>";const a=document.createElement("a");a.className="cds-link";a.href="/new";a.textContent="Reconnect this client";box.appendChild(a);return box}
|
|
48892
49233
|
box.innerHTML="<h3 class='cds-panel-title'>Intake for "+escT(name)+"</h3>";
|
|
49234
|
+
{const rn=iRetryNote(id);if(rn)box.appendChild(rn)}
|
|
48893
49235
|
if(!d.industry){const label=document.createElement("p");label.textContent="Choose this client\u2019s industry before preparing the intake.";box.appendChild(label);const select=document.createElement("select");select.className="cds-select";select.setAttribute("aria-label","Client industry");for(const item of [{slug:"",label:"Choose the client industry"},...INDUSTRIES]){const o=document.createElement("option");o.value=item.slug;o.textContent=item.label;select.appendChild(o)}box.appendChild(select);const save=document.createElement("button");save.className="cds-btn";save.textContent="Confirm industry";save.onclick=async()=>{if(!select.value)return;if(await postJSON("/api/industry",{locationId:id,industry:select.value}))await iLoad(id)};box.appendChild(save);return box}
|
|
48894
49236
|
if(d.status==="received"&&d.savedAt){const note=document.createElement("p");note.textContent="Submitted answers are loaded. A newer saved draft may include changes that have not been submitted.";box.appendChild(note)}
|
|
48895
49237
|
const sub=document.createElement("p");sub.className="muted";
|
|
@@ -49052,46 +49394,67 @@ let rOpen={},rData={},rBusy={},rSeen={};
|
|
|
49052
49394
|
let profCache=null,profLoading=false;
|
|
49053
49395
|
async function profLoad(){if(profLoading)return;profLoading=true;try{profCache=((await(await fetch("/api/agency-profile")).json()).profile)||{}}catch(e){profCache={}}profLoading=false;render(lastState,lastRun)}
|
|
49054
49396
|
/* What they bought (2026-09-06). Foundation is always built; the ticks decide the features.
|
|
49055
|
-
|
|
49056
|
-
and
|
|
49397
|
+
One list since 2026-09-29 (Jerry's approved design): the fourteen Assessment items, each with
|
|
49398
|
+
what it means for the client and what ticking it does for this client's preset. The server
|
|
49399
|
+
derives the build scope from the ticks, so the build reads what it always read. Approve also
|
|
49400
|
+
waits for the Assessment question above the list. */
|
|
49057
49401
|
/* The ticks and package chosen but not yet saved, kept outside the block so a redraw keeps them (review of c09e946). */
|
|
49058
|
-
let scopeDraft={};
|
|
49059
|
-
|
|
49402
|
+
let scopeDraft={},boughtNotice={};
|
|
49403
|
+
const BOUGHT_ITEMS=${scriptJson(boughtItemsFor(FEATURE_IDS))};
|
|
49404
|
+
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)}
|
|
49060
49405
|
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()}
|
|
49406
|
+
function boughtView(state,id){return (state.boughtViews||{})[id]||{items:BOUGHT_ITEMS,ticks:BOUGHT_ITEMS.map(i=>i.id),source:"all",answer:null,attached:false,ready:false,suggestions:[]}}
|
|
49407
|
+
/* "Have you run the Assessment for <client>?" One click records the answer; Yes opens the attach flow. */
|
|
49408
|
+
function assessmentQuestion(id,v){
|
|
49409
|
+
const q=mk("div","cds-stack");const who=((typeof ROSTER!=="undefined"&&ROSTER.find(r=>r.locationId===id))||{}).name||"this client";
|
|
49410
|
+
q.appendChild(mk("h4","cds-panel-label","Have you run the Assessment for "+who+"?"));
|
|
49411
|
+
const row=mk("div","cds-row");
|
|
49412
|
+
const opt=(choice,label)=>{const on=v.answer===choice;const b=mk("button","cds-btn"+(on?"":" ghost"),label);b.setAttribute("aria-pressed",String(on));
|
|
49413
|
+
b.onclick=async()=>{b.disabled=true;const r=await postJSON("/api/assessment-choice",{locationId:id,choice});if(r){if(r.approvalWithdrawn)boughtNotice[id]=r.message;load()}else b.disabled=false};row.appendChild(b)};
|
|
49414
|
+
opt("attached","Yes, attach it");opt("run-first","Run it first (recommended)");opt("without","Build without an Assessment");q.appendChild(row);
|
|
49415
|
+
if(v.attached||v.answer==="attached")q.appendChild(assessmentPanel(id));
|
|
49416
|
+
else if(v.answer==="run-first"){const p=mk("p","");const a=document.createElement("a");a.className="cds-link";a.href="/assessment";a.target="_blank";a.rel="noopener";a.textContent="Run the Assessment";p.appendChild(a);p.appendChild(document.createTextNode(". When it is saved, come back and attach it here."));q.appendChild(p)}
|
|
49417
|
+
else if(v.answer==="without")q.appendChild(mk("p","muted","Recorded: no Assessment for this client. The client document says so."));
|
|
49418
|
+
else q.appendChild(mk("p","muted","Approve waits for an answer."));
|
|
49419
|
+
return q}
|
|
49061
49420
|
function scopeBlock(id,state){
|
|
49062
49421
|
const sc=document.createElement("div");sc.className="cds-stack";sc.id="wtb-"+id;
|
|
49063
|
-
|
|
49064
|
-
|
|
49422
|
+
const v=boughtView(state,id);
|
|
49423
|
+
sc.appendChild(assessmentQuestion(id,v));
|
|
49424
|
+
sc.appendChild(mk("h4","cds-panel-label","What they bought"));
|
|
49425
|
+
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 Built get built."));
|
|
49065
49426
|
if(!profCache){profLoad();const p=document.createElement("p");p.className="muted";p.textContent="Loading your packages\u2026";sc.appendChild(p);return sc}
|
|
49066
49427
|
const saved=(state.scope||{})[id]||null;const pk=(profCache.packages||[]);
|
|
49067
49428
|
const sel=document.createElement("select");sel.className="cds-select";sel.setAttribute("aria-label","Package they bought");
|
|
49068
|
-
const o0=document.createElement("option");o0.value="";o0.textContent=pk.length?"No package: tick the
|
|
49429
|
+
const o0=document.createElement("option");o0.value="";o0.textContent=pk.length?"No package: tick the items below":"No packages saved yet (add them on Your agency): tick the items below";sel.appendChild(o0);
|
|
49069
49430
|
for(const p of pk){const o=document.createElement("option");o.value=p.id;o.textContent=p.name+(p.price?" \xB7 "+moneyText(p.price):"");sel.appendChild(o)}
|
|
49070
|
-
|
|
49071
|
-
const
|
|
49072
|
-
const prefill=(state.scopePrefill||{})[id];let ticked=new Set(saved?(saved.wholePreset?offered:saved.features):(prefill||[]));if(saved&&saved.package)sel.value=saved.package;
|
|
49073
|
-
const base=JSON.stringify(saved);if(scopeDraft[id]&&scopeDraft[id].base!==base)delete scopeDraft[id];const dr=scopeDraft[id];if(dr){ticked=new Set(dr.ticked);sel.value=dr.pkg}
|
|
49431
|
+
let ticked=new Set(v.ticks);if(saved&&saved.package)sel.value=saved.package;
|
|
49432
|
+
const base=JSON.stringify([saved,v.ticks]);if(scopeDraft[id]&&scopeDraft[id].base!==base)delete scopeDraft[id];const dr=scopeDraft[id];if(dr){ticked=new Set(dr.ticked);sel.value=dr.pkg}
|
|
49074
49433
|
const keepScope=()=>{scopeDraft[id]={ticked:[...ticked],pkg:sel.value,base:base};unsaved.hidden=false};
|
|
49075
|
-
const everything=document.createElement("button");everything.className="cds-btn
|
|
49076
|
-
everything.title="
|
|
49077
|
-
everything.onclick=
|
|
49434
|
+
const everything=document.createElement("button");everything.className="cds-btn ghost";everything.textContent="Tick everything";
|
|
49435
|
+
everything.title="Ticks all fourteen. Nothing is saved until you press Save what they bought.";
|
|
49436
|
+
everything.onclick=()=>{ticked=new Set(v.items.map(i=>i.id));keepScope();drawBoxes();drawFigs()};
|
|
49078
49437
|
sc.appendChild(everything);
|
|
49079
49438
|
const boxes=document.createElement("div");boxes.className="cds-stack";
|
|
49080
|
-
function drawBoxes(){boxes.innerHTML="";for(const
|
|
49081
|
-
l.dataset.k=
|
|
49082
|
-
const t=document.createElement("span");const tb=document.createElement("b");tb.textContent=
|
|
49439
|
+
function drawBoxes(){boxes.innerHTML="";for(const it of v.items){const l=document.createElement("label");l.className="cds-check";
|
|
49440
|
+
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);
|
|
49441
|
+
const t=document.createElement("span");const tb=document.createElement("b");tb.textContent=it.name;t.appendChild(tb);t.appendChild(mk("small","",it.means));
|
|
49442
|
+
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)}}
|
|
49083
49443
|
const figs=((state.assessmentFigures||{})[id])||{};
|
|
49084
|
-
function drawFigs(){for(const l of boxes.querySelectorAll("label")){const
|
|
49085
|
-
|
|
49086
|
-
const
|
|
49444
|
+
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)}}}
|
|
49445
|
+
/* A package names build features: it ticks their items and unticks the other built ones; the rest stay as they are. */
|
|
49446
|
+
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();
|
|
49447
|
+
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.";
|
|
49087
49448
|
const sv=document.createElement("button");sv.className="cds-btn ghost";sv.textContent="Save what they bought";
|
|
49088
49449
|
const status=document.createElement("p");status.className="muted";status.setAttribute("role","status");
|
|
49089
|
-
sv.onclick=async()=>{if(!ticked.size){status.textContent="Tick at least one
|
|
49090
|
-
if(!
|
|
49091
|
-
const
|
|
49092
|
-
|
|
49450
|
+
sv.onclick=async()=>{if(!ticked.size){status.textContent="Tick at least one item they bought, then save.";return}
|
|
49451
|
+
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}
|
|
49452
|
+
const body={locationId:id,bought:[...ticked]};if(sel.value)body.package=sel.value;const r=await postJSON("/api/scope",body);if(r){unsaved.hidden=true;if(r.approvalWithdrawn)boughtNotice[id]=r.message;else delete boughtNotice[id];delete scopeDraft[id];delete assessmentCards[id];load()}};
|
|
49453
|
+
const nBought=v.ticks.length,nBuilt=v.items.filter(it=>it.kind==="built"&&v.ticks.includes(it.id)).length;
|
|
49454
|
+
status.textContent=saved&&v.source==="saved"?"Saved: "+nBought+" of "+v.items.length+" bought, "+nBuilt+" of them built automatically"+(saved.package?", from the "+saved.package+" package":"")+". 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.";
|
|
49093
49455
|
const unsaved=mk("span","cds-chip warn","Not saved yet");unsaved.hidden=!scopeDraft[id];
|
|
49094
49456
|
sc.appendChild(sel);sc.appendChild(boxes);sc.appendChild(note);sc.appendChild(sv);sc.appendChild(unsaved);sc.appendChild(status);
|
|
49457
|
+
if(boughtNotice[id]){const n=mk("p","cds-callout warn",boughtNotice[id]);n.setAttribute("role","alert");sc.appendChild(n)}
|
|
49095
49458
|
return sc}
|
|
49096
49459
|
async function rLoad(id){const r=await(await fetch("/api/review?locationId="+id)).json();rData[id]=r;render(lastState,lastRun)}
|
|
49097
49460
|
// Finding 20 (2026-08-26): writing the review takes 30\u201390 s and the page re-renders every few seconds meanwhile,
|
|
@@ -49109,7 +49472,6 @@ function reviewPanel(id,name,state,run){const r=rData[id];const d=document.creat
|
|
|
49109
49472
|
{const sc=(state.staffChecks||{})[id];if(!sc||sc.status!=="ready"){const w=document.createElement("p");w.className="cds-callout warn";w.setAttribute("role","alert");w.textContent=sc?STAFF_REVIEW_WARNING:STAFF_REVIEW_UNCHECKED;d.appendChild(w)}}
|
|
49110
49473
|
/* Codex F1 (2026-09-06): the review is the model's output and the names are other businesses' records; both are TEXT. */
|
|
49111
49474
|
const bt=(el,label,text)=>{const b=document.createElement("b");b.textContent=label;el.appendChild(b);el.appendChild(document.createTextNode(moneyText(text)))};
|
|
49112
|
-
d.appendChild(assessmentPanel(id));
|
|
49113
49475
|
const busy=reviewBusy(id,run);
|
|
49114
49476
|
const gen=document.createElement("button");gen.className="cds-btn ghost";
|
|
49115
49477
|
gen.textContent=busy?"Thinking it through\u2026":(r&&r.ok)?"Regenerate":"Generate the review";gen.disabled=busy;
|
|
@@ -49164,17 +49526,13 @@ function reviewPanel(id,name,state,run){const r=rData[id];const d=document.creat
|
|
|
49164
49526
|
const a=document.createElement("a");a.className="cds-link";a.href="/blueprint?locationId="+encodeURIComponent(id);a.target="_blank";a.rel="noopener";a.textContent="Open the prepared client document";brand.appendChild(a)};
|
|
49165
49527
|
brand.appendChild(an);brand.appendChild(ac);brand.appendChild(dl);d.appendChild(brand);
|
|
49166
49528
|
const row=document.createElement("div");row.className="cds-row";
|
|
49167
|
-
|
|
49168
|
-
|
|
49169
|
-
|
|
49170
|
-
|
|
49171
|
-
|
|
49172
|
-
|
|
49173
|
-
|
|
49174
|
-
hand mark goes through markStage(), which asks for the name once, keeps it, and shows any
|
|
49175
|
-
refusal instead of swallowing it. */
|
|
49176
|
-
ap.onclick=async()=>{await markStage(id,2,"done")};
|
|
49177
|
-
row.appendChild(ap);row.appendChild(gen);d.appendChild(row);
|
|
49529
|
+
/* Rehearsal 2, finding 15: this was a third approve button (labelled unlock the build) that only
|
|
49530
|
+
ticked the step, so Build then refused with "Approve building before running Build." It is now the
|
|
49531
|
+
same "Approve this plan" as the next step, and it appears only when approving can go through:
|
|
49532
|
+
once what they bought is saved, and not after approval. */
|
|
49533
|
+
if((state.buildApproved||{})[id]){const ap=document.createElement("button");ap.className="cds-btn ghost";ap.textContent="Approved";ap.disabled=true;row.appendChild(ap)}
|
|
49534
|
+
else if(scopeChosen(state,id)&&!["template","hidden"].includes(typeOf(state,id))&&!PROTECTED[id])row.appendChild(approveButton(id,name,state,"review","cds-btn"));
|
|
49535
|
+
row.appendChild(gen);d.appendChild(row);
|
|
49178
49536
|
return d}
|
|
49179
49537
|
function clientPageError(raw,next){
|
|
49180
49538
|
const sentence=value=>{const t=String(value||"").trim().replace(/\\s*\\u2014\\s*/g,". ").replace(/[.!?;:]+$/,"");return t?t.charAt(0).toUpperCase()+t.slice(1)+".":""};
|
|
@@ -49355,11 +49713,6 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
|
|
|
49355
49713
|
else if(!st.some(s=>s==="done"))div.appendChild(unnest(preflightCard(c)));
|
|
49356
49714
|
/* Kept reachable after a finished Build while a re-run is held for staff (the gate saves its check). */
|
|
49357
49715
|
{const sc=(state.staffChecks||{})[c.locationId];if(!parkedType&&((st[3]||"pending")!=="done"||(sc&&sc.status!=="ready")))div.appendChild(unnest(staffCard(c,state)))}
|
|
49358
|
-
const approveBuilding=async()=>{const typed=prompt('Approve the Blueprint Review for "'+c.name+'" and let Command OS build in this account.'+String.fromCharCode(10,10)+'Type the client name exactly to confirm:');if(typed===null)return;
|
|
49359
|
-
try{const response=await fetch("/api/build-approval",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({locationId:c.locationId,confirmName:typed})});const r=await response.json();
|
|
49360
|
-
if(!response.ok||r.ok!==true){r.ok=false;r.error=r.error||"Command OS could not confirm the result. Refresh this client to read its saved state before retrying."}
|
|
49361
|
-
if(!r.ok)alert(r.error);else load()
|
|
49362
|
-
}catch(e){alert("Command OS could not confirm the result. Refresh this client to read its saved state before retrying.")}};
|
|
49363
49716
|
// The steps, laid out as the owner approved on 2026-09-25: a progress bar, the next step in
|
|
49364
49717
|
// full, then one row per step. Only the layout changed. Every button below keeps the condition
|
|
49365
49718
|
// and the handler it had in the seven-column layout; the next-step panel draws the SAME
|
|
@@ -49456,13 +49809,23 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
|
|
|
49456
49809
|
is "Choose what the client bought", which opens the Blueprint Review at that block. */
|
|
49457
49810
|
const mustChoose=i===2&&!scopeChosen(state,c.locationId);
|
|
49458
49811
|
const gateLabel=mustChoose?"Choose what the client bought":({2:"Approve this plan",5:"Confirm these tasks are finished",6:"Sign this client live"})[i];
|
|
49459
|
-
|
|
49812
|
+
/* Approve opens the one approval panel; the other gates open a hand-mark panel with the name on it (rehearsal 2). */
|
|
49813
|
+
const mkGate=cls=>{if(i===2&&!mustChoose)return approveButton(c.locationId,c.name,state,"gate",cls);
|
|
49814
|
+
const action=document.createElement("button");action.className=cls;action.textContent=gateLabel;const key="mark:"+c.locationId+":"+i;
|
|
49815
|
+
action.onclick=()=>{if(mustChoose)return openWhatTheyBought(c.locationId);icShow(action,key,markSpec(c.locationId,i,gateLabel))};return icAfter(action,key)};
|
|
49460
49816
|
const canBuild=!parkedType&&(i<=1||sandbox||Boolean((state.buildApproved||{})[c.locationId]));
|
|
49461
|
-
|
|
49817
|
+
/* Starts the run; the answer is an error sentence, or "" when it started. */
|
|
49818
|
+
const startIt=async()=>{let j=null;try{const res=await fetch("/api/run",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({locationId:c.locationId,stage:i,intakeConfirmed:i===0})});j=await res.json()}catch(e){}
|
|
49819
|
+
return (j&&j.ok)?"":((j&&j.error)||"Command OS could not confirm the result. Refresh this client to read its saved state before retrying.")};
|
|
49820
|
+
/* Intake, and Verify on an older page build, ask first: in a panel under the button, not a confirm() (rehearsal 2). */
|
|
49821
|
+
const runAsk=i===0?{title:"Create or repair the intake in "+c.name,text:["Intake writes custom fields, one form, and one published notification workflow."].concat(INTAKE_NOTIFICATION_REVIEW),ok:"Create or repair these intake items",busyLabel:"Starting\u2026",run:startIt}
|
|
49822
|
+
:(i===4&&PAGE_PLANS.includes(c.locationId))?{title:"Run the check on "+c.name,text:["Verify submits one test lead to the pages "+c.name+"\u2019s older build made, watches it arrive, and deletes the test contact afterwards. It may fire this client\u2019s own automations once."],ok:"Run the check",busyLabel:"Starting\u2026",run:startIt}:null;
|
|
49823
|
+
const runKeyOf="run:"+c.locationId+":"+i;
|
|
49824
|
+
const runIt=async(ev,btn)=>{ev.stopPropagation();if(runAsk)return icShow(btn,runKeyOf,runAsk);const err=await startIt();if(err)alert(err);load()};
|
|
49462
49825
|
const mkRun=(cls,label)=>{const b=document.createElement("button");b.className=cls;
|
|
49463
49826
|
const busy=run&&run.running;b.textContent=busy&&run.locationId===c.locationId&&run.stage===i?"Running\u2026":label;
|
|
49464
49827
|
b.disabled=Boolean(busy)||(!st.some(s=>s==="done")&&!preflights[c.locationId]?.ok);
|
|
49465
|
-
b.onclick=runIt;return b};
|
|
49828
|
+
b.onclick=ev=>runIt(ev,b);return runAsk?icAfter(b,runKeyOf):b};
|
|
49466
49829
|
if(!live&&nIss){const si=document.createElement("button");si.className="cds-link sactbtn";si.textContent=nIss>1?"See issues":"See issue";si.onclick=toggleIssues;act.appendChild(si)}
|
|
49467
49830
|
/* One way to act on a step: the current step's buttons live in the next-step block only; other rows keep their links.
|
|
49468
49831
|
A gate the owner has already answered offers nothing. */
|
|
@@ -49481,7 +49844,7 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
|
|
|
49481
49844
|
if(canBuild&&POWERED.includes(i))btns.appendChild(mkRun("cds-btn runbtn sbig","Run "+LABELS[i]));
|
|
49482
49845
|
if(!parkedType&&canBuild&&POWERED.includes(i)&&run&&run.running&&run.locationId!==c.locationId){const why=document.createElement("p");why.className="muted swhy";why.textContent="A step is running for another client. This one can start when it finishes.";btns.appendChild(why)}
|
|
49483
49846
|
if(!btns.children.length&&!live&&!parkedType){const why=document.createElement("p");why.className="muted swhy";
|
|
49484
|
-
why.textContent=anyBusy?"Another step is running for this client. This one opens when it finishes.":POWERED.includes(i)?"Approve
|
|
49847
|
+
why.textContent=anyBusy?"Another step is running for this client. This one opens when it finishes.":POWERED.includes(i)?"Approve this plan in step 3, and this step can run.":"";
|
|
49485
49848
|
if(why.textContent)btns.appendChild(why)}
|
|
49486
49849
|
nextBox.title=el.title||"";nextBox.appendChild(main);nextBox.appendChild(btns)}
|
|
49487
49850
|
row.appendChild(el)});
|
|
@@ -49508,9 +49871,9 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
|
|
|
49508
49871
|
if(view.stale.length){const d=mk("details","cds-details");d.appendChild(mk("summary","","Details"));for(const stale of view.stale){const p=document.createElement("p");p.textContent=LABELS[stale.stage]+": stale. "+stale.reason;d.appendChild(p)}div.appendChild(d)}
|
|
49509
49872
|
if(view.approvedAt){const p=document.createElement("p");p.textContent="Building approved on "+niceDate(view.approvedAt,true)+".";div.appendChild(p)}
|
|
49510
49873
|
for(const repair of view.repairs){const p=document.createElement("p");p.textContent="Live workflow repair: "+repair.name+". "+repair.changes.join(" ");div.appendChild(p)}
|
|
49511
|
-
/*
|
|
49874
|
+
/* Rehearsal 2, finding 15: the second building-approval button that was here is gone. The one "Approve this plan"
|
|
49875
|
+
(step 3) approves the listed live workflow repairs too, and says so in its panel. */
|
|
49512
49876
|
if(view.planHash&&!parkedType&&!scopeChosen(state,c.locationId)){const ch=document.createElement("button");ch.className="cds-btn ghost";ch.textContent="Choose what the client bought";ch.onclick=()=>openWhatTheyBought(c.locationId);div.appendChild(ch)}
|
|
49513
|
-
else if(view.planHash&&!parkedType){const approve=document.createElement("button");approve.className="cds-btn ghost";approve.textContent="Approve building\u2026";approve.onclick=async()=>{const typed=sandbox?c.name:prompt("Approve building and the listed live workflow repairs for "+c.name+". Type the client name to confirm.");if(typed===null)return;if(await postJSON("/api/build-approval",{locationId:c.locationId,confirmName:typed,repairHashes:view.repairs.map(r=>r.hash)}))load()};div.appendChild(approve)}
|
|
49514
49877
|
}else{const d=mk("details","cds-details");d.appendChild(mk("summary","","Details"));const p=document.createElement("p");p.textContent="The executable plan is prepared when Build runs.";d.appendChild(p);div.appendChild(d)}
|
|
49515
49878
|
if(view?.handoff)div.appendChild(unnest(handoffPanel(c,view)));
|
|
49516
49879
|
const _prot=PROTECTED[c.locationId];
|
|
@@ -49528,11 +49891,11 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
|
|
|
49528
49891
|
rv.onclick=async()=>{await postJSON("/api/build-approval",{locationId:c.locationId,revoke:true});load()};
|
|
49529
49892
|
ab.appendChild(s1);ab.appendChild(rv);ab.appendChild(protectBtn(c))}
|
|
49530
49893
|
else if(parkedType){ab.appendChild(protectBtn(c))}
|
|
49531
|
-
else{
|
|
49532
|
-
const
|
|
49533
|
-
|
|
49534
|
-
|
|
49535
|
-
ab.appendChild(
|
|
49894
|
+
else{/* No second approve button here (rehearsal 2, finding 15): step 3's "Approve this plan" is the one. */
|
|
49895
|
+
const w=document.createElement("span");w.className="muted";w.textContent="Automated building is off for this client until you approve this plan in step 3. ";
|
|
49896
|
+
ab.appendChild(w);
|
|
49897
|
+
if(!scopeChosen(lastState,c.locationId)){const btn=document.createElement("button");btn.className="cds-btn ghost";btn.textContent="Choose what the client bought";btn.onclick=()=>openWhatTheyBought(c.locationId);ab.appendChild(btn)}
|
|
49898
|
+
ab.appendChild(protectBtn(c))}
|
|
49536
49899
|
div.appendChild(ab)}
|
|
49537
49900
|
// A "live" claim is re-derived from the signature, never trusted from the
|
|
49538
49901
|
// stage array: the gate runs on the write path, and this file can be restored
|
|
@@ -49574,6 +49937,13 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
|
|
|
49574
49937
|
txt.textContent=t.text;if(t.reappeared){txt.textContent="";txt.appendChild(chip("warn","Came back after you ticked it"));txt.appendChild(document.createTextNode(" "+t.text))}
|
|
49575
49938
|
row.appendChild(cb);row.appendChild(txt);box.appendChild(row)});
|
|
49576
49939
|
div.appendChild(box)}}
|
|
49940
|
+
// Suggested next steps with Claude (2026-09-29): from what they bought, shown beside the
|
|
49941
|
+
// jobs once Verify has run, never a job to tick and never something go-live waits on.
|
|
49942
|
+
{const sg=((lastState.boughtViews||{})[c.locationId]||{}).suggestions||[];
|
|
49943
|
+
if(sg.length&&(Boolean((lastState.outcomes||{})[c.locationId+":4"])||((lastState.manualTasks||{})[c.locationId]||[]).length)){
|
|
49944
|
+
const box=mk("div","cds-stack");box.appendChild(mk("h3","cds-panel-title","Suggested next steps with Claude"));
|
|
49945
|
+
box.appendChild(mk("p","muted","From what they bought. These are not jobs in the account, and nothing waits on them."));
|
|
49946
|
+
const ul=mk("ul","cds-list");for(const s of sg)ul.appendChild(mk("li","",s));box.appendChild(ul);div.appendChild(box)}}
|
|
49577
49947
|
const _intakeIn=(iData[c.locationId]&&iData[c.locationId].status==="received");
|
|
49578
49948
|
const _writing=reviewBusy(c.locationId,run);
|
|
49579
49949
|
{ // Review is accessible before intake so an assessment can fill it.
|
|
@@ -49585,7 +49955,7 @@ title.textContent="Claude usage for this client";details.appendChild(title);for(
|
|
|
49585
49955
|
if(rOpen[c.locationId])div.appendChild(unnest(reviewPanel(c.locationId,c.name,state,run)))}
|
|
49586
49956
|
if(st[0]==="done"){const ir=document.createElement("div");ir.className="cds-row";
|
|
49587
49957
|
const ib2=document.createElement("button");ib2.className="cds-btn ghost";ib2.textContent=iOpen[c.locationId]?"Close the intake":"Complete the intake";ib2.setAttribute("aria-expanded",String(Boolean(iOpen[c.locationId])));
|
|
49588
|
-
ib2.onclick=async()=>{iOpen[c.locationId]=!iOpen[c.locationId];if(iOpen[c.locationId]&&!iData[c.locationId])await iLoad(c.locationId);else render(lastState,lastRun)};
|
|
49958
|
+
ib2.onclick=async()=>{iOpen[c.locationId]=!iOpen[c.locationId];if(!iOpen[c.locationId])iRetryStop(c.locationId);if(iOpen[c.locationId]&&!iData[c.locationId])await iLoad(c.locationId);else render(lastState,lastRun)};
|
|
49589
49959
|
ir.appendChild(ib2);
|
|
49590
49960
|
const dd=iData[c.locationId];
|
|
49591
49961
|
if(dd&&dd.ok){const ic=document.createElement("span");ic.className="cds-chip istatus "+(dd.status==="received"?"ok":dd.status==="sent"?"warn":"");
|
|
@@ -49636,10 +50006,77 @@ function acctRow(c,state,run){const row=document.createElement("li");row.classNa
|
|
|
49636
50006
|
Derived server-side from the same board the cards show (/api/queue): no second
|
|
49637
50007
|
list to fall out of step with. Clicking a row opens that account's card. \u2500\u2500 */
|
|
49638
50008
|
let queueData=null,showQueueRest=false,showLive=false;
|
|
49639
|
-
|
|
49640
|
-
|
|
49641
|
-
|
|
49642
|
-
|
|
50009
|
+
/* \u2500\u2500 Inline confirm (rehearsal 2, 2026-09-29) \u2500\u2500
|
|
50010
|
+
A panel under the button instead of prompt(), confirm() or alert(): one stray keystroke dismissed a
|
|
50011
|
+
prompt, and an alert sent the member off the page to read why. The panel keeps its fields, says what
|
|
50012
|
+
went wrong in place, and is drawn again after a redraw with what was typed (its state lives here). */
|
|
50013
|
+
function icPanel(key){const st=icOpen[key],sp=st.spec;const p=document.createElement("div");p.className="cds-callout info cds-stack ic-panel";p.dataset.icp=key;p.setAttribute("role","group");
|
|
50014
|
+
if(sp.title)p.setAttribute("aria-label",sp.title);
|
|
50015
|
+
/* Clicks and keys stay in the panel: a queue row behind it opens a client on click. */
|
|
50016
|
+
p.onclick=e=>e.stopPropagation();
|
|
50017
|
+
p.onkeydown=e=>{e.stopPropagation();if(e.key==="Escape"&&!st.busy){delete icOpen[key];p.remove()}};
|
|
50018
|
+
if(sp.title)p.appendChild(mk("b","",sp.title));
|
|
50019
|
+
for(const line of [].concat(sp.text||[]))p.appendChild(mk("p","",line));
|
|
50020
|
+
const inputs={};let ok;
|
|
50021
|
+
for(const f of sp.fields||[]){const l=document.createElement("label");l.className="cds-field";l.appendChild(mk("span","",f.label));
|
|
50022
|
+
const inp=document.createElement("input");inp.className="cds-input";inp.autocomplete="off";inp.name="ic:"+key+":"+f.id;inp.value=st.values[f.id]!=null?st.values[f.id]:(f.value||"");
|
|
50023
|
+
inp.oninput=()=>{st.values[f.id]=inp.value};inp.onkeydown=e=>{if(e.key==="Enter"){e.preventDefault();ok.click()}};
|
|
50024
|
+
l.appendChild(inp);p.appendChild(l);inputs[f.id]=inp}
|
|
50025
|
+
const err=mk("p","cds-callout warn",st.error||"");err.setAttribute("role","alert");err.hidden=!st.error;p.appendChild(err);
|
|
50026
|
+
const row=document.createElement("div");row.className="cds-row";
|
|
50027
|
+
ok=document.createElement("button");ok.type="button";ok.className="cds-btn";ok.textContent=st.busy?(sp.busyLabel||"Working\u2026"):sp.ok;ok.disabled=Boolean(st.busy);
|
|
50028
|
+
const no=document.createElement("button");no.type="button";no.className="cds-btn ghost";no.textContent="Cancel";no.disabled=Boolean(st.busy);
|
|
50029
|
+
no.onclick=()=>{delete icOpen[key];p.remove()};
|
|
50030
|
+
ok.onclick=async()=>{if(st.busy)return;const vals={};for(const f of sp.fields||[])vals[f.id]=String(inputs[f.id].value||"").trim();
|
|
50031
|
+
st.busy=true;st.error="";err.hidden=true;ok.disabled=true;no.disabled=true;ok.textContent=sp.busyLabel||"Working\u2026";
|
|
50032
|
+
let msg="";try{msg=(await sp.run(vals))||""}catch(e){msg="Command OS could not confirm the result. Refresh this client to read its saved state before retrying."}
|
|
50033
|
+
st.busy=false;
|
|
50034
|
+
/* A redraw during the request drew a new panel from the state (showing "Approving\u2026"); this one may
|
|
50035
|
+
be detached. Every copy of the panel is replaced with a fresh one, or removed on success (review of d407f04). */
|
|
50036
|
+
const copies=new Set([p,...icLive(key)]);
|
|
50037
|
+
if(msg){st.error=msg;for(const x of copies)if(x.parentNode)x.replaceWith(icPanel(key));if(redrawPending)scheduleFlush();return}
|
|
50038
|
+
delete icOpen[key];for(const x of copies)x.remove();if(sp.after)await sp.after();else load()};
|
|
50039
|
+
row.appendChild(ok);row.appendChild(no);p.appendChild(row);return p}
|
|
50040
|
+
/* Opens the panel under the button (or focuses it when it is already open). */
|
|
50041
|
+
function icShow(btn,key,spec){const nx=btn.nextElementSibling;if(icOpen[key]&&nx&&nx.dataset&&nx.dataset.icp===key){const f=nx.querySelector("input")||nx.querySelector("button");if(f)f.focus();return}
|
|
50042
|
+
icOpen[key]={spec:spec,values:{},error:"",busy:false};const p=icPanel(key);btn.insertAdjacentElement("afterend",p);
|
|
50043
|
+
const f=p.querySelector("input")||p.querySelector("button");if(f)f.focus()}
|
|
50044
|
+
/* Marks a button as the owner of a panel, so a redraw puts an open panel back under it. */
|
|
50045
|
+
/* Put back synchronously by render() before it restores focus, so a field typed in keeps the cursor;
|
|
50046
|
+
the microtask covers a card drawn outside render(). */
|
|
50047
|
+
let icQueue=[];
|
|
50048
|
+
function icAfter(btn,key){if(icOpen[key]){icQueue.push([btn,key]);queueMicrotask(icFlush)}return btn}
|
|
50049
|
+
function icFlush(){const q=icQueue;icQueue=[];for(const [btn,key] of q){const nx=btn.nextElementSibling;if(btn.parentNode&&icOpen[key]&&!(nx&&nx.dataset&&nx.dataset.icp===key))btn.insertAdjacentElement("afterend",icPanel(key))}}
|
|
50050
|
+
/* Every panel for this key on the page now. */
|
|
50051
|
+
function icLive(key){return [...document.querySelectorAll(".ic-panel")].filter(x=>x.dataset.icp===key)}
|
|
50052
|
+
/* A panel whose question the state has answered elsewhere is dropped, so a later revoke or reset never
|
|
50053
|
+
reopens it with old typing (review of d407f04). */
|
|
50054
|
+
function icPrune(state){for(const k of Object.keys(icOpen)){const st=icOpen[k];if(!st||st.busy)continue;const [kind,id,step]=k.split(":");
|
|
50055
|
+
const stages=((state.clients||{})[id]||{}).stages||[];
|
|
50056
|
+
if(kind==="approve"&&(state.buildApproved||{})[id])delete icOpen[k];
|
|
50057
|
+
else if((kind==="mark"||kind==="qmark")&&stages[Number(step)]==="done")delete icOpen[k]}}
|
|
50058
|
+
/* The name Command OS records on a hand mark, as last saved on this computer. */
|
|
50059
|
+
function opName(){let n="";try{n=localStorage.getItem("cmdos-operator")||""}catch(e){}return n}
|
|
50060
|
+
function saveOpName(n){try{localStorage.setItem("cmdos-operator",n)}catch(e){}}
|
|
50061
|
+
const NAME_FIELD=()=>({id:"who",label:"Your name",value:opName()});
|
|
50062
|
+
function needName(v){if(!v.who)return "Type your name. Command OS records who did this, so nobody has to take it on trust.";saveOpName(v.who);return ""}
|
|
50063
|
+
/* The ONE approval (rehearsal 2, finding 15). The next-step button and the Blueprint Review both open this,
|
|
50064
|
+
with the same label and the same request; nothing else approves, and nothing refuses and points elsewhere. */
|
|
50065
|
+
function approveSpec(id,name,state){const sandbox=typeOf(state,id)==="sandbox";
|
|
50066
|
+
const view=(state.buildViews||{})[id];const repairs=(view&&view.planHash&&view.repairs)||[];
|
|
50067
|
+
return {title:"Approve this plan for "+name,
|
|
50068
|
+
text:["Command OS then builds in this account what the Blueprint Review lists"+(repairs.length?", and makes the "+repairs.length+" live workflow "+plural(repairs.length,"repair")+" listed with the plan":"")+"."],
|
|
50069
|
+
fields:[NAME_FIELD()].concat(sandbox?[]:[{id:"client",label:"Type the client name to confirm: "+name,value:""}]),
|
|
50070
|
+
ok:"Approve",busyLabel:"Approving\u2026",
|
|
50071
|
+
run:async v=>{const bad=needName(v);if(bad)return bad;
|
|
50072
|
+
if(!sandbox&&v.client.toLowerCase()!==String(name).trim().toLowerCase())return 'Type the client name exactly as shown: "'+name+'".';
|
|
50073
|
+
let res,j=null;try{res=await fetch("/api/build-approval",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({locationId:id,confirmName:sandbox?name:v.client,by:v.who,repairHashes:repairs.map(r=>r.hash)})})}
|
|
50074
|
+
catch(e){return "Command OS could not confirm the result. Refresh this client to read its saved state before retrying."}
|
|
50075
|
+
try{j=await res.json()}catch(e){}
|
|
50076
|
+
if(!res.ok||!j||j.ok!==true)return (j&&j.error)||"Command OS could not confirm the result. Refresh this client to read its saved state before retrying.";
|
|
50077
|
+
return ""}}}
|
|
50078
|
+
function approveButton(id,name,state,place,cls){const b=document.createElement("button");b.className=cls;b.textContent="Approve this plan";
|
|
50079
|
+
const key="approve:"+id+":"+place;b.onclick=()=>icShow(b,key,approveSpec(id,name,lastState||state));return icAfter(b,key)}
|
|
49643
50080
|
/* Finding 35: a write whose reply nobody reads is a button that can start failing in
|
|
49644
50081
|
silence. Every cockpit write goes through here, so a refusal is shown rather than
|
|
49645
50082
|
swallowed. */
|
|
@@ -49652,13 +50089,14 @@ async function postJSON(url,body){
|
|
|
49652
50089
|
return j||true}
|
|
49653
50090
|
/* Nothing hand-markable that Command OS can check for itself. Steps it runs are marked
|
|
49654
50091
|
by the run; steps that need a person are marked by that person, on the record. */
|
|
49655
|
-
async function markStage(id,i,status){
|
|
49656
|
-
if(POWERED.includes(i)&&status!=="pending")
|
|
49657
|
-
|
|
49658
|
-
|
|
49659
|
-
|
|
49660
|
-
|
|
49661
|
-
|
|
50092
|
+
async function markStage(id,i,status,who){
|
|
50093
|
+
if(POWERED.includes(i)&&status!=="pending")return LABELS[i]+" is a step Command OS runs and checks for you, so it cannot be ticked off by hand. Use the Run this step button on the card. If it went wrong, set it back to not started and run it again.";
|
|
50094
|
+
let r=null;try{r=await(await fetch("/api/stage",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({locationId:id,stage:i,status:status,by:who})})).json()}catch(e){}
|
|
50095
|
+
if(!r||!r.ok)return (r&&r.error)||"Command OS could not confirm the result. Refresh this client to read its saved state before retrying.";
|
|
50096
|
+
return ""}
|
|
50097
|
+
/* A hand mark opens a panel with the name that goes on it (prefilled from last time) instead of a prompt. */
|
|
50098
|
+
function markSpec(id,i,label,text){return {title:label,text:[].concat(text||[],"Marking this done puts your name and the time on it."),fields:[NAME_FIELD()],ok:label,busyLabel:"Saving\u2026",
|
|
50099
|
+
run:async v=>{const bad=needName(v);if(bad)return bad;return markStage(id,i,"done",v.who)},after:async()=>{await loadQueue();load()}}}
|
|
49662
50100
|
/* cheap: a background refresh uses GET /api/queue, which derives the queue from the saved board without re-reading
|
|
49663
50101
|
every client's intake from GoHighLevel; the POST (which does) stays for the page's own loads. */
|
|
49664
50102
|
/* Three ways to read the queue:
|
|
@@ -49718,10 +50156,9 @@ function queueRow(it){const row=document.createElement("li");row.className="q-ro
|
|
|
49718
50156
|
/* State in words: running is info; anything unfinished is warn. */
|
|
49719
50157
|
side.appendChild(it.running?chip("info","Running now"):it.needs==="you"?chip("warn","Your call"):(it.proof&&it.proof.issues&&it.proof.issues.length)?chip("warn","To sort out"):chip("warn","Not run yet"));
|
|
49720
50158
|
if(it.action==="mark"){const mb=document.createElement("button");mb.className="cds-btn ghost";mb.textContent="Mark done";
|
|
49721
|
-
|
|
49722
|
-
|
|
49723
|
-
|
|
49724
|
-
side.appendChild(mb)}
|
|
50159
|
+
const mkey="qmark:"+it.locationId+":"+it.stageIndex;
|
|
50160
|
+
mb.onclick=(e)=>{e.stopPropagation();icShow(mb,mkey,markSpec(it.locationId,it.stageIndex,"Mark done",it.title))};
|
|
50161
|
+
side.appendChild(icAfter(mb,mkey))}
|
|
49725
50162
|
else side.appendChild(mk("span","cds-link acct-open","Open"));
|
|
49726
50163
|
row.appendChild(side);
|
|
49727
50164
|
row.onclick=()=>{if(it.href){rememberClient(it.locationId);location.href=it.href}else queueOpen(it.locationId)};
|
|
@@ -49745,7 +50182,9 @@ function operatorLine(){const who=document.createElement("span");who.className="
|
|
|
49745
50182
|
let nm="";try{nm=localStorage.getItem("cmdos-operator")||""}catch(e){}
|
|
49746
50183
|
who.appendChild(document.createTextNode(nm?("Marking as "+nm):"Nobody set to mark steps"));
|
|
49747
50184
|
const ch=document.createElement("button");ch.className="cds-link";ch.textContent=nm?"Change":"Set your name";
|
|
49748
|
-
|
|
50185
|
+
/* A panel with the name field, not a prompt (rehearsal 2). */
|
|
50186
|
+
ch.onclick=()=>icShow(ch,"opname",{title:"Who marks steps done",fields:[NAME_FIELD()],ok:"Save",busyLabel:"Saving\u2026",run:async v=>needName(v),after:async()=>render(lastState,lastRun)});
|
|
50187
|
+
who.appendChild(icAfter(ch,"opname"));return who}
|
|
49749
50188
|
function queueBlock(match){const q=queueData;if(!q||!q.ok)return queueReadError();
|
|
49750
50189
|
const frag=document.createDocumentFragment();
|
|
49751
50190
|
const items=(q.items||[]).filter(i=>match({name:i.clientName,locationId:i.locationId}));
|
|
@@ -49939,7 +50378,8 @@ const KEY_FACTS=[/website/i,/primary goal/i,/primary services|services you sell/
|
|
|
49939
50378
|
function capAnswer(v){return Array.isArray(v)?v.join(", "):String(v==null?"":v).trim()}
|
|
49940
50379
|
function renderIntakeCapability(m,c){const id=c.locationId;const data=iData[id];
|
|
49941
50380
|
if(!data){iLoad(id);const p=panel("Intake","The client\u2019s answers");p.appendChild(mk("p","muted","Reading the intake\u2026"));m.appendChild(p);return}
|
|
49942
|
-
if(data.ok===false){const
|
|
50381
|
+
if(data.ok===false&&data.transient){const p=mk("p","muted",data.error||"Could not refresh just now; retrying.");p.setAttribute("role","status");m.appendChild(p)}
|
|
50382
|
+
else if(data.ok===false){const w=mk("div","cds-callout warn");w.setAttribute("role","alert");w.appendChild(mk("span","ttl","Could not read the intake"));w.appendChild(mk("span","",data.error||"Refresh the page to try again."));m.appendChild(w)}
|
|
49943
50383
|
const answers=data.answers||{};const questions=data.questions||[];
|
|
49944
50384
|
const answerOf=q=>capAnswer(answers[q.label]!=null?answers[q.label]:answers[q.key]);
|
|
49945
50385
|
const answered=questions.length?questions.filter(q=>answerOf(q)).map(q=>[q.label,answerOf(q)]):Object.entries(answers).map(([k,v])=>[k,capAnswer(v)]).filter(x=>x[1]);
|
|
@@ -50063,7 +50503,7 @@ function restoreView(v){const m=document.getElementById("main");if(!v||!m)return
|
|
|
50063
50503
|
if(v.focus){const el=[...m.querySelectorAll(v.focus.tag)].filter(x=>fieldKey(x,m)===v.focus.key)[v.focus.n];
|
|
50064
50504
|
if(el&&document.activeElement!==el){try{el.focus({preventScroll:true})}catch(e){}try{if(v.focus.st!=null)el.setSelectionRange(v.focus.st,v.focus.en)}catch(e){}}}
|
|
50065
50505
|
if(window.scrollX!==v.x||window.scrollY!==v.y){try{window.scrollTo(v.x,v.y)}catch(e){}}}
|
|
50066
|
-
function render(state,run){const v=captureView();try{renderPage(state,run)}finally{lastDrawnSig=dataSig(state,run);restoreView(v)}}
|
|
50506
|
+
function render(state,run){const v=captureView();try{if(state)icPrune(state);renderPage(state,run);icFlush()}finally{lastDrawnSig=dataSig(state,run);restoreView(v)}}
|
|
50067
50507
|
function renderPage(state,run){if(state.roster){ROSTER.splice(0,ROSTER.length,...state.roster)}syncReviews(state);lastState=state;lastRun=run;
|
|
50068
50508
|
if(state&&state.protectedAccounts)PROTECTED=state.protectedAccounts;const m=document.getElementById("main");m.innerHTML="";
|
|
50069
50509
|
/* Four screens share this page's data and differ only in what they show. */
|
|
@@ -50917,9 +51357,18 @@ async function runDashboard(argv) {
|
|
|
50917
51357
|
if (req.method === "POST" && url === "/api/client-assessment") {
|
|
50918
51358
|
readBody(req, res, 2e4, async (body) => {
|
|
50919
51359
|
try {
|
|
50920
|
-
const
|
|
51360
|
+
const request2 = JSON.parse(body);
|
|
51361
|
+
const result = await assessmentAttachmentAction(request2, attachmentDeps);
|
|
51362
|
+
let withdrawn = false;
|
|
51363
|
+
if (typeof request2?.clientLocationId === "string") {
|
|
51364
|
+
const after = withdrawIfBindingChanged(readCockpitState(), request2.clientLocationId);
|
|
51365
|
+
if (after.withdrawn) {
|
|
51366
|
+
writeCockpitState(after.state);
|
|
51367
|
+
withdrawn = true;
|
|
51368
|
+
}
|
|
51369
|
+
}
|
|
50921
51370
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
50922
|
-
res.end(JSON.stringify(result));
|
|
51371
|
+
res.end(JSON.stringify({ ...result, ...withdrawn ? { approvalWithdrawn: true, message: APPROVAL_WITHDRAWN_FOR_ASSESSMENT } : {} }));
|
|
50923
51372
|
} catch (e) {
|
|
50924
51373
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
50925
51374
|
logQuietly(`client assessment: ${(0, import_node_util5.inspect)(e)}`);
|
|
@@ -51304,12 +51753,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
51304
51753
|
}
|
|
51305
51754
|
const assessmentFigures = {};
|
|
51306
51755
|
for (const loc of Object.keys(st.assessmentAttachments ?? {})) assessmentFigures[loc] = assessmentFiguresFor(st, loc);
|
|
51307
|
-
const scopePrefill = {};
|
|
51308
|
-
for (const loc of Object.keys(st.assessmentPurchases ?? {})) {
|
|
51309
|
-
const p = scopePrefillFor(st, loc);
|
|
51310
|
-
if (p) scopePrefill[loc] = p;
|
|
51311
|
-
}
|
|
51312
51756
|
const scopeOffered = Object.fromEntries(currentRoster().map((c) => [c.locationId, offeredFeaturesFor(st, c.locationId)]));
|
|
51757
|
+
const boughtViews = Object.fromEntries(currentRoster().map((c) => [c.locationId, boughtViewFor(st, c.locationId, scopeOffered[c.locationId])]));
|
|
51313
51758
|
const buildViews = Object.fromEntries(currentRoster().map((c) => {
|
|
51314
51759
|
const saved = readSavedPlan(c.locationId);
|
|
51315
51760
|
const approval = st.planApprovals?.[c.locationId];
|
|
@@ -51340,9 +51785,9 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
51340
51785
|
delivery: void 0
|
|
51341
51786
|
};
|
|
51342
51787
|
}
|
|
51343
|
-
for (const [id, view2] of Object.entries(buildViews)) if (!view2.planHash || !view2.approvedAt || view2.stale.length || st.outcomes?.[`${id}:4`]?.ok !== true || st.manualTasks?.[id]
|
|
51788
|
+
for (const [id, view2] of Object.entries(buildViews)) if (!view2.planHash || !view2.approvedAt || view2.stale.length || st.outcomes?.[`${id}:4`]?.ok !== true || openTasks(st.manualTasks?.[id] ?? []).length || st.outcomes?.[`${id}:4`]?.issues?.length) liveClaims[id] = { live: false };
|
|
51344
51789
|
const outcomes = Object.fromEntries(Object.entries(st.outcomes ?? {}).map(([key, value]) => [key, { ...value, ...key.endsWith(":4") && value.verification ? { plain: verifyPanelOutcome(value) } : { ...plainFallbackOutcome(value), plain: plainFallbackOutcome(value.plain ?? value) }, error: value.error ? "The step did not finish. Check the account before retrying." : void 0 }]));
|
|
51345
|
-
res.end(JSON.stringify({ ...st, assessmentFigures,
|
|
51790
|
+
res.end(JSON.stringify({ ...st, assessmentFigures, boughtViews, outcomes, manualTasks: Object.fromEntries(Object.entries(st.manualTasks ?? {}).map(([id, tasks2]) => {
|
|
51346
51791
|
const records = st.outcomes?.[`${id}:4`]?.verification;
|
|
51347
51792
|
const allowed = records ? verifyReadback(records).allowPublishing : false;
|
|
51348
51793
|
return [id, tasks2.map((t) => ({ ...t, text: verifyPublishingText(plainFallback(t.text, { nextStep: true }), allowed) })).filter((t) => t.text)];
|
|
@@ -51404,7 +51849,9 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
51404
51849
|
if (stage === 3) {
|
|
51405
51850
|
if (!hasChosenScope(st0, rk)) throw new Error(who.clientKey ? secondClientChooseMessage(who.clientKey, who.name) : CHOOSE_BEFORE_BUILD);
|
|
51406
51851
|
const ap = st0.planApprovals?.[rk];
|
|
51407
|
-
|
|
51852
|
+
const assessmentWait = assessmentGateRefusal(st0, rk, who.clientKey, who.name);
|
|
51853
|
+
if (assessmentWait) throw new Error(assessmentWait);
|
|
51854
|
+
if (ap && ap.scopeHash !== approvalScopeHash(st0, rk)) throw new Error(boughtUnchangedSinceApproval(ap, st0, rk) ? ASSESSMENT_CHANGED_SINCE_APPROVAL : SCOPE_CHANGED_SINCE_APPROVAL);
|
|
51408
51855
|
}
|
|
51409
51856
|
if (who.clientKey && [3, 4].includes(stage)) {
|
|
51410
51857
|
inputHash = secondClientInputHash(readSavedPlan(rk));
|
|
@@ -51423,7 +51870,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
51423
51870
|
const client = clientFor(locationId2);
|
|
51424
51871
|
if (!client) throw new Error(INTAKE_UNREADABLE);
|
|
51425
51872
|
const snap = await readIntakeSnapshot(client, locationId2);
|
|
51426
|
-
if (snap.status === "unavailable") throw
|
|
51873
|
+
if (snap.status === "unavailable") throw intakeUnavailableError(snap);
|
|
51427
51874
|
normalizedAnswers = normalizedIntake(snap);
|
|
51428
51875
|
inputHash = intakeInput(normalizedAnswers, st0.accounts?.[locationId2]?.industry, composeQuestionSet({ industry: st0.accounts?.[locationId2]?.industry, agency: readOverlay() }));
|
|
51429
51876
|
st0 = { ...st0, intakeInputs: { ...st0.intakeInputs, [locationId2]: { hash: inputHash, answers: normalizedAnswers, status: snap.status } } };
|
|
@@ -51444,7 +51891,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
51444
51891
|
const c = clientFor(locationId2);
|
|
51445
51892
|
if (!c) throw new Error(INTAKE_UNREADABLE);
|
|
51446
51893
|
const snap = await readIntakeSnapshot(c, locationId2);
|
|
51447
|
-
if (snap.status === "unavailable") throw
|
|
51894
|
+
if (snap.status === "unavailable") throw intakeUnavailableError(snap);
|
|
51448
51895
|
if (snap.status !== "received") throw new Error("Open the real form, submit it, then refresh.");
|
|
51449
51896
|
const required = composeQuestionSet({ industry: st0.accounts?.[locationId2]?.industry, agency: readOverlay() }).filter((q3) => q3.required && !String(normalizedAnswers?.[q3.label] ?? normalizedAnswers?.[q3.key] ?? "").trim());
|
|
51450
51897
|
if (required.length) throw new Error("Complete these required intake answers: " + required.map((q3) => q3.label).join(", ") + ".");
|
|
@@ -51530,7 +51977,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
51530
51977
|
const st = readCockpitState();
|
|
51531
51978
|
if (!c && !st.pending?.[locationId2]) throw new Error(INTAKE_CONNECT_REQUIRED);
|
|
51532
51979
|
const snap = c ? await readIntakeSnapshot(c, locationId2, Boolean(st.intakeSent?.[locationId2])) : { status: "not_installed", fields: {}, queryKeys: {}, submissions: 0 };
|
|
51533
|
-
if (snap.status === "unavailable") throw
|
|
51980
|
+
if (snap.status === "unavailable") throw intakeUnavailableError(snap);
|
|
51534
51981
|
const industry = st.accounts?.[locationId2]?.industry;
|
|
51535
51982
|
const questions = composeQuestionSet({ industry, agency: readOverlay() }).map((q3) => ({ key: q3.key, contactTag: q3.field.kind === "standard" ? q3.field.tag : void 0, label: q3.label, sectionLabel: q3.sectionLabel, required: q3.required, options: q3.options ?? null, placeholder: q3.placeholder ?? "", multi: q3.field.kind === "custom" && q3.field.dataType === "MULTIPLE_OPTIONS" }));
|
|
51536
51983
|
const answers = prefillTeamAnswers(normalizedIntake(snap, st.intakeDrafts?.[locationId2]), st.staffRoster?.[locationId2], questions);
|
|
@@ -51543,7 +51990,8 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
51543
51990
|
} catch (e) {
|
|
51544
51991
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
51545
51992
|
logQuietly(`client-intake ${new URL(req.url || "/", "http://x").searchParams.get("locationId") || ""}: ${e instanceof Error ? e.message : String(e)}`);
|
|
51546
|
-
res.end(JSON.stringify({ ok: false, error:
|
|
51993
|
+
if (e instanceof Error && e.message === INTAKE_READ_TRANSIENT) res.end(JSON.stringify({ ok: false, transient: true, error: INTAKE_REFRESH_RETRYING }));
|
|
51994
|
+
else res.end(JSON.stringify({ ok: false, error: intakeRequestError(e) }));
|
|
51547
51995
|
}
|
|
51548
51996
|
})();
|
|
51549
51997
|
return;
|
|
@@ -51966,7 +52414,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
51966
52414
|
}
|
|
51967
52415
|
const name = currentRoster().find((r) => r.locationId === locationId2)?.name || locationId2;
|
|
51968
52416
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
51969
|
-
res.end(reviewDocumentHtml(review, { scope: documentScopeFor(st, locationId2, readSavedPlan(locationId2)), assessment: st.assessmentAttachments?.[locationId2], purchasedFeatureIds: assessmentPurchasesFor(st, locationId2), planLines: readSavedPlan(locationId2) ? executablePlanLines(readSavedPlan(locationId2)) : [], clientName: name, agencyName: q3.get("agency") || st.agencyName, accent: q3.get("accent") || st.agencyAccent }));
|
|
52417
|
+
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)), planLines: readSavedPlan(locationId2) ? executablePlanLines(readSavedPlan(locationId2)) : [], clientName: name, agencyName: q3.get("agency") || st.agencyName, accent: q3.get("accent") || st.agencyAccent }));
|
|
51970
52418
|
return;
|
|
51971
52419
|
}
|
|
51972
52420
|
if (req.method === "GET" && url === "/api/review") {
|
|
@@ -51978,12 +52426,15 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
51978
52426
|
if (req.method === "POST" && url === "/api/build-approval") {
|
|
51979
52427
|
readBody(req, res, 5e3, async (body) => {
|
|
51980
52428
|
try {
|
|
51981
|
-
const { locationId: locationId2, clientKey, confirmName, revoke, repairHashes } = JSON.parse(body);
|
|
52429
|
+
const { locationId: locationId2, clientKey, confirmName, revoke, repairHashes, by } = JSON.parse(body);
|
|
52430
|
+
const approvedBy = typeof by === "string" && by.trim() ? { by: by.trim().slice(0, 80) } : {};
|
|
51982
52431
|
const who = cockpitClient(readCockpitState(), currentRoster(), locationId2, clientKey);
|
|
51983
52432
|
const rk = who.recordKey;
|
|
51984
52433
|
if (PROTECTED_LOCATIONS[who.locationId]) throw new Error(PROTECTED_LOCATIONS[who.locationId] + " can never be approved for building.");
|
|
51985
52434
|
const current = readCockpitState();
|
|
51986
52435
|
if (!revoke && !hasChosenScope(current, rk)) throw new Error(who.clientKey ? secondClientChooseMessage(who.clientKey, who.name) : CHOOSE_WHAT_THEY_BOUGHT);
|
|
52436
|
+
const assessmentWait = revoke ? null : assessmentGateRefusal(current, rk, who.clientKey, who.name);
|
|
52437
|
+
if (assessmentWait) throw new Error(assessmentWait);
|
|
51987
52438
|
if (!revoke && !readAccountRulesSafe().sandboxes[who.locationId] && String(confirmName || "").trim().toLowerCase() !== who.name.trim().toLowerCase()) {
|
|
51988
52439
|
throw new Error(`Type the client's name exactly ("${who.name}") to approve automated building.`);
|
|
51989
52440
|
}
|
|
@@ -51991,15 +52442,15 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
51991
52442
|
const approvals = { ...current.planApprovals };
|
|
51992
52443
|
if (revoke) delete approvals[rk];
|
|
51993
52444
|
else if (who.clientKey) {
|
|
51994
|
-
approvals[rk] = { inputHash: secondClientInputHash(readSavedPlan(rk)), at, repairHashes: [], staffHash: staffListHash(current.staffRoster?.[who.locationId]), scopeHash:
|
|
52445
|
+
approvals[rk] = { inputHash: secondClientInputHash(readSavedPlan(rk)), at, repairHashes: [], staffHash: staffListHash(current.staffRoster?.[who.locationId]), scopeHash: approvalScopeHash(current, rk), boughtHash: approvalBoughtHash(current, rk), ...approvedBy };
|
|
51995
52446
|
} else {
|
|
51996
52447
|
const c = clientFor(who.locationId);
|
|
51997
52448
|
if (!c) throw new Error(INTAKE_UNREADABLE);
|
|
51998
52449
|
const snap = await readIntakeSnapshot(c, who.locationId);
|
|
51999
|
-
if (snap.status === "unavailable") throw
|
|
52450
|
+
if (snap.status === "unavailable") throw intakeUnavailableError(snap);
|
|
52000
52451
|
const hash = intakeInput(normalizedIntake(snap), current.accounts?.[who.locationId]?.industry, composeQuestionSet({ industry: current.accounts?.[who.locationId]?.industry, agency: readOverlay() }));
|
|
52001
52452
|
if (current.outcomes?.[`${rk}:1`]?.inputHash && current.outcomes[`${rk}:1`].inputHash !== hash) throw new Error("The intake changed. Run Brief again before approving building.");
|
|
52002
|
-
const approval = { inputHash: hash, at, repairHashes: readWorkflowRepairs(who.locationId).filter((r) => Array.isArray(repairHashes) && repairHashes.includes(r.hash)).map((r) => r.hash), staffHash: staffListHash(current.staffRoster?.[who.locationId]), scopeHash:
|
|
52453
|
+
const approval = { inputHash: hash, at, repairHashes: readWorkflowRepairs(who.locationId).filter((r) => Array.isArray(repairHashes) && repairHashes.includes(r.hash)).map((r) => r.hash), staffHash: staffListHash(current.staffRoster?.[who.locationId]), scopeHash: approvalScopeHash(current, rk), boughtHash: approvalBoughtHash(current, rk), ...approvedBy };
|
|
52003
52454
|
approvals[rk] = approval;
|
|
52004
52455
|
}
|
|
52005
52456
|
const latest = readCockpitState();
|
|
@@ -52357,26 +52808,59 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
52357
52808
|
});
|
|
52358
52809
|
return;
|
|
52359
52810
|
}
|
|
52811
|
+
if (req.method === "POST" && url === "/api/assessment-choice") {
|
|
52812
|
+
readBody(req, res, 5e3, (body) => {
|
|
52813
|
+
try {
|
|
52814
|
+
const { locationId: locationId2, clientKey, choice } = JSON.parse(body);
|
|
52815
|
+
const who = cockpitClient(readCockpitState(), currentRoster(), locationId2, clientKey);
|
|
52816
|
+
if (!isAssessmentChoice(choice)) throw new Error("Choose one of the three answers.");
|
|
52817
|
+
const rk = who.recordKey;
|
|
52818
|
+
const current = readCockpitState();
|
|
52819
|
+
const { state: nextState, withdrawn } = withdrawIfBindingChanged({ ...current, assessmentChoice: { ...current.assessmentChoice, [rk]: { choice, at: (/* @__PURE__ */ new Date()).toISOString() } } }, rk);
|
|
52820
|
+
writeCockpitState(nextState);
|
|
52821
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
52822
|
+
res.end(JSON.stringify({ ok: true, choice, ...withdrawn ? { approvalWithdrawn: true, message: APPROVAL_WITHDRAWN_FOR_ASSESSMENT } : {} }));
|
|
52823
|
+
} catch (e) {
|
|
52824
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
52825
|
+
res.end(JSON.stringify({ ok: false, error: e instanceof Error ? e.message : "bad request" }));
|
|
52826
|
+
}
|
|
52827
|
+
});
|
|
52828
|
+
return;
|
|
52829
|
+
}
|
|
52360
52830
|
if (req.method === "POST" && url === "/api/scope") {
|
|
52361
52831
|
readBody(req, res, 1e4, (body) => {
|
|
52362
52832
|
try {
|
|
52363
|
-
const { locationId: locationId2, clientKey, package: pkg2, features } = JSON.parse(body);
|
|
52833
|
+
const { locationId: locationId2, clientKey, package: pkg2, features, bought } = JSON.parse(body);
|
|
52364
52834
|
const who = cockpitClient(readCockpitState(), currentRoster(), locationId2, clientKey);
|
|
52365
|
-
|
|
52366
|
-
|
|
52367
|
-
if (
|
|
52835
|
+
let ticks;
|
|
52836
|
+
let derived;
|
|
52837
|
+
if (bought !== void 0) {
|
|
52838
|
+
if (!Array.isArray(bought) || bought.some((b) => !isBoughtId(b))) throw new Error("Tick items from the list.");
|
|
52839
|
+
ticks = [...new Set(bought.map(String))];
|
|
52840
|
+
if (!ticks.length) throw new Error(NOTHING_BOUGHT);
|
|
52841
|
+
derived = scopeFromBought(ticks, offeredFeaturesFor(readCockpitState(), who.recordKey), typeof pkg2 === "string" && pkg2.trim() ? pkg2 : void 0);
|
|
52842
|
+
if (!("wholePreset" in derived) && !derived.features.length) throw new Error(NOTHING_BUILT_FROM_TICKS);
|
|
52843
|
+
} else if (features !== null && !Array.isArray(features)) throw new Error("features must be a list of feature ids, or null to confirm the whole preset");
|
|
52844
|
+
const next = derived ?? (features === null ? { wholePreset: true } : { ...typeof pkg2 === "string" ? { package: pkg2 } : {}, features: features.map(String) });
|
|
52845
|
+
if (!derived && Array.isArray(features)) {
|
|
52368
52846
|
const ticked = features.map(String).filter(isFeatureId);
|
|
52369
52847
|
const offered = offeredFeaturesFor(readCockpitState(), who.recordKey);
|
|
52370
52848
|
if (ticked.length && !ticked.some((f) => offered.includes(f))) throw new Error(NOTHING_TICKED_IN_PLAN);
|
|
52371
52849
|
}
|
|
52372
52850
|
const rk = who.recordKey;
|
|
52373
52851
|
let nextState = setClientScope(readCockpitState(), rk, next);
|
|
52374
|
-
const
|
|
52375
|
-
|
|
52376
|
-
|
|
52377
|
-
|
|
52378
|
-
|
|
52379
|
-
|
|
52852
|
+
const savedTicks = { ...nextState.bought ?? {} };
|
|
52853
|
+
if (ticks) savedTicks[rk] = ticks;
|
|
52854
|
+
else delete savedTicks[rk];
|
|
52855
|
+
nextState = { ...nextState, bought: savedTicks };
|
|
52856
|
+
if (!Object.keys(savedTicks).length) delete nextState.bought;
|
|
52857
|
+
const { state: bound, withdrawn } = withdrawIfBindingChanged(nextState, rk);
|
|
52858
|
+
nextState = bound;
|
|
52859
|
+
{
|
|
52860
|
+
const offered = offeredFeaturesFor(nextState, rk);
|
|
52861
|
+
replaceBuildObligationsOfType(rk, BOUGHT_JOB_TYPE, ticks ? boughtJobs(ticks, offered) : []);
|
|
52862
|
+
const tasks2 = withBoughtJobs(nextState.manualTasks?.[rk] ?? [], ticks ? boughtJobLines(ticks, offered) : []);
|
|
52863
|
+
if (tasks2.length || nextState.manualTasks?.[rk]) nextState = { ...nextState, manualTasks: { ...nextState.manualTasks, [rk]: tasks2 } };
|
|
52380
52864
|
}
|
|
52381
52865
|
writeCockpitState(nextState);
|
|
52382
52866
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -52435,7 +52919,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
52435
52919
|
const client = clientFor(locationId2);
|
|
52436
52920
|
if (!client) throw new Error(INTAKE_UNREADABLE);
|
|
52437
52921
|
const snap = await readIntakeSnapshot(client, locationId2);
|
|
52438
|
-
if (snap.status === "unavailable") throw
|
|
52922
|
+
if (snap.status === "unavailable") throw intakeUnavailableError(snap);
|
|
52439
52923
|
const answers = normalizedIntake(snap);
|
|
52440
52924
|
hash = intakeInput(answers, state.accounts?.[locationId2]?.industry, composeQuestionSet({ industry: state.accounts?.[locationId2]?.industry, agency: readOverlay() }));
|
|
52441
52925
|
state = { ...state, intakeInputs: { ...state.intakeInputs, [locationId2]: { answers, hash, status: snap.status } } };
|
|
@@ -52517,7 +53001,7 @@ ${opportunityNoteFailure?.detail ?? ""}`);
|
|
|
52517
53001
|
process.on("SIGTERM", stop2);
|
|
52518
53002
|
});
|
|
52519
53003
|
}
|
|
52520
|
-
var import_zod67, import_node_util5, fs42, path40, http, import_child_process4, import_child_process5, osmod, STAGES2, ACCOUNT_TYPES, SECOND_CLIENT_NO_INTAKE, CHOOSE_WHAT_THEY_BOUGHT, CHOOSE_BEFORE_BUILD, SCOPE_REFUSAL, NOTHING_TICKED_IN_PLAN, SCOPE_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_SCOPE, activeRun2, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, PARKED_NOT_BUILT, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_FORM_REQUIRED, INTAKE_CONNECT_REQUIRED, INTAKE_OWN_MESSAGES, currentRosterRef, staffGateRef, BOARD_ROUTES, BOARD_TITLES, SCREEN_HEADS, anthropicKeyWrites, SEAT_TOKEN_HEADER, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS, ConnectionRequest, DisconnectRequest, ReportBrandSaveRequest;
|
|
53004
|
+
var import_zod67, import_node_util5, fs42, path40, http, import_child_process4, import_child_process5, osmod, STAGES2, ACCOUNT_TYPES, SECOND_CLIENT_NO_INTAKE, CHOOSE_WHAT_THEY_BOUGHT, CHOOSE_BEFORE_BUILD, SCOPE_REFUSAL, NOTHING_BOUGHT, NOTHING_BUILT_FROM_TICKS, ASSESSMENT_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_ASSESSMENT, NOTHING_TICKED_IN_PLAN, SCOPE_CHANGED_SINCE_APPROVAL, APPROVAL_WITHDRAWN_FOR_SCOPE, activeRun2, handoffEmailPreviews, UPGRADE_MSG, DOC_EXTS, DOC_MAX_BYTES, PARKED_NOT_BUILT, NEEDS_AGENCY_KEY, INTAKE_UNREADABLE, INTAKE_READ_TRANSIENT, INTAKE_REFRESH_RETRYING, INTAKE_ANSWERS_MISSING, INTAKE_READ_FAILED, INTAKE_FORM_REQUIRED, INTAKE_CONNECT_REQUIRED, INTAKE_OWN_MESSAGES, currentRosterRef, staffGateRef, BOARD_ROUTES, BOARD_TITLES, SCREEN_HEADS, anthropicKeyWrites, SEAT_TOKEN_HEADER, NOT_SIGNED_IN_MSG, SEAT_GUARDED_READS, ConnectionRequest, DisconnectRequest, ReportBrandSaveRequest;
|
|
52521
53005
|
var init_dashboard = __esm({
|
|
52522
53006
|
"src/dashboard.ts"() {
|
|
52523
53007
|
"use strict";
|
|
@@ -52585,6 +53069,7 @@ var init_dashboard = __esm({
|
|
|
52585
53069
|
init_plan_from_preset();
|
|
52586
53070
|
init_plan_guide();
|
|
52587
53071
|
init_attachment();
|
|
53072
|
+
init_bought_list();
|
|
52588
53073
|
init_intake_contact();
|
|
52589
53074
|
init_client_intake();
|
|
52590
53075
|
init_client_intake();
|
|
@@ -52642,6 +53127,10 @@ var init_dashboard = __esm({
|
|
|
52642
53127
|
CHOOSE_WHAT_THEY_BOUGHT = "Choose what the client bought before approving: Blueprint Review, then What they bought, then Save.";
|
|
52643
53128
|
CHOOSE_BEFORE_BUILD = "Choose what the client bought before building: Blueprint Review, then What they bought, then Save.";
|
|
52644
53129
|
SCOPE_REFUSAL = /Nobody has chosen what|what this client bought|features (are )?selected for this client|No features are selected|features nobody ticked/i;
|
|
53130
|
+
NOTHING_BOUGHT = "Tick at least one item they bought, then save.";
|
|
53131
|
+
NOTHING_BUILT_FROM_TICKS = "None of the ticked items is built for this client's industry, so nothing was saved. Tick at least one item marked Built.";
|
|
53132
|
+
ASSESSMENT_CHANGED_SINCE_APPROVAL = "The Assessment answer or the attached Assessment changed after the build was approved, so Build did not start. Approve this plan again, then build.";
|
|
53133
|
+
APPROVAL_WITHDRAWN_FOR_ASSESSMENT = "The Assessment answer changed, so the build approval was withdrawn. Approve this plan again before building.";
|
|
52645
53134
|
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.";
|
|
52646
53135
|
SCOPE_CHANGED_SINCE_APPROVAL = "What they bought changed after the build was approved, so Build did not start. Approve this plan again, then build.";
|
|
52647
53136
|
APPROVAL_WITHDRAWN_FOR_SCOPE = "What they bought changed, so the build approval was withdrawn. Approve this plan again before building.";
|
|
@@ -52653,10 +53142,17 @@ var init_dashboard = __esm({
|
|
|
52653
53142
|
PARKED_NOT_BUILT = "Template and hidden accounts are not built from Command OS. Change the account's type to Client first.";
|
|
52654
53143
|
NEEDS_AGENCY_KEY = "No agency key on file. Run register_agency_key in GHL Command first.";
|
|
52655
53144
|
INTAKE_UNREADABLE = "Command OS could not read this client's account. Paste its key again in Build a client account.";
|
|
53145
|
+
INTAKE_READ_TRANSIENT = "Command OS could not reach this client's account just now. Try again in a moment.";
|
|
53146
|
+
INTAKE_REFRESH_RETRYING = "Could not refresh just now; retrying.";
|
|
53147
|
+
INTAKE_ANSWERS_MISSING = "The intake form shows a submission, but no contact in this client's account has the answers yet. If the client has not submitted the form, send it again. If they have, press Refresh status in a minute.";
|
|
53148
|
+
INTAKE_READ_FAILED = "The intake could not be read. Press Refresh status to try again.";
|
|
52656
53149
|
INTAKE_FORM_REQUIRED = "Run the intake step in Client builds to create the form, then come back and submit.";
|
|
52657
53150
|
INTAKE_CONNECT_REQUIRED = "Connect this sub-account by pasting its Private Integration key in Command OS.";
|
|
52658
53151
|
INTAKE_OWN_MESSAGES = [
|
|
52659
53152
|
INTAKE_UNREADABLE,
|
|
53153
|
+
INTAKE_READ_TRANSIENT,
|
|
53154
|
+
INTAKE_ANSWERS_MISSING,
|
|
53155
|
+
INTAKE_READ_FAILED,
|
|
52660
53156
|
...CLAUDE_REPAIR_MESSAGES,
|
|
52661
53157
|
"The limit is 60,000 characters. Paste an excerpt or shorten this text file.",
|
|
52662
53158
|
"That account is not on the board.",
|
|
@@ -53939,11 +54435,11 @@ function registerAccountExportTools(server2, client, registry2, builderClient) {
|
|
|
53939
54435
|
};
|
|
53940
54436
|
const dataA = await fetchWithOwnKey(locationA);
|
|
53941
54437
|
const dataB = await fetchWithOwnKey(locationB);
|
|
53942
|
-
const { comparison, unavailable } = buildLocationComparison(dataA, dataB, SECTIONS);
|
|
54438
|
+
const { comparison, unavailable: unavailable2 } = buildLocationComparison(dataA, dataB, SECTIONS);
|
|
53943
54439
|
diff.comparison = comparison;
|
|
53944
54440
|
if (usedRegisteredKey.length) diff.usedRegisteredKeyFor = usedRegisteredKey;
|
|
53945
|
-
if (
|
|
53946
|
-
diff.unavailable =
|
|
54441
|
+
if (unavailable2.length) {
|
|
54442
|
+
diff.unavailable = unavailable2;
|
|
53947
54443
|
diff.warning = "Some sections could not be read. Those sections are marked unavailable \u2014 do NOT read them as empty. A Private Integration key only works on the sub-account it was created in, so register each account's own key (register_location) before comparing them.";
|
|
53948
54444
|
}
|
|
53949
54445
|
return jsonResponse(diff);
|
|
@@ -54212,13 +54708,13 @@ function discoverAssets(input) {
|
|
|
54212
54708
|
return assets;
|
|
54213
54709
|
}
|
|
54214
54710
|
function buildLocationComparison(dataA, dataB, sections) {
|
|
54215
|
-
const
|
|
54711
|
+
const unavailable2 = [];
|
|
54216
54712
|
const comparison = {};
|
|
54217
54713
|
for (const key of sections) {
|
|
54218
54714
|
const a = dataA[key] ?? { error: "not read" };
|
|
54219
54715
|
const b = dataB[key] ?? { error: "not read" };
|
|
54220
|
-
if ("error" in a)
|
|
54221
|
-
if ("error" in b)
|
|
54716
|
+
if ("error" in a) unavailable2.push(`locationA.${key}: ${a.error}`);
|
|
54717
|
+
if ("error" in b) unavailable2.push(`locationB.${key}: ${b.error}`);
|
|
54222
54718
|
if ("error" in a || "error" in b) {
|
|
54223
54719
|
comparison[key] = {
|
|
54224
54720
|
A: "error" in a ? "unavailable" : a.names.length,
|
|
@@ -54238,7 +54734,7 @@ function buildLocationComparison(dataA, dataB, sections) {
|
|
|
54238
54734
|
onlyInB: b.names.filter((n) => !setA.has(n))
|
|
54239
54735
|
};
|
|
54240
54736
|
}
|
|
54241
|
-
return { comparison, unavailable };
|
|
54737
|
+
return { comparison, unavailable: unavailable2 };
|
|
54242
54738
|
}
|
|
54243
54739
|
var import_zod70, OFF_PLATFORM_ASSETS;
|
|
54244
54740
|
var init_account_export = __esm({
|
|
@@ -64417,8 +64913,8 @@ function registerAccountHealthTools(server2, client) {
|
|
|
64417
64913
|
conversations,
|
|
64418
64914
|
phoneNumbers
|
|
64419
64915
|
};
|
|
64420
|
-
const
|
|
64421
|
-
return { locationId: loc, requestedWindow: { days, start: startISO, end: endISO }, sections, unavailable };
|
|
64916
|
+
const unavailable2 = Object.entries(sections).filter(([, v]) => v.status === "unavailable").map(([k]) => k);
|
|
64917
|
+
return { locationId: loc, requestedWindow: { days, start: startISO, end: endISO }, sections, unavailable: unavailable2 };
|
|
64422
64918
|
}
|
|
64423
64919
|
);
|
|
64424
64920
|
}
|