@velaro/cli 1.4.102 → 1.4.104
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/README.md +19 -0
- package/bin/velaro.js +14 -0
- package/lib/api.js +2 -1
- package/lib/apple-pay-presets.js +66 -0
- package/lib/apple-pay-presets.test.js +61 -0
- package/lib/apple-pay.js +32 -0
- package/lib/apple-pay.test.js +32 -0
- package/lib/avatar-switch.js +42 -0
- package/lib/avatar-switch.test.js +49 -0
- package/lib/build-info.js +28 -0
- package/lib/build-info.test.js +24 -0
- package/lib/commands/apple-pay-preset.js +65 -0
- package/lib/commands/apple-pay.js +73 -0
- package/lib/commands/avatar.js +60 -2
- package/lib/commands/billing-automation.js +184 -0
- package/lib/commands/copilotstudio.js +14 -1
- package/lib/commands/enterprise-reports.js +1 -0
- package/lib/commands/entitlement.js +79 -1
- package/lib/commands/env.js +1 -1
- package/lib/commands/focus.js +1 -1
- package/lib/commands/index.js +38 -2
- package/lib/commands/logs-insights.js +342 -0
- package/lib/commands/logs.js +310 -11
- package/lib/commands/magento.js +16 -0
- package/lib/commands/mcp-key.js +7 -1
- package/lib/commands/netsuite.js +67 -2
- package/lib/commands/notifications.js +34 -1
- package/lib/commands/ops.js +23 -54
- package/lib/commands/report-backfill.js +23 -5
- package/lib/commands/report.js +77 -14
- package/lib/commands/salesforce.js +101 -0
- package/lib/commands/shopify.js +84 -0
- package/lib/commands/signature.js +79 -0
- package/lib/commands/site-builder.js +30 -0
- package/lib/commands/support.js +22 -0
- package/lib/commands/teams.js +11 -1
- package/lib/commands/workflow.js +157 -2
- package/lib/commands/zoom-phone.js +15 -0
- package/lib/enterprise-reports.test.js +1 -1
- package/lib/entitlement-report-packages.test.js +47 -0
- package/lib/logs-insights.test.js +146 -0
- package/lib/mcp-key.test.js +11 -0
- package/lib/messaging-routes.test.js +10 -1
- package/lib/oauth.js +14 -2
- package/lib/report-range.js +21 -0
- package/lib/report-range.test.js +38 -0
- package/lib/shopify-write-policy.test.js +34 -0
- package/lib/signature-policy.js +70 -0
- package/lib/signature-policy.test.js +48 -0
- package/package.json +1 -1
package/lib/commands/ops.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describeRawMeta, fetchRetry429 } from '../log-search.js';
|
|
2
2
|
import { get, del, post, getCredentials } from '../api.js';
|
|
3
3
|
import { readConfig, ENVS } from '../config.js';
|
|
4
|
+
import { parseBuild, formatBuild } from '../build-info.js';
|
|
4
5
|
import { collectAll, render as renderDeployState } from '../deploy-state.js';
|
|
5
6
|
|
|
6
7
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -27,12 +28,6 @@ function extractLogEntries(response) {
|
|
|
27
28
|
return [];
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
function utcLabel(dateStr) {
|
|
31
|
-
if (!dateStr) return '';
|
|
32
|
-
const d = new Date(dateStr);
|
|
33
|
-
return d.toISOString().replace('T', ' ').slice(0, 16) + ' UTC';
|
|
34
|
-
}
|
|
35
|
-
|
|
36
31
|
const OK = '\x1b[32m✓\x1b[0m';
|
|
37
32
|
const WARN = '\x1b[33m⚠\x1b[0m';
|
|
38
33
|
const ERR = '\x1b[31m✗\x1b[0m';
|
|
@@ -46,21 +41,15 @@ function severityPrefix(s) {
|
|
|
46
41
|
return LOW;
|
|
47
42
|
}
|
|
48
43
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (!res.ok) return { label, ok: false, status: res.status };
|
|
53
|
-
const data = await res.json();
|
|
54
|
-
return { label, ok: true, version: data.version, deployedAt: data.deployedAt };
|
|
55
|
-
} catch (e) {
|
|
56
|
-
return { label, ok: false, err: e.message };
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
44
|
+
// Reads the real running build from /api/health (build.commitId|sha, builtAt). The old /Version
|
|
45
|
+
// endpoint returned a stale CI-stamped version/deployedAt, so workers polling for a new staging
|
|
46
|
+
// build never saw it land (2026-10-12).
|
|
60
47
|
async function pingHealth(label, url) {
|
|
61
48
|
try {
|
|
62
49
|
const res = await fetch(url, { signal: AbortSignal.timeout(6000) });
|
|
63
|
-
|
|
50
|
+
let build = null;
|
|
51
|
+
try { build = parseBuild(await res.json()); } catch { /* non-JSON health body */ }
|
|
52
|
+
return { label, ok: res.ok, status: res.status, build };
|
|
64
53
|
} catch (e) {
|
|
65
54
|
return { label, ok: false, err: e.message };
|
|
66
55
|
}
|
|
@@ -133,31 +122,32 @@ export const opsCommand = {
|
|
|
133
122
|
|
|
134
123
|
const cfg = readConfig();
|
|
135
124
|
|
|
136
|
-
// ── 1. Deploy
|
|
125
|
+
// ── 1+2. Deploy (real running build) + API health, all four services ────
|
|
137
126
|
const prodBase = cfg.envs?.prod?.adminApiBase ?? ENVS.prod.adminApiBase;
|
|
138
127
|
const stagingBase = cfg.envs?.staging?.adminApiBase ?? ENVS.staging.adminApiBase;
|
|
128
|
+
const prodMsgBase = cfg.envs?.prod?.messagingApiBase ?? ENVS.prod.messagingApiBase;
|
|
129
|
+
const stagingMsgBase = cfg.envs?.staging?.messagingApiBase ?? ENVS.staging.messagingApiBase;
|
|
139
130
|
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
131
|
+
const services = await Promise.all([
|
|
132
|
+
pingHealth('admin/prod', `${prodBase}/api/health`),
|
|
133
|
+
pingHealth('admin/staging', `${stagingBase}/api/health`),
|
|
134
|
+
pingHealth('msg/prod', `${prodMsgBase}/api/health`),
|
|
135
|
+
pingHealth('msg/staging', `${stagingMsgBase}/api/health`),
|
|
143
136
|
]);
|
|
144
137
|
|
|
145
|
-
console.log('\n\x1b[1m── Deploy
|
|
146
|
-
for (const
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
const ver = `\x1b[36m${v.version}\x1b[0m`;
|
|
150
|
-
const time = `${ago(v.deployedAt).padEnd(12)} (${utcLabel(v.deployedAt)})`;
|
|
151
|
-
console.log(` ${sym} ${v.label.padEnd(8)} ${ver.padEnd(30)} deployed ${time}`);
|
|
138
|
+
console.log('\n\x1b[1m── Deploy / API Health ─────────────────────────────────────\x1b[0m');
|
|
139
|
+
for (const h of services) {
|
|
140
|
+
if (h.ok) {
|
|
141
|
+
console.log(` ${OK} ${h.label.padEnd(14)} ${formatBuild(h.build)}`);
|
|
152
142
|
} else {
|
|
153
|
-
const detail =
|
|
154
|
-
console.log(` ${ERR} ${
|
|
143
|
+
const detail = h.err ?? `HTTP ${h.status}`;
|
|
144
|
+
console.log(` ${ERR} ${h.label.padEnd(14)} ${h.build ? formatBuild(h.build) + ' ' : ''}UNHEALTHY — ${detail}`);
|
|
155
145
|
}
|
|
156
146
|
}
|
|
157
147
|
|
|
158
148
|
// ── 1b. Where each push landed: staging environment, staging slot 1 of production, production ──
|
|
159
149
|
// Same data as `velaro deploy-state` (needs gh). Never blocks the rest of ops if gh is missing.
|
|
160
|
-
console.log('\n
|
|
150
|
+
console.log('\n[1m── Where each push landed ──────────────────────────────────[0m');
|
|
161
151
|
try {
|
|
162
152
|
const state = await collectAll();
|
|
163
153
|
console.log(renderDeployState(state, { compact: true }).split('\n').map(l => ' ' + l).join('\n'));
|
|
@@ -166,27 +156,6 @@ export const opsCommand = {
|
|
|
166
156
|
console.log(` ${WARN} Deploy state unavailable (${e.message}). Needs gh logged in; run velaro deploy-state.`);
|
|
167
157
|
}
|
|
168
158
|
|
|
169
|
-
// ── 2. API health (admin + messaging, both envs) ──────────────────────────
|
|
170
|
-
const prodMsgBase = cfg.envs?.prod?.messagingApiBase ?? ENVS.prod.messagingApiBase;
|
|
171
|
-
const stagingMsgBase = cfg.envs?.staging?.messagingApiBase ?? ENVS.staging.messagingApiBase;
|
|
172
|
-
|
|
173
|
-
const [adminProdHealth, adminStagingHealth, msgProdHealth, msgStagingHealth] = await Promise.all([
|
|
174
|
-
pingHealth('admin/prod', `${prodBase}/Status`),
|
|
175
|
-
pingHealth('admin/staging', `${stagingBase}/Status`),
|
|
176
|
-
pingHealth('msg/prod', `${prodMsgBase}/api/health`),
|
|
177
|
-
pingHealth('msg/staging', `${stagingMsgBase}/api/health`),
|
|
178
|
-
]);
|
|
179
|
-
|
|
180
|
-
console.log('\n\x1b[1m── API Health ──────────────────────────────────────────────\x1b[0m');
|
|
181
|
-
for (const h of [adminProdHealth, adminStagingHealth, msgProdHealth, msgStagingHealth]) {
|
|
182
|
-
if (h.ok) {
|
|
183
|
-
console.log(` ${OK} ${h.label}`);
|
|
184
|
-
} else {
|
|
185
|
-
const detail = h.err ?? `HTTP ${h.status}`;
|
|
186
|
-
console.log(` ${ERR} ${h.label.padEnd(20)} ${detail}`);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
159
|
// ── 3. Hangfire failed jobs ───────────────────────────────────────────────
|
|
191
160
|
console.log('\n\x1b[1m── Hangfire ────────────────────────────────────────────────\x1b[0m');
|
|
192
161
|
try {
|
|
@@ -246,7 +215,7 @@ export const opsCommand = {
|
|
|
246
215
|
// ── 5. Recent errors + perf + AI routing (last 1h) ───────────────────────
|
|
247
216
|
console.log('\n\x1b[1m── Recent Issues (last 1h) ─────────────────────────────────\x1b[0m');
|
|
248
217
|
try {
|
|
249
|
-
const stagingMsgBaseLocal =
|
|
218
|
+
const stagingMsgBaseLocal = stagingMsgBase;
|
|
250
219
|
const from1h = new Date(Date.now() - 3600000).toISOString();
|
|
251
220
|
|
|
252
221
|
// One search at a time: the server runs at most 2 exact scans per instance. 429 (gate full) is retried honouring Retry-After.
|
|
@@ -1,22 +1,40 @@
|
|
|
1
|
-
import { messagingPost } from '../api.js';
|
|
1
|
+
import { messagingGet, messagingPost } from '../api.js';
|
|
2
2
|
import { runCommand } from '../run.js';
|
|
3
3
|
|
|
4
4
|
// -- velaro report-backfill <type> --from --to [--apply] ---------------------------
|
|
5
5
|
// Backend: velaro-messaging POST /Reports/backfill/{type}?from&to&apply (site managers; site from auth).
|
|
6
6
|
// Preview by default ("would fill N"); --apply writes. Idempotent. Type "csat" fills survey ratings only.
|
|
7
|
+
// Type "csat-demo" adds tagged demo CSAT assigned to the demo agents (allow-listed demo sites only, e.g. 100050);
|
|
8
|
+
// "csat-demo-undo" with the SAME --from/--to deletes exactly the tagged rows. Example:
|
|
9
|
+
// velaro report-backfill csat-demo --from 2026-08-01 --to 2026-10-06 [--apply]
|
|
10
|
+
// velaro report-backfill csat-demo-undo --from 2026-08-01 --to 2026-10-06 [--apply]
|
|
11
|
+
// history-classification(-sample) --apply is QUEUED (HTTP 202 + runId): the run happens in the background on the server;
|
|
12
|
+
// read progress/result with --status (GET /Reports/backfill/{type}/status). csat --apply stays synchronous (200).
|
|
7
13
|
export const reportBackfillCommand = {
|
|
8
14
|
command: 'report-backfill <type>',
|
|
9
|
-
describe: 'Backfill report data for your site (preview by default; --apply to write).
|
|
15
|
+
describe: 'Backfill report data for your site (preview by default; --apply to write). Types: csat, csat-demo, csat-demo-undo, history-classification-sample (propose tags from 20 recent chats), history-classification (tag and score past chats, 100 per run)',
|
|
10
16
|
builder: (y) => y
|
|
11
|
-
.positional('type', { type: 'string', describe: 'Backfill type
|
|
12
|
-
.option('from', { type: 'string',
|
|
13
|
-
.option('to', { type: 'string',
|
|
17
|
+
.positional('type', { type: 'string', describe: 'Backfill type: csat | csat-demo | csat-demo-undo | history-classification-sample | history-classification' })
|
|
18
|
+
.option('from', { type: 'string', describe: 'yyyy-MM-dd (required unless --status)' })
|
|
19
|
+
.option('to', { type: 'string', describe: 'yyyy-MM-dd (required unless --status)' })
|
|
20
|
+
.option('status', { type: 'boolean', default: false, describe: 'Show the latest queued/running/finished apply run for this type (history-classification types)' })
|
|
14
21
|
.option('apply', { type: 'boolean', default: false, describe: 'Write the changes (default: preview only)' })
|
|
15
22
|
.option('json', { type: 'boolean', default: false, describe: 'Raw JSON output' }),
|
|
16
23
|
handler: runCommand(async (argv) => {
|
|
24
|
+
if (argv.status) {
|
|
25
|
+
const s = await messagingGet(`/Reports/backfill/${encodeURIComponent(argv.type)}/status`);
|
|
26
|
+
console.log(JSON.stringify(s, null, 2));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!argv.from || !argv.to) throw new Error('--from and --to are required (yyyy-MM-dd) unless --status');
|
|
17
30
|
const qs = new URLSearchParams({ from: argv.from, to: argv.to, apply: String(!!argv.apply) });
|
|
18
31
|
const r = await messagingPost(`/Reports/backfill/${encodeURIComponent(argv.type)}?${qs}`);
|
|
19
32
|
if (argv.json) { console.log(JSON.stringify(r, null, 2)); return; }
|
|
33
|
+
if (argv.apply && r && r.runId) {
|
|
34
|
+
console.log(r.alreadyRunning ? `\nA run is already ${r.status} for this site (run ${r.runId}). Not starting another.` : `\nBackfill QUEUED (run ${r.runId}). It runs in the background; check progress with: velaro report-backfill ${argv.type} --status`);
|
|
35
|
+
console.log(JSON.stringify(r, null, 2));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
20
38
|
console.log(argv.apply ? '\nBackfill APPLIED' : '\nBackfill PREVIEW (nothing written; add --apply to write)');
|
|
21
39
|
console.log(JSON.stringify(r, null, 2));
|
|
22
40
|
}),
|
package/lib/commands/report.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writeFileSync } from 'fs';
|
|
2
2
|
import { getCredentials, request, messagingRequest } from '../api.js';
|
|
3
3
|
import { runCommand } from '../run.js';
|
|
4
|
+
import { toExclusiveEnd, exclusiveEndParam } from '../report-range.js';
|
|
4
5
|
|
|
5
6
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
6
7
|
|
|
@@ -33,7 +34,7 @@ function section(title) {
|
|
|
33
34
|
function dateParams(argv) {
|
|
34
35
|
const params = [];
|
|
35
36
|
if (argv.start) params.push(`start=${encodeURIComponent(argv.start)}`);
|
|
36
|
-
if (argv.end) params.push(`end=${encodeURIComponent(argv.end)}`);
|
|
37
|
+
if (argv.end) params.push(`end=${encodeURIComponent(exclusiveEndParam(argv.end))}`);
|
|
37
38
|
return params.length ? `?${params.join('&')}` : '';
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -66,12 +67,12 @@ const callcenterTeamsCommand = {
|
|
|
66
67
|
describe: 'Team breakdown for the call center report',
|
|
67
68
|
builder: (y) => y
|
|
68
69
|
.option('start', { type: 'string', describe: 'Start date (YYYY-MM-DD)' })
|
|
69
|
-
.option('end', { type: 'string', describe: 'End date (YYYY-MM-DD)' })
|
|
70
|
+
.option('end', { type: 'string', describe: 'End date (YYYY-MM-DD; includes that whole day)' })
|
|
70
71
|
.option('sla', { type: 'number', describe: 'SLA threshold in seconds (overrides server default)' }),
|
|
71
72
|
handler: async (argv) => {
|
|
72
73
|
const params = [];
|
|
73
74
|
if (argv.start) params.push(`start=${encodeURIComponent(argv.start)}`);
|
|
74
|
-
if (argv.end) params.push(`end=${encodeURIComponent(argv.end)}`);
|
|
75
|
+
if (argv.end) params.push(`end=${encodeURIComponent(exclusiveEndParam(argv.end))}`);
|
|
75
76
|
if (argv.sla) params.push(`slaSeconds=${argv.sla}`);
|
|
76
77
|
const qs = params.length ? `?${params.join('&')}` : '';
|
|
77
78
|
const data = await fetchReport(`/CallCenterAnalytics/TeamBreakdown${qs}`);
|
|
@@ -106,7 +107,7 @@ const callcenterChannelsCommand = {
|
|
|
106
107
|
describe: 'Channel breakdown for the call center report',
|
|
107
108
|
builder: (y) => y
|
|
108
109
|
.option('start', { type: 'string', describe: 'Start date (YYYY-MM-DD)' })
|
|
109
|
-
.option('end', { type: 'string', describe: 'End date (YYYY-MM-DD)' }),
|
|
110
|
+
.option('end', { type: 'string', describe: 'End date (YYYY-MM-DD; includes that whole day)' }),
|
|
110
111
|
handler: async (argv) => {
|
|
111
112
|
const data = await fetchReport(`/CallCenterAnalytics/ChannelBreakdown${dateParams(argv)}`);
|
|
112
113
|
const channels = Array.isArray(data) ? data : (data?.channels ?? []);
|
|
@@ -141,7 +142,7 @@ const callcenterCommand = {
|
|
|
141
142
|
.command(callcenterTeamsCommand)
|
|
142
143
|
.command(callcenterChannelsCommand)
|
|
143
144
|
.option('start', { type: 'string', describe: 'Start date (YYYY-MM-DD)' })
|
|
144
|
-
.option('end', { type: 'string', describe: 'End date (YYYY-MM-DD)' }),
|
|
145
|
+
.option('end', { type: 'string', describe: 'End date (YYYY-MM-DD; includes that whole day)' }),
|
|
145
146
|
handler: async (argv) => {
|
|
146
147
|
const data = await fetchReport(`/CallCenterAnalytics/Report${dateParams(argv)}`);
|
|
147
148
|
|
|
@@ -197,12 +198,12 @@ const servicelevelCommand = {
|
|
|
197
198
|
describe: 'Service level compliance report over time',
|
|
198
199
|
builder: (y) => y
|
|
199
200
|
.option('start', { type: 'string', describe: 'Start date (YYYY-MM-DD)' })
|
|
200
|
-
.option('end', { type: 'string', describe: 'End date (YYYY-MM-DD)' })
|
|
201
|
+
.option('end', { type: 'string', describe: 'End date (YYYY-MM-DD; includes that whole day)' })
|
|
201
202
|
.option('granularity', { type: 'string', choices: ['hour', 'day', 'week', 'month'], default: 'day', describe: 'Time bucket size' }),
|
|
202
203
|
handler: async (argv) => {
|
|
203
204
|
const params = [];
|
|
204
|
-
if (argv.start) params.push(`
|
|
205
|
-
if (argv.end) params.push(`
|
|
205
|
+
if (argv.start) params.push(`startDate=${encodeURIComponent(argv.start)}`);
|
|
206
|
+
if (argv.end) params.push(`endDate=${encodeURIComponent(exclusiveEndParam(argv.end))}`);
|
|
206
207
|
if (argv.granularity) params.push(`granularity=${argv.granularity}`);
|
|
207
208
|
const qs = params.length ? `?${params.join('&')}` : '';
|
|
208
209
|
const data = await fetchReport(`/ServiceLevel${qs}`);
|
|
@@ -249,17 +250,17 @@ const conversationsCommand = {
|
|
|
249
250
|
describe: 'Conversation volume report over time',
|
|
250
251
|
builder: (y) => y
|
|
251
252
|
.option('start', { type: 'string', describe: 'Start date (YYYY-MM-DD)' })
|
|
252
|
-
.option('end', { type: 'string', describe: 'End date (YYYY-MM-DD)' })
|
|
253
|
+
.option('end', { type: 'string', describe: 'End date (YYYY-MM-DD; includes that whole day)' })
|
|
253
254
|
.option('granularity', { type: 'string', choices: ['hour', 'day', 'week', 'month'], default: 'day', describe: 'Time bucket size' })
|
|
254
255
|
.option('channel', { type: 'string', describe: 'Filter by channel (e.g. Web, TwilioSms, WhatsApp)' }),
|
|
255
256
|
handler: async (argv) => {
|
|
256
257
|
const params = [];
|
|
257
|
-
if (argv.start) params.push(`
|
|
258
|
-
if (argv.end) params.push(`
|
|
258
|
+
if (argv.start) params.push(`startDate=${encodeURIComponent(argv.start)}`);
|
|
259
|
+
if (argv.end) params.push(`endDate=${encodeURIComponent(exclusiveEndParam(argv.end))}`);
|
|
259
260
|
if (argv.granularity) params.push(`granularity=${argv.granularity}`);
|
|
260
261
|
if (argv.channel) params.push(`channel=${encodeURIComponent(argv.channel)}`);
|
|
261
262
|
const qs = params.length ? `?${params.join('&')}` : '';
|
|
262
|
-
const data = await fetchReport(`/
|
|
263
|
+
const data = await fetchReport(`/ConversationsReport${qs}`);
|
|
263
264
|
|
|
264
265
|
section('Conversations Report');
|
|
265
266
|
|
|
@@ -384,7 +385,7 @@ const workedHoursCommand = {
|
|
|
384
385
|
describe: `Worked hours breakdown report: site KPIs + per-agent/per-day detail (Time Tracking, max ${MAX_WORKED_HOURS_RANGE_DAYS}-day range per query)`,
|
|
385
386
|
builder: (y) => y
|
|
386
387
|
.option('start', { type: 'string', demandOption: true, describe: 'Start date (ISO 8601 or YYYY-MM-DD)' })
|
|
387
|
-
.option('end', { type: 'string', demandOption: true, describe: `End date (ISO 8601 or YYYY-MM-DD) -- must be at most ${MAX_WORKED_HOURS_RANGE_DAYS} days after --start` }),
|
|
388
|
+
.option('end', { type: 'string', demandOption: true, describe: `End date (ISO 8601 or YYYY-MM-DD; a plain date includes that whole day) -- must be at most ${MAX_WORKED_HOURS_RANGE_DAYS} days after --start` }),
|
|
388
389
|
handler: async (argv) => {
|
|
389
390
|
const start = new Date(argv.start);
|
|
390
391
|
const end = new Date(argv.end);
|
|
@@ -400,7 +401,7 @@ const workedHoursCommand = {
|
|
|
400
401
|
|
|
401
402
|
let data;
|
|
402
403
|
try {
|
|
403
|
-
data = await messagingRequest('POST', '/Reports/worked-hours-breakdown', { Start: start.toISOString(), End: end.toISOString() });
|
|
404
|
+
data = await messagingRequest('POST', '/Reports/worked-hours-breakdown', { Start: start.toISOString(), End: toExclusiveEnd(argv.end, end).toISOString() });
|
|
404
405
|
} catch (err) {
|
|
405
406
|
if (/403/.test(err.message)) {
|
|
406
407
|
console.error('Time Tracking is not enabled for this account.');
|
|
@@ -678,6 +679,67 @@ const exportCommand = {
|
|
|
678
679
|
handler: () => {},
|
|
679
680
|
};
|
|
680
681
|
|
|
682
|
+
// ── country performance ───────────────────────────────────────────────────────
|
|
683
|
+
|
|
684
|
+
const countryGetCommand = {
|
|
685
|
+
command: 'get',
|
|
686
|
+
describe: 'Performance by Country (US / Canada / Europe / Other)',
|
|
687
|
+
builder: (y) => y
|
|
688
|
+
.option('start', { type: 'string', describe: 'Start date (YYYY-MM-DD), default 30 days ago' })
|
|
689
|
+
.option('end', { type: 'string', describe: 'End date (YYYY-MM-DD), default now' })
|
|
690
|
+
.option('attribution', { type: 'string', choices: ['visitor', 'agent', 'group'], default: 'visitor', describe: 'visitor country, agent profile country, or assigned group' })
|
|
691
|
+
.option('channel', { type: 'string', describe: 'Limit to one channel, e.g. Web or TwilioSms' })
|
|
692
|
+
.option('json', { type: 'boolean', describe: 'Print raw JSON' }),
|
|
693
|
+
handler: runCommand(async (argv) => {
|
|
694
|
+
const end = argv.end ? new Date(argv.end) : new Date();
|
|
695
|
+
const start = argv.start ? new Date(argv.start) : new Date(end.getTime() - 30 * 86400000);
|
|
696
|
+
const r = await messagingRequest('POST', '/reports/performance-by-country/query', {
|
|
697
|
+
start: start.toISOString(), end: end.toISOString(), attribution: argv.attribution,
|
|
698
|
+
channels: argv.channel ? [argv.channel] : [],
|
|
699
|
+
});
|
|
700
|
+
if (argv.json) { console.log(JSON.stringify(r, null, 2)); return; }
|
|
701
|
+
section(`Performance by Country (${r.attribution})`);
|
|
702
|
+
for (const b of r.buckets ?? []) {
|
|
703
|
+
fmtRow(b.bucket, `eng ${b.engagements} handled ${b.handled} missed ${b.missed} abandoned ${b.abandoned} csat ${fmtNum(b.csatAvg)} (${b.csatCount})`);
|
|
704
|
+
}
|
|
705
|
+
section('By agent');
|
|
706
|
+
for (const a of r.agents ?? []) {
|
|
707
|
+
fmtRow(a.agentName ?? '', `${a.bucket} eng ${a.engagements} handled ${a.handled} csat ${fmtNum(a.csatAvg)}${a.agentCountryNotSet ? ' [country not set]' : ''}`);
|
|
708
|
+
}
|
|
709
|
+
}),
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
const countryGroupsCommand = {
|
|
713
|
+
command: 'groups',
|
|
714
|
+
describe: 'Show the site groups and their Performance by Country bucket; set mappings with --set <teamId>=<label>',
|
|
715
|
+
builder: (y) => y.option('set', { type: 'array', describe: 'teamId=label (repeatable). REPLACES the whole mapping.' }),
|
|
716
|
+
handler: runCommand(async (argv) => {
|
|
717
|
+
const groups = await messagingRequest('GET', '/reports/performance-by-country/groups');
|
|
718
|
+
let current;
|
|
719
|
+
if (argv.set && argv.set.length) {
|
|
720
|
+
const map = {};
|
|
721
|
+
for (const pair of argv.set) {
|
|
722
|
+
const i = String(pair).indexOf('=');
|
|
723
|
+
if (i < 1) throw new Error(`--set expects teamId=label, got "${pair}"`);
|
|
724
|
+
map[String(pair).slice(0, i)] = String(pair).slice(i + 1);
|
|
725
|
+
}
|
|
726
|
+
current = await messagingRequest('PUT', '/reports/performance-by-country/group-map', { map });
|
|
727
|
+
} else {
|
|
728
|
+
current = await messagingRequest('GET', '/reports/performance-by-country/group-map');
|
|
729
|
+
}
|
|
730
|
+
const map = current?.map ?? {};
|
|
731
|
+
section('Groups');
|
|
732
|
+
for (const g of Array.isArray(groups) ? groups : []) fmtRow(`${g.id} ${g.name}`, map[g.id] ?? '(unmapped: Other)');
|
|
733
|
+
}),
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
const countryCommand = {
|
|
737
|
+
command: 'country',
|
|
738
|
+
describe: 'Performance by Country report and group-to-bucket mapping',
|
|
739
|
+
builder: (y) => y.command(countryGetCommand).command(countryGroupsCommand).demandCommand(1, 'Specify: get | groups'),
|
|
740
|
+
handler: () => {},
|
|
741
|
+
};
|
|
742
|
+
|
|
681
743
|
// ── top-level report command ──────────────────────────────────────────────────
|
|
682
744
|
|
|
683
745
|
export const reportCommand = {
|
|
@@ -692,6 +754,7 @@ export const reportCommand = {
|
|
|
692
754
|
.command(visitorTrafficCommand)
|
|
693
755
|
.command(schedulesCommand)
|
|
694
756
|
.command(exportCommand)
|
|
757
|
+
.command(countryCommand)
|
|
695
758
|
.demandCommand(1, 'Specify a report type. Run "velaro report --help" for options.'),
|
|
696
759
|
handler: () => {},
|
|
697
760
|
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* velaro salesforce — Salesforce integration diagnostics (OAuth, per-object read probes)
|
|
3
|
+
*
|
|
4
|
+
* velaro salesforce diagnose --site-id <id> [--quick] Live read-only diagnostic: config, auth, per-capability probes
|
|
5
|
+
* velaro salesforce matrix --site-id <id> Capability matrix vs live probes (same data as diagnose)
|
|
6
|
+
*
|
|
7
|
+
* Calls messaging DiagController GET /diagnostics/test-salesforce?siteId=<id>&deep=<bool> (Velaro staff only). Read-only:
|
|
8
|
+
* nothing is created or changed in Salesforce, and a write capability is never reported as verified.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { messagingRequest } from '../api.js';
|
|
12
|
+
|
|
13
|
+
const GRN = '\x1b[32m';
|
|
14
|
+
const RED = '\x1b[31m';
|
|
15
|
+
const YEL = '\x1b[33m';
|
|
16
|
+
const DIM = '\x1b[2m';
|
|
17
|
+
const BOLD = '\x1b[1m';
|
|
18
|
+
const RST = '\x1b[0m';
|
|
19
|
+
|
|
20
|
+
const ICON = { pass: `${GRN}✓${RST}`, warn: `${YEL}!${RST}`, fail: `${RED}✗${RST}`, skip: `${DIM}-${RST}` };
|
|
21
|
+
|
|
22
|
+
async function fetchReport(argv, deep) {
|
|
23
|
+
const siteId = argv['site-id'];
|
|
24
|
+
if (!siteId) {
|
|
25
|
+
console.error(`${RED}✗ --site-id is required${RST}`);
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const r = await messagingRequest('GET', `/diagnostics/test-salesforce?siteId=${encodeURIComponent(siteId)}&deep=${deep ? 'true' : 'false'}`);
|
|
31
|
+
return r?.report ?? r;
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.error(`\n${RED}✗ Diagnostic failed: ${e.message}${RST}\n`);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function printSteps(report) {
|
|
40
|
+
console.log(`\n${BOLD}Salesforce diagnostic — site ${report.siteId ?? report.SiteId}${RST} ${DIM}${report.product} / ${report.target}${RST}`);
|
|
41
|
+
console.log(` overall: ${String(report.overall).toUpperCase()}\n`);
|
|
42
|
+
for (const s of report.steps ?? []) {
|
|
43
|
+
const ms = s.latencyMs != null ? `${DIM} (${s.latencyMs}ms)${RST}` : '';
|
|
44
|
+
console.log(` ${ICON[s.status] ?? '?'} ${s.name}${ms}${s.detail ? ` ${DIM}${s.detail}${RST}` : ''}`);
|
|
45
|
+
if (s.hint) console.log(` ${DIM}→ ${s.hint}${RST}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function printCapabilities(report) {
|
|
50
|
+
const caps = report.capabilities ?? [];
|
|
51
|
+
if (!caps.length) return;
|
|
52
|
+
console.log(`\n${BOLD}Capabilities${RST} ${DIM}(package = skill tier; live = read-only SOQL probe now; writes are never probed)${RST}\n`);
|
|
53
|
+
for (const c of caps) {
|
|
54
|
+
const live = c.liveOk === true ? `${GRN}live ok${RST}` : c.liveOk === false ? `${RED}live FAIL${RST}` : `${DIM}not probed${RST}`;
|
|
55
|
+
const proven = c.liveVerified ? `${GRN}verified${RST}` : `${DIM}unverified${RST}`;
|
|
56
|
+
console.log(` ${c.capability.padEnd(38)} ${String(c.package).padEnd(9)} ${String(c.documented).padEnd(6)} ${live} ${proven}`);
|
|
57
|
+
if (c.liveOk === false && c.hint) console.log(` ${DIM}→ ${c.hint}${RST}`);
|
|
58
|
+
}
|
|
59
|
+
console.log('');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function diagnoseHandler(argv) {
|
|
63
|
+
const report = await fetchReport(argv, !argv.quick);
|
|
64
|
+
if (!report) return;
|
|
65
|
+
if (argv.json) { console.log(JSON.stringify(report, null, 2)); return; }
|
|
66
|
+
printSteps(report);
|
|
67
|
+
printCapabilities(report);
|
|
68
|
+
if (report.overall === 'failed' || report.overall === 'not_configured') process.exitCode = 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function matrixHandler(argv) {
|
|
72
|
+
const report = await fetchReport(argv, true);
|
|
73
|
+
if (!report) return;
|
|
74
|
+
if (argv.json) { console.log(JSON.stringify(report.capabilities ?? [], null, 2)); return; }
|
|
75
|
+
printCapabilities(report);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const salesforceCommand = {
|
|
79
|
+
command: 'salesforce <subcommand>',
|
|
80
|
+
describe: 'Salesforce integration diagnostics (token, per-object read probes)',
|
|
81
|
+
builder: y => y
|
|
82
|
+
.command({
|
|
83
|
+
command: 'diagnose',
|
|
84
|
+
describe: 'Live read-only diagnostic: configuration, token, per-object probes',
|
|
85
|
+
builder: yy => yy
|
|
86
|
+
.option('site-id', { describe: 'Customer site id', type: 'number' })
|
|
87
|
+
.option('quick', { describe: 'Skip per-capability probes', type: 'boolean', default: false })
|
|
88
|
+
.option('json', { describe: 'Raw JSON output', type: 'boolean', default: false }),
|
|
89
|
+
handler: diagnoseHandler,
|
|
90
|
+
})
|
|
91
|
+
.command({
|
|
92
|
+
command: 'matrix',
|
|
93
|
+
describe: 'Capability matrix: package tier and what a live read probe proves',
|
|
94
|
+
builder: yy => yy
|
|
95
|
+
.option('site-id', { describe: 'Customer site id', type: 'number' })
|
|
96
|
+
.option('json', { describe: 'Raw JSON output', type: 'boolean', default: false }),
|
|
97
|
+
handler: matrixHandler,
|
|
98
|
+
})
|
|
99
|
+
.demandCommand(1),
|
|
100
|
+
handler: () => {},
|
|
101
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* velaro shopify - Shopify write-tier policy (velaro-messaging ShopifyIntegrationsController)
|
|
3
|
+
*
|
|
4
|
+
* velaro shopify write-policy get
|
|
5
|
+
* velaro shopify write-policy set [--enable-shopify-write|--no-enable-shopify-write]
|
|
6
|
+
* [--require-confirmation|--no-require-confirmation]
|
|
7
|
+
* [--max-refund-amount N|default] [--max-discount-percent N|default]
|
|
8
|
+
* [--max-discount-fixed-amount N|default]
|
|
9
|
+
*
|
|
10
|
+
* Backend contract: GET/PUT /api/Shopify/WritePolicy (velaro-messaging). Site is resolved
|
|
11
|
+
* server-side from the auth context, never caller-supplied. Partial update: omitted flags keep
|
|
12
|
+
* their stored value; the word "default" sends null, returning that cap to its default.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { messagingGet, messagingPut } from '../api.js';
|
|
16
|
+
|
|
17
|
+
const GRN = '\x1b[32m';
|
|
18
|
+
const BOLD = '\x1b[1m';
|
|
19
|
+
const RST = '\x1b[0m';
|
|
20
|
+
|
|
21
|
+
export function parseCap(v, name) {
|
|
22
|
+
if (v === undefined) return undefined;
|
|
23
|
+
if (v === null || String(v).toLowerCase() === 'default' || String(v).toLowerCase() === 'null') return null;
|
|
24
|
+
const n = Number(v);
|
|
25
|
+
if (!Number.isFinite(n) || n < 0) throw new Error(`--${name} must be a non-negative number or "default".`);
|
|
26
|
+
return n;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildWritePolicyBody(argv) {
|
|
30
|
+
const body = {};
|
|
31
|
+
if (argv.enableShopifyWrite !== undefined) body.enableShopifyWrite = !!argv.enableShopifyWrite;
|
|
32
|
+
if (argv.requireConfirmation !== undefined) body.requireConfirmation = !!argv.requireConfirmation;
|
|
33
|
+
const caps = [
|
|
34
|
+
['maxRefundAmount', 'max-refund-amount'],
|
|
35
|
+
['maxDiscountPercent', 'max-discount-percent'],
|
|
36
|
+
['maxDiscountFixedAmount', 'max-discount-fixed-amount'],
|
|
37
|
+
];
|
|
38
|
+
for (const [key, flag] of caps) {
|
|
39
|
+
const v = parseCap(argv[key], flag);
|
|
40
|
+
if (v !== undefined) body[key] = v;
|
|
41
|
+
}
|
|
42
|
+
if (!Object.keys(body).length) throw new Error('Provide at least one option to change (see --help).');
|
|
43
|
+
return body;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function printPolicy(p) {
|
|
47
|
+
console.log(`\n${BOLD}Shopify write policy${RST}`);
|
|
48
|
+
console.log(` Shopify write skills enabled: ${p.enableShopifyWrite}`);
|
|
49
|
+
console.log(` Require confirmation: ${p.requireConfirmation}`);
|
|
50
|
+
console.log(` Max refund amount: ${p.maxRefundAmount}`);
|
|
51
|
+
console.log(` Max discount percent: ${p.maxDiscountPercent}`);
|
|
52
|
+
console.log(` Max discount fixed amount: ${p.maxDiscountFixedAmount}\n`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function getHandler() {
|
|
56
|
+
printPolicy(await messagingGet('/Shopify/WritePolicy'));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function setHandler(argv) {
|
|
60
|
+
const updated = await messagingPut('/Shopify/WritePolicy', buildWritePolicyBody(argv));
|
|
61
|
+
console.log(`\n${GRN}✓${RST} Shopify write policy updated.`);
|
|
62
|
+
printPolicy(updated);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const shopifyCommand = {
|
|
66
|
+
command: 'shopify <subcommand>',
|
|
67
|
+
describe: 'Shopify integration settings (write-tier policy: enable toggle, confirmation, refund/discount caps)',
|
|
68
|
+
builder: y => y
|
|
69
|
+
.command({
|
|
70
|
+
command: 'write-policy <action>',
|
|
71
|
+
describe: 'Get or set the Shopify write policy for this site',
|
|
72
|
+
builder: yy => yy
|
|
73
|
+
.positional('action', { choices: ['get', 'set'], type: 'string' })
|
|
74
|
+
.option('enable-shopify-write', { type: 'boolean', describe: 'Master toggle for the 11 Shopify write skills (reads unaffected)' })
|
|
75
|
+
.option('require-confirmation', { type: 'boolean', describe: 'Require customer confirmation before a write skill runs' })
|
|
76
|
+
.option('max-refund-amount', { type: 'string', describe: 'Max refund amount, or "default"' })
|
|
77
|
+
.option('max-discount-percent', { type: 'string', describe: 'Max percentage discount, or "default"' })
|
|
78
|
+
.option('max-discount-fixed-amount', { type: 'string', describe: 'Max fixed discount, or "default"' }),
|
|
79
|
+
handler: argv => (argv.action === 'get' ? getHandler() : setHandler(argv)),
|
|
80
|
+
})
|
|
81
|
+
.demandCommand(1, 'Specify a shopify subcommand.')
|
|
82
|
+
.strict(),
|
|
83
|
+
handler: () => {},
|
|
84
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { messagingGet, messagingPut } from '../api.js';
|
|
2
|
+
import { buildPolicyBody, findPolicy, describeMode, parseTeamId } from '../signature-policy.js';
|
|
3
|
+
|
|
4
|
+
// velaro signature policy get|set
|
|
5
|
+
//
|
|
6
|
+
// Company email signature policy for the calling admin's site. Goes through GET/PUT Signatures/policy in
|
|
7
|
+
// velaro-messaging (admin only there). The site comes from the caller's token, never from an argument.
|
|
8
|
+
|
|
9
|
+
const GRN = '\x1b[32m';
|
|
10
|
+
const DIM = '\x1b[2m';
|
|
11
|
+
const BOLD = '\x1b[1m';
|
|
12
|
+
const RESET = '\x1b[0m';
|
|
13
|
+
|
|
14
|
+
function printRow(label, row) {
|
|
15
|
+
console.log(`${BOLD}${label}${RESET}`);
|
|
16
|
+
if (!row) { console.log(` ${DIM}no override, follows the company-wide setting${RESET}`); return; }
|
|
17
|
+
console.log(` mode ${describeMode(row.mode)}`);
|
|
18
|
+
console.log(` signature ${row.corporateSignature ? `${row.corporateSignature.length} characters of HTML` : '(none)'}`);
|
|
19
|
+
const dis = row.disclaimerEnabled === null || row.disclaimerEnabled === undefined ? 'follows company-wide (off if none set)' : row.disclaimerEnabled ? 'on' : 'off';
|
|
20
|
+
console.log(` disclaimer ${dis}`);
|
|
21
|
+
if (row.disclaimerText) console.log(` text ${row.disclaimerText}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const getCommand = {
|
|
25
|
+
command: 'get',
|
|
26
|
+
describe: 'Show the company signature policy and any per-team overrides',
|
|
27
|
+
builder: (y) => y.option('team-id', { type: 'number', describe: 'Show only this team override (0 = company-wide)' }),
|
|
28
|
+
handler: async (argv) => {
|
|
29
|
+
const data = await messagingGet('/Signatures/policy');
|
|
30
|
+
const rows = Array.isArray(data.policies) ? data.policies : [];
|
|
31
|
+
if (argv['team-id'] !== undefined) {
|
|
32
|
+
printRow(`Team ${argv['team-id']}`, findPolicy(rows, argv['team-id']));
|
|
33
|
+
} else {
|
|
34
|
+
printRow('Company-wide', findPolicy(rows, 0));
|
|
35
|
+
for (const r of rows.filter((p) => p.teamId !== 0)) printRow(`Team ${r.teamId}`, r);
|
|
36
|
+
}
|
|
37
|
+
console.log(`\n${DIM}Suggested disclaimer:${RESET} ${data.suggestedDisclaimer ?? ''}`);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const setCommand = {
|
|
42
|
+
command: 'set',
|
|
43
|
+
describe: 'Update the signature policy (only the options you pass change; default clears back to company-wide)',
|
|
44
|
+
builder: (y) => y
|
|
45
|
+
.option('team-id', { type: 'number', default: 0, describe: '0 = company-wide, otherwise a per-team override' })
|
|
46
|
+
.option('mode', { type: 'string', describe: 'locked | company-default | personal-only | default' })
|
|
47
|
+
.option('signature', { type: 'string', describe: 'Company signature HTML (empty string clears it)' })
|
|
48
|
+
.option('disclaimer', { type: 'boolean', describe: 'Turn the legal disclaimer on or off' })
|
|
49
|
+
.option('disclaimer-default', { type: 'boolean', describe: 'Clear the disclaimer on/off choice back to company-wide' })
|
|
50
|
+
.option('disclaimer-text', { type: 'string', describe: 'Legal disclaimer text (empty string clears it)' }),
|
|
51
|
+
handler: async (argv) => {
|
|
52
|
+
const teamId = parseTeamId(argv['team-id']);
|
|
53
|
+
const data = await messagingGet('/Signatures/policy');
|
|
54
|
+
const current = findPolicy(data.policies, teamId);
|
|
55
|
+
const body = buildPolicyBody(current, {
|
|
56
|
+
teamId,
|
|
57
|
+
mode: argv.mode,
|
|
58
|
+
corporateSignature: argv.signature,
|
|
59
|
+
disclaimerEnabled: argv['disclaimer-default'] ? null : argv.disclaimer,
|
|
60
|
+
disclaimerText: argv['disclaimer-text'],
|
|
61
|
+
});
|
|
62
|
+
await messagingPut('/Signatures/policy', body);
|
|
63
|
+
console.log(`${GRN}Signature policy updated.${RESET} Run \`velaro signature policy get\` to review.`);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const policyCommand = {
|
|
68
|
+
command: 'policy <subcommand>',
|
|
69
|
+
describe: 'View and update the company email signature policy',
|
|
70
|
+
builder: (y) => y.command(getCommand).command(setCommand).demandCommand(1, 'Specify a subcommand: get | set'),
|
|
71
|
+
handler: () => {},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const signatureCommand = {
|
|
75
|
+
command: 'signature <subcommand>',
|
|
76
|
+
describe: 'Email signature controls',
|
|
77
|
+
builder: (y) => y.command(policyCommand).demandCommand(1, 'Specify: policy get | policy set'),
|
|
78
|
+
handler: () => {},
|
|
79
|
+
};
|