@velaro/cli 1.4.70 → 1.4.71
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/bin/velaro.js +2 -0
- package/lib/commands/experience-impact.js +6 -0
- package/lib/commands/report-explain.js +29 -0
- package/lib/commands/support.js +57 -1
- package/lib/report-explain.json +232 -0
- package/lib/siteUser.js +35 -0
- package/lib/siteUser.test.js +45 -0
- package/package.json +1 -1
package/bin/velaro.js
CHANGED
|
@@ -41,6 +41,7 @@ import { bookingCommand } from '../lib/commands/booking.js';
|
|
|
41
41
|
import { reportCommand } from '../lib/commands/report.js';
|
|
42
42
|
import { scheduleAdherenceCommand } from '../lib/commands/schedule-adherence.js';
|
|
43
43
|
import { experienceImpactCommand } from '../lib/commands/experience-impact.js';
|
|
44
|
+
import { reportExplainCommand } from '../lib/commands/report-explain.js';
|
|
44
45
|
import { conversationEfficiencyCommand } from '../lib/commands/conversation-efficiency.js';
|
|
45
46
|
import { dbCommand } from '../lib/commands/db.js';
|
|
46
47
|
import { focusCommand } from '../lib/commands/focus.js';
|
|
@@ -144,6 +145,7 @@ await yargs(hideBin(process.argv))
|
|
|
144
145
|
.command(conversationEfficiencyCommand)
|
|
145
146
|
.command(scheduleAdherenceCommand)
|
|
146
147
|
.command(experienceImpactCommand)
|
|
148
|
+
.command(reportExplainCommand)
|
|
147
149
|
.command(dbCommand)
|
|
148
150
|
.command(focusCommand)
|
|
149
151
|
.command(automationDraftCommand)
|
|
@@ -36,6 +36,12 @@ export const experienceImpactCommand = {
|
|
|
36
36
|
console.log(` ${label}: ${x.chats} chats | avg wait ${x.avgWaitSec}s | missed ${x.missed} | CSAT ${x.csatResponses ? x.csatAvg : '-'} (${x.csatResponses ?? 0})`);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
// Additive fields from the rebuilt report (absent on older backends).
|
|
40
|
+
if (d.coverage) {
|
|
41
|
+
const c = d.coverage;
|
|
42
|
+
console.log(` CSAT coverage: ${(c.sources ?? []).map(x => `${x.label} ${x.responses} (avg ${x.avg ?? '-'})`).join(' | ')} | survey submissions ${c.surveySubmissions ?? 0}, rated ${c.surveyRated ?? 0}, response rate ${c.responseRatePct ?? 0}% | CSAT survey configured: ${c.csatSurveyConfigured ? 'yes' : 'no'}`);
|
|
43
|
+
}
|
|
44
|
+
for (const x of d.reasons ?? []) console.log(` Why ${x.kpi} is low/empty [${x.code}]: ${x.message}${x.action ? ' Action: ' + x.action : ''}`);
|
|
39
45
|
for (const i of d.insights ?? []) console.log(` [${i.severity}] ${i.text}`);
|
|
40
46
|
for (const a of d.agents ?? []) {
|
|
41
47
|
console.log(` [${a.agentId}] ${a.name}: index ${a.experienceIndex} adherence ${a.adherencePct}% CSAT ${a.csatAvg ?? '-'} handled ${a.handled}`);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { runCommand } from '../run.js';
|
|
5
|
+
|
|
6
|
+
// -- velaro report-explain [report] ---------------------------------------------
|
|
7
|
+
// Static how-to-interpret guidance per report (same data as the report_explain MCP tool
|
|
8
|
+
// and copilot-kb/report-interpretation-guide.md). No network call.
|
|
9
|
+
|
|
10
|
+
export const reportExplainCommand = {
|
|
11
|
+
command: 'report-explain [report]',
|
|
12
|
+
describe: 'Explain how to read a report, its data sources, setup prerequisites and why values can be zero',
|
|
13
|
+
builder: (y) => y
|
|
14
|
+
.positional('report', { type: 'string', describe: 'Report key, e.g. experience-impact (omit to list all)' })
|
|
15
|
+
.option('json', { type: 'boolean', default: false, describe: 'Raw JSON output' }),
|
|
16
|
+
handler: runCommand(async (argv) => {
|
|
17
|
+
const all = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'report-explain.json'), 'utf8'));
|
|
18
|
+
const key = (argv.report ?? '').toString().trim().toLowerCase();
|
|
19
|
+
if (!key) {
|
|
20
|
+
if (argv.json) { console.log(JSON.stringify(Object.keys(all))); return; }
|
|
21
|
+
for (const [k, r] of Object.entries(all)) console.log(`${k}: ${r.name}`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const r = all[key];
|
|
25
|
+
if (!r) throw new Error(`Unknown report "${key}". Valid keys: ${Object.keys(all).join(', ')}`);
|
|
26
|
+
if (argv.json) { console.log(JSON.stringify(r, null, 2)); return; }
|
|
27
|
+
console.log(`\n${r.name}\n Answers: ${r.answers}\n Sources: ${r.sources}\n Setup: ${r.prerequisites}\n How to read: ${r.interpret}\n Why zero/empty: ${r.whyZero}\n What to do: ${r.fix}\n Related: ${r.related}`);
|
|
28
|
+
}),
|
|
29
|
+
};
|
package/lib/commands/support.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// velaro support — staff SupportTools / staff-scoped actions (superadmin only)
|
|
2
2
|
import { request, messagingPost, messagingGet } from '../api.js';
|
|
3
|
+
import { ensureSiteUser } from '../siteUser.js';
|
|
3
4
|
|
|
4
5
|
async function toggleFlag(args) {
|
|
5
6
|
if (!args.site || !args.flag || args.enabled === undefined) {
|
|
@@ -91,6 +92,18 @@ async function agentLookup(args) {
|
|
|
91
92
|
console.log(JSON.stringify(result, null, 2));
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
async function addSiteUser(args) {
|
|
96
|
+
// Password via --password or VELARO_NEW_USER_PASSWORD env (preferred; keeps it out of shell history). Never printed.
|
|
97
|
+
const password = args.password || process.env.VELARO_NEW_USER_PASSWORD;
|
|
98
|
+
try {
|
|
99
|
+
const r = await ensureSiteUser({ get: messagingGet, post: messagingPost }, { ...args, siteId: args.site, password });
|
|
100
|
+
console.log(`${r.created ? 'Created' : 'Updated existing'} ${r.email} on site ${r.siteId} (userId ${r.userId}, roles ${r.roles.join(',')}); password set, usable immediately.`);
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.error(`add-site-user failed: ${e.message}`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
94
107
|
async function listDeployments(args) {
|
|
95
108
|
const result = await request('GET', `SupportTools/sites/${args.site}/deployments`);
|
|
96
109
|
if (args.json) {
|
|
@@ -114,8 +127,43 @@ async function tourInsights(args) {
|
|
|
114
127
|
console.log(JSON.stringify(result, null, 2));
|
|
115
128
|
}
|
|
116
129
|
|
|
130
|
+
function windowQuery(args, extra = {}) {
|
|
131
|
+
const q = new URLSearchParams();
|
|
132
|
+
if (args.from) q.set('from', args.from);
|
|
133
|
+
if (args.to) q.set('to', args.to);
|
|
134
|
+
for (const [k, v] of Object.entries(extra)) if (v !== undefined) q.set(k, String(v));
|
|
135
|
+
const s = q.toString();
|
|
136
|
+
return s ? `?${s}` : '';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function connectionTimeline(args) {
|
|
140
|
+
// velaro-messaging SupportToolsController.AgentConnectionTimelineAsync (staff-only, read-only).
|
|
141
|
+
const result = await messagingGet(
|
|
142
|
+
`SupportTools/sites/${args.site}/agents/${args.agent}/connection-timeline${windowQuery(args, { limit: args.limit })}`);
|
|
143
|
+
console.log(JSON.stringify(result, null, 2));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function connectionEvidence(args) {
|
|
147
|
+
// velaro-messaging SupportToolsController.ConnectionEvidenceAsync (staff-only, read-only).
|
|
148
|
+
const result = await messagingGet(
|
|
149
|
+
`SupportTools/sites/${args.site}/connection-evidence${windowQuery(args)}`);
|
|
150
|
+
console.log(JSON.stringify(result, null, 2));
|
|
151
|
+
}
|
|
152
|
+
|
|
117
153
|
export function supportCommand(yargs) {
|
|
118
154
|
return yargs
|
|
155
|
+
.command('connection-timeline', 'Raw ordered agent connection events (connect/disconnect/reconnect) for one agent in a time window (Velaro staff, read-only)', y => y
|
|
156
|
+
.option('site', { type: 'number', demandOption: true, desc: 'Site ID' })
|
|
157
|
+
.option('agent', { type: 'number', demandOption: true, desc: 'WorkspaceUser.Id of the agent' })
|
|
158
|
+
.option('from', { type: 'string', desc: 'ISO start (default: 24h before --to)' })
|
|
159
|
+
.option('to', { type: 'string', desc: 'ISO end (default: now)' })
|
|
160
|
+
.option('limit', { type: 'number', default: 2000, desc: 'Max events (1-5000)' })
|
|
161
|
+
, a => connectionTimeline(a))
|
|
162
|
+
.command('connection-evidence', 'Dispute-grade connection evidence for a site: Velaro probe uptime, incidents (incl. excluded, with reason), per-agent disconnects and client-vs-server reconciliation (Velaro staff, read-only)', y => y
|
|
163
|
+
.option('site', { type: 'number', demandOption: true, desc: 'Site ID' })
|
|
164
|
+
.option('from', { type: 'string', desc: 'ISO start (default: 30 days before --to)' })
|
|
165
|
+
.option('to', { type: 'string', desc: 'ISO end (default: now)' })
|
|
166
|
+
, a => connectionEvidence(a))
|
|
119
167
|
.command('tour-insights', 'Moshky setup-tour funnel + scrubbed feedback across all sites, or one site\'s tour timeline with --site (Velaro staff, read-only)', y => y
|
|
120
168
|
.option('site', { type: 'number', desc: 'Optional site ID: show that site\'s tour events and feedback instead of the funnel' })
|
|
121
169
|
.option('days', { type: 'number', default: 30, desc: 'Look-back window in days (max 365)' })
|
|
@@ -136,6 +184,14 @@ export function supportCommand(yargs) {
|
|
|
136
184
|
.command('agent-lookup', 'Resolve a bare WorkspaceUser.Id (AgentId) to its Email/DisplayName/SiteId across all sites (superadmin only, read-only)', y => y
|
|
137
185
|
.option('id', { type: 'number', demandOption: true, desc: 'WorkspaceUser.Id / AgentId to look up' })
|
|
138
186
|
, a => agentLookup(a))
|
|
187
|
+
.command('add-site-user', 'Create or ensure a NORMAL user (agent|administrator, never staff) on ANY site with an explicit non-forcing password (staff only, idempotent)', y => y
|
|
188
|
+
.option('site', { type: 'number', demandOption: true, desc: 'Target site ID' })
|
|
189
|
+
.option('email', { type: 'string', demandOption: true, desc: 'User email' })
|
|
190
|
+
.option('password', { type: 'string', desc: 'Password (or set VELARO_NEW_USER_PASSWORD; never logged)' })
|
|
191
|
+
.option('role', { type: 'string', default: 'agent', choices: ['agent', 'administrator'], desc: 'agent or administrator' })
|
|
192
|
+
.option('firstName', { type: 'string' })
|
|
193
|
+
.option('lastName', { type: 'string' })
|
|
194
|
+
, a => addSiteUser(a))
|
|
139
195
|
.command('toggle-flag', 'Toggle a single legacy Subscription.cs feature flag on a site (superadmin only, allowlisted flags)', y => y
|
|
140
196
|
.option('site', { type: 'number', demandOption: true, desc: 'Site ID' })
|
|
141
197
|
.option('flag', { type: 'string', demandOption: true, desc: 'Flag name, e.g. EnableScraperBotIndexing (must be on SupportToolsController.AllowedFeatureFlags)' })
|
|
@@ -155,5 +211,5 @@ export function supportCommand(yargs) {
|
|
|
155
211
|
.option('channel', { type: 'string', desc: 'Conversation source, e.g. Web, TwilioSms (default Web)' })
|
|
156
212
|
.option('overrideIndexes', { type: 'string', desc: 'Comma-separated index names to override the config\'s own IndexName for this query' })
|
|
157
213
|
, a => chatQuery(a))
|
|
158
|
-
.demandCommand(1, 'Specify a subcommand: deployments | impersonate | agent-lookup | toggle-flag | scraper-create-job | chat-query | send-email | tour-insights');
|
|
214
|
+
.demandCommand(1, 'Specify a subcommand: deployments | impersonate | agent-lookup | add-site-user | toggle-flag | scraper-create-job | chat-query | send-email | tour-insights | connection-timeline | connection-evidence');
|
|
159
215
|
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
{
|
|
2
|
+
"experience-impact": {
|
|
3
|
+
"name": "Experience Impact",
|
|
4
|
+
"answers": "How staffing and adherence affect what customers feel, as a 0 to 100 Experience Index.",
|
|
5
|
+
"sources": "Work Schedules and presence, chat queue timing, missed chats, and CSAT from ticket replies and 1-5 survey ratings (questions tagged csat or star_rating).",
|
|
6
|
+
"prerequisites": "Work Schedules for agents; a survey with a 1-5 CSAT or star question attached to post-chat or a workflow, or ticket CSAT requests turned on; chat traffic in range.",
|
|
7
|
+
"interpret": "Weights: service level 30, adherence 25, CSAT 25, missed chats 20; a signal with no data drops out. CSAT needs 5+ responses to count. Response rate = rated survey submissions / all survey submissions. Actual agents below scheduled while missed chats rise = staffing gap.",
|
|
8
|
+
"whyZero": "CSAT empty: no CSAT survey configured; survey exists but nobody answered; answers came in with no 1-5 rating question; fewer than 5 responses; or the range predates the survey. If older chats were surveyed but never counted, ask Velaro support to run a CSAT backfill. Adherence empty: no Work Schedules. Sample data never mixes with real data.",
|
|
9
|
+
"fix": "Add a 1-5 star or CSAT question to the post-chat survey, enable ticket CSAT requests, widen the range, then fix the weakest component.",
|
|
10
|
+
"related": "Schedule Adherence, Service Level, Missed Chats, Surveys & NPS, Sentiment, Agent Performance"
|
|
11
|
+
},
|
|
12
|
+
"conversations": {
|
|
13
|
+
"name": "Conversations",
|
|
14
|
+
"answers": "How many conversations came in, on which channels and when.",
|
|
15
|
+
"sources": "Every conversation record on your account, grouped by channel, team, bot or agent, and time.",
|
|
16
|
+
"prerequisites": "At least one deployed channel (chat widget, SMS, email, voice, social).",
|
|
17
|
+
"interpret": "Overview shows totals and trend; Breakdown splits by channel, team and handler. Look at peaks by hour and day to plan staffing. Rising volume with flat staffing predicts longer waits.",
|
|
18
|
+
"whyZero": "No channel deployed or no traffic; the date range or team filter is too narrow; or you lack access to that team's data.",
|
|
19
|
+
"fix": "Widen the range, clear filters, confirm the deployment is live and the widget loads on your site.",
|
|
20
|
+
"related": "Service Level, Missed Chats, Visitor Traffic"
|
|
21
|
+
},
|
|
22
|
+
"missed": {
|
|
23
|
+
"name": "Missed Chats",
|
|
24
|
+
"answers": "Visitors who asked for help and got no live answer, and why.",
|
|
25
|
+
"sources": "Chat requests that ended without an agent or bot response, plus offline or unanswered conversations.",
|
|
26
|
+
"prerequisites": "Live chat deployed with agents or routing configured.",
|
|
27
|
+
"interpret": "Overview gives the missed count and rate. Breakdown shows time of day and team. Analysis groups likely causes such as nobody online, queue timeout, or visitor left. A missed spike at one hour points to a coverage gap.",
|
|
28
|
+
"whyZero": "Genuinely none, no chat traffic, or a bot answers everything so nothing reaches an agent queue.",
|
|
29
|
+
"fix": "Add agents or schedules for peak hours, review routing and after-hours behavior, enable offline message capture.",
|
|
30
|
+
"related": "Service Level, Experience Impact, Agent Availability"
|
|
31
|
+
},
|
|
32
|
+
"agent-performance": {
|
|
33
|
+
"name": "Agent Performance",
|
|
34
|
+
"answers": "How each agent handles conversations: volume, handle time, response speed and quality.",
|
|
35
|
+
"sources": "Conversations assigned to and handled by each agent, message timestamps, and survey scores where collected.",
|
|
36
|
+
"prerequisites": "Agents added to the account and handling chats.",
|
|
37
|
+
"interpret": "Compare an agent against the team over several weeks, not one day. Pair handle time with CSAT: fast and unhappy is a quality problem, slow and happy may be complex work.",
|
|
38
|
+
"whyZero": "Agent has no assigned chats in range, was not online, or the filter excludes their team. Quality scores need survey data.",
|
|
39
|
+
"fix": "Check routing assigns chats to that agent and that they set themselves available.",
|
|
40
|
+
"related": "Response Time, Agent Utilization, Surveys & NPS"
|
|
41
|
+
},
|
|
42
|
+
"response-time": {
|
|
43
|
+
"name": "Response Time",
|
|
44
|
+
"answers": "How quickly customers get a first reply and how long replies take afterwards.",
|
|
45
|
+
"sources": "Message timestamps between the customer's message and the first agent or bot reply.",
|
|
46
|
+
"prerequisites": "Chats with at least one reply.",
|
|
47
|
+
"interpret": "Look at the median and the slow tail together. A fine average with a long tail means some customers wait far too long.",
|
|
48
|
+
"whyZero": "No replies in range, or chats were only bot-handled and excluded from agent timing.",
|
|
49
|
+
"fix": "Add coverage at slow hours, turn on auto-greeting, review the routing capacity limit.",
|
|
50
|
+
"related": "Service Level, Agent Performance"
|
|
51
|
+
},
|
|
52
|
+
"service-level": {
|
|
53
|
+
"name": "Service Level",
|
|
54
|
+
"answers": "The share of chats answered within your target time.",
|
|
55
|
+
"sources": "Queue entry and first-answer timestamps compared with your configured service level threshold.",
|
|
56
|
+
"prerequisites": "A service level threshold in report settings and chats that enter an agent queue.",
|
|
57
|
+
"interpret": "A common target is 80 percent answered in 30 seconds. Check the trend by hour to find breaches.",
|
|
58
|
+
"whyZero": "No chats were queued for agents in the range, or no threshold is set.",
|
|
59
|
+
"fix": "Set the threshold, then staff to the hours that breach it.",
|
|
60
|
+
"related": "Response Time, Missed Chats, Experience Impact"
|
|
61
|
+
},
|
|
62
|
+
"sentiment": {
|
|
63
|
+
"name": "Sentiment",
|
|
64
|
+
"answers": "The overall tone of customer messages, by contact and by agent.",
|
|
65
|
+
"sources": "AI sentiment scoring of conversation messages.",
|
|
66
|
+
"prerequisites": "Sentiment reporting enabled on your plan and enough conversations to score.",
|
|
67
|
+
"interpret": "Watch the trend and the share of negative conversations. Use By Agent for coaching, By Contact to spot at-risk customers. Sentiment is inferred from text, so treat single scores as hints.",
|
|
68
|
+
"whyZero": "Feature not enabled, conversations too short to score, or scoring has not run yet for recent chats.",
|
|
69
|
+
"fix": "Confirm the plan includes it, widen the range, check again after the next processing cycle.",
|
|
70
|
+
"related": "Customer Health, Surveys & NPS, Experience Impact"
|
|
71
|
+
},
|
|
72
|
+
"health": {
|
|
73
|
+
"name": "Customer Health",
|
|
74
|
+
"answers": "Which contacts look healthy or at risk based on recent interactions.",
|
|
75
|
+
"sources": "Conversation frequency, sentiment, unresolved issues and survey results per contact.",
|
|
76
|
+
"prerequisites": "Contacts with conversation history.",
|
|
77
|
+
"interpret": "Low scores with rising contact frequency are the first to act on. Scores are relative to your own customers.",
|
|
78
|
+
"whyZero": "New account with little history, or sentiment data is not yet available.",
|
|
79
|
+
"fix": "Allow history to accumulate and enable surveys to add a direct signal.",
|
|
80
|
+
"related": "Sentiment, Surveys & NPS, Intelligence Suite"
|
|
81
|
+
},
|
|
82
|
+
"surveys": {
|
|
83
|
+
"name": "Surveys & NPS",
|
|
84
|
+
"answers": "What customers say in surveys: CSAT, NPS and custom questions, and how many respond.",
|
|
85
|
+
"sources": "Survey answers collected after chats, through workflows, standalone links or email.",
|
|
86
|
+
"prerequisites": "At least one active survey (post-chat CSAT, NPS or custom) delivered on a channel.",
|
|
87
|
+
"interpret": "Read the average with the response count and response rate. NPS is promoters minus detractors. Under about 30 responses, trends are noisy.",
|
|
88
|
+
"whyZero": "No survey enabled, survey not attached to the channel or workflow, nobody responded, or the range predates the survey. The visitor may close the chat before the survey shows.",
|
|
89
|
+
"fix": "Enable a short post-chat survey on every staffed channel, test it yourself, and keep questions to one or two.",
|
|
90
|
+
"related": "Experience Impact, Sentiment, Missed Chats"
|
|
91
|
+
},
|
|
92
|
+
"tickets": {
|
|
93
|
+
"name": "Tickets",
|
|
94
|
+
"answers": "Ticket volume, status, age and resolution speed.",
|
|
95
|
+
"sources": "Tickets created from email, forms, chats and manual entry.",
|
|
96
|
+
"prerequisites": "Ticketing or an email inbox connected.",
|
|
97
|
+
"interpret": "Watch open backlog and time to resolve. A growing backlog with steady intake means capacity is short.",
|
|
98
|
+
"whyZero": "Ticketing not enabled, no inbox connected, or no tickets in range.",
|
|
99
|
+
"fix": "Connect an inbox or enable ticket creation from forms and chat.",
|
|
100
|
+
"related": "Conversations, Agent Performance"
|
|
101
|
+
},
|
|
102
|
+
"calls": {
|
|
103
|
+
"name": "Calls",
|
|
104
|
+
"answers": "Phone call volume, duration, answered and missed calls.",
|
|
105
|
+
"sources": "Call records from your connected phone line or voice integration.",
|
|
106
|
+
"prerequisites": "A provisioned phone line or connected voice integration.",
|
|
107
|
+
"interpret": "Compare answered against missed by hour. Long durations may signal complexity or unresolved issues.",
|
|
108
|
+
"whyZero": "No phone line connected or no calls in range.",
|
|
109
|
+
"fix": "Connect a number and confirm it routes into Velaro.",
|
|
110
|
+
"related": "IVR, Voice"
|
|
111
|
+
},
|
|
112
|
+
"ivr": {
|
|
113
|
+
"name": "IVR",
|
|
114
|
+
"answers": "How callers move through your phone menus and where they drop off.",
|
|
115
|
+
"sources": "IVR call events for each menu step and outcome.",
|
|
116
|
+
"prerequisites": "An IVR configured on a phone line.",
|
|
117
|
+
"interpret": "A high drop-off at one step suggests a confusing prompt. Many transfers to an agent from one option shows demand to automate.",
|
|
118
|
+
"whyZero": "No IVR configured or no calls reached it.",
|
|
119
|
+
"fix": "Complete IVR setup, place a test call, then re-check.",
|
|
120
|
+
"related": "Calls, Voice"
|
|
121
|
+
},
|
|
122
|
+
"account-utilization": {
|
|
123
|
+
"name": "Account Utilization",
|
|
124
|
+
"answers": "How much of your plan (agents, bots, AI usage, channels) you are using.",
|
|
125
|
+
"sources": "Plan limits compared with measured usage on your account.",
|
|
126
|
+
"prerequisites": "Administrator or manager access.",
|
|
127
|
+
"interpret": "Near 100 percent means you may need a higher limit; very low usage means features you pay for are idle.",
|
|
128
|
+
"whyZero": "Feature not enabled yet or no usage in range.",
|
|
129
|
+
"fix": "Turn on unused features or request a limit increase.",
|
|
130
|
+
"related": "Agent Utilization"
|
|
131
|
+
},
|
|
132
|
+
"agent-availability": {
|
|
133
|
+
"name": "Agent Availability",
|
|
134
|
+
"answers": "When agents were online, away or offline.",
|
|
135
|
+
"sources": "Agent presence and status changes.",
|
|
136
|
+
"prerequisites": "Agents logging in to the workspace.",
|
|
137
|
+
"interpret": "Compare online time with scheduled time and chat demand to find coverage gaps.",
|
|
138
|
+
"whyZero": "Agents did not log in or the selected agents are not in range.",
|
|
139
|
+
"fix": "Confirm agents sign in and set their status to available.",
|
|
140
|
+
"related": "Schedule Adherence, Agent Utilization, Missed Chats"
|
|
141
|
+
},
|
|
142
|
+
"agent-utilization": {
|
|
143
|
+
"name": "Agent Utilization",
|
|
144
|
+
"answers": "How busy agents are compared with their capacity.",
|
|
145
|
+
"sources": "Concurrent chat load and online time per agent.",
|
|
146
|
+
"prerequisites": "Administrator or manager access; agents handling chats.",
|
|
147
|
+
"interpret": "Around 60 to 80 percent is healthy. Above that risks long waits and burnout, well below means spare capacity.",
|
|
148
|
+
"whyZero": "Agents were not online or not given chats.",
|
|
149
|
+
"fix": "Rebalance routing and capacity limits.",
|
|
150
|
+
"related": "Agent Availability, Agent Performance"
|
|
151
|
+
},
|
|
152
|
+
"visitor-traffic": {
|
|
153
|
+
"name": "Visitor Traffic",
|
|
154
|
+
"answers": "Who is visiting pages that have the Velaro widget and how many start chats.",
|
|
155
|
+
"sources": "Visitor tracking from the widget script on your site.",
|
|
156
|
+
"prerequisites": "Visitor tracking enabled and the widget installed on your pages.",
|
|
157
|
+
"interpret": "Compare visits with chats started to find pages where engagement is low.",
|
|
158
|
+
"whyZero": "Widget not installed on those pages, tracking not enabled, or an ad blocker or consent setting blocks it.",
|
|
159
|
+
"fix": "Verify the embed code on the live page and the tracking setting.",
|
|
160
|
+
"related": "Conversations"
|
|
161
|
+
},
|
|
162
|
+
"schedule-adherence": {
|
|
163
|
+
"name": "Schedule Adherence",
|
|
164
|
+
"answers": "Whether agents were online when scheduled.",
|
|
165
|
+
"sources": "Work Schedules compared with agent presence, plus approved exceptions.",
|
|
166
|
+
"prerequisites": "Work Schedules assigned to agents; the site time zone set correctly.",
|
|
167
|
+
"interpret": "Adherence is minutes available during scheduled time divided by scheduled minutes. Approved exceptions are excluded. Use Intraday for now and Timeline for the day's detail.",
|
|
168
|
+
"whyZero": "No schedules assigned, agents not scheduled in range, or time zone mismatch.",
|
|
169
|
+
"fix": "Create and assign schedules and check the time zone.",
|
|
170
|
+
"related": "Experience Impact, Agent Availability, Service Level"
|
|
171
|
+
},
|
|
172
|
+
"chat-intelligence": {
|
|
173
|
+
"name": "Chat Intelligence",
|
|
174
|
+
"answers": "What topics customers ask about, trends, and where the bot or KB falls short.",
|
|
175
|
+
"sources": "AI topic grouping of conversations and missed topics.",
|
|
176
|
+
"prerequisites": "Enough conversations (typically dozens) and the feature enabled.",
|
|
177
|
+
"interpret": "Rising themes show new demand. Missed topics are KB gaps: write or ingest content for them. Recommendations can be applied or dismissed.",
|
|
178
|
+
"whyZero": "Too little traffic, feature not enabled, or analysis has not run.",
|
|
179
|
+
"fix": "Widen the range and allow the next analysis run.",
|
|
180
|
+
"related": "Intelligence Suite, Conversation Efficiency"
|
|
181
|
+
},
|
|
182
|
+
"conversation-efficiency": {
|
|
183
|
+
"name": "Conversation Efficiency",
|
|
184
|
+
"answers": "How efficiently conversations are resolved and where time is lost.",
|
|
185
|
+
"sources": "Message counts, handle time, transfers and outcomes.",
|
|
186
|
+
"prerequisites": "Conversations with agent involvement.",
|
|
187
|
+
"interpret": "Agents shows who is above or below the norm, Correlation shows what drives long chats, Recommendations suggests actions.",
|
|
188
|
+
"whyZero": "No handled conversations in range.",
|
|
189
|
+
"fix": "Widen the range or pick a team with traffic.",
|
|
190
|
+
"related": "Agent Performance, Chat Intelligence"
|
|
191
|
+
},
|
|
192
|
+
"intelligence": {
|
|
193
|
+
"name": "Intelligence Suite",
|
|
194
|
+
"answers": "AI analysis across agents, customers, knowledge gaps, volume forecasts, revenue impact, competitors and channels.",
|
|
195
|
+
"sources": "Conversation history, surveys, and connected commerce or CRM data where present.",
|
|
196
|
+
"prerequisites": "Feature enabled on the plan and a few weeks of data. Revenue Impact needs a commerce or CRM integration.",
|
|
197
|
+
"interpret": "Treat each tab as guidance. Predictive Volume is a forecast, not a guarantee. Revenue Impact is an estimate from connected data.",
|
|
198
|
+
"whyZero": "Not enough history, missing integration, or feature not enabled.",
|
|
199
|
+
"fix": "Connect the relevant integration and allow data to accumulate.",
|
|
200
|
+
"related": "Customer Health, Chat Intelligence"
|
|
201
|
+
},
|
|
202
|
+
"operations": {
|
|
203
|
+
"name": "Operations Center",
|
|
204
|
+
"answers": "Live view of queues, agents and conversations right now.",
|
|
205
|
+
"sources": "Real-time presence and queue state.",
|
|
206
|
+
"prerequisites": "Workforce management enabled.",
|
|
207
|
+
"interpret": "Use it for moment-to-moment staffing. A rising queue with idle agents suggests a routing problem.",
|
|
208
|
+
"whyZero": "No active conversations or agents online.",
|
|
209
|
+
"fix": "Confirm agents are online and routing is active.",
|
|
210
|
+
"related": "Agent Availability, Service Level"
|
|
211
|
+
},
|
|
212
|
+
"voice": {
|
|
213
|
+
"name": "Voice",
|
|
214
|
+
"answers": "Individual call history and voice usage and billing.",
|
|
215
|
+
"sources": "Call log and voice minute usage.",
|
|
216
|
+
"prerequisites": "Voice enabled with a phone number.",
|
|
217
|
+
"interpret": "Call Log for detail, Usage for minutes against your plan.",
|
|
218
|
+
"whyZero": "No voice line or no calls.",
|
|
219
|
+
"fix": "Set up a voice number.",
|
|
220
|
+
"related": "Calls, IVR"
|
|
221
|
+
},
|
|
222
|
+
"email-subscriptions": {
|
|
223
|
+
"name": "Email Reports",
|
|
224
|
+
"answers": "Scheduled report summaries emailed to you.",
|
|
225
|
+
"sources": "The same data as the reports, summarized on your schedule.",
|
|
226
|
+
"prerequisites": "Access to the underlying report; some are manager or admin only.",
|
|
227
|
+
"interpret": "Each email links back to the full report.",
|
|
228
|
+
"whyZero": "Subscription not saved, the report is not on your plan, or it had no data for the period.",
|
|
229
|
+
"fix": "Re-save the subscription and check spam filtering for noreply@velaro.com.",
|
|
230
|
+
"related": "All reports"
|
|
231
|
+
}
|
|
232
|
+
}
|
package/lib/siteUser.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Ensure a NORMAL (agent|administrator) user with an explicit, non-forcing password on any customer site.
|
|
2
|
+
// Composes velaro-messaging's existing staff SuperAdmin endpoints (no new backend):
|
|
3
|
+
// GET SuperAdmin/users/{siteId} (find by email)
|
|
4
|
+
// POST SuperAdmin/users/{siteId} (create WorkspaceUser + Entra, SendEmail=false)
|
|
5
|
+
// POST SuperAdmin/users/{siteId}/{userId}/set-password (non-forcing, audit-logged server side)
|
|
6
|
+
// The password is only ever sent in the HTTPS body of set-password; never logged or returned.
|
|
7
|
+
const ROLE_MAP = { agent: 'Agent', administrator: 'Administrator' };
|
|
8
|
+
|
|
9
|
+
export async function ensureSiteUser({ get, post }, args) {
|
|
10
|
+
const role = String(args.role || 'agent').toLowerCase();
|
|
11
|
+
if (!ROLE_MAP[role]) throw new Error('role must be agent or administrator (staff/superadmin roles are never granted here).');
|
|
12
|
+
const siteId = Number(args.siteId);
|
|
13
|
+
if (!Number.isInteger(siteId) || siteId <= 0) throw new Error('siteId must be a positive integer.');
|
|
14
|
+
const email = String(args.email || '').trim().toLowerCase();
|
|
15
|
+
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) throw new Error('A valid email is required.');
|
|
16
|
+
if (typeof args.password !== 'string' || args.password.length < 8) throw new Error('password is required (min 8 characters).');
|
|
17
|
+
|
|
18
|
+
const listed = await get(`SuperAdmin/users/${siteId}`);
|
|
19
|
+
const users = Array.isArray(listed) ? listed : (listed?.users || []);
|
|
20
|
+
let user = users.find(u => String(u.email || u.Email || '').toLowerCase() === email);
|
|
21
|
+
let created = false;
|
|
22
|
+
if (!user) {
|
|
23
|
+
const res = await post(`SuperAdmin/users/${siteId}`, {
|
|
24
|
+
email, firstName: args.firstName || '', lastName: args.lastName || '',
|
|
25
|
+
roles: [ROLE_MAP[role]], sendEmail: false,
|
|
26
|
+
});
|
|
27
|
+
if (!res?.success) throw new Error(`Create failed: ${res?.error || res?.message || 'unknown error'}`);
|
|
28
|
+
user = { id: res.user.id ?? res.user.Id, roles: res.user.roles };
|
|
29
|
+
created = true;
|
|
30
|
+
}
|
|
31
|
+
const userId = user.id ?? user.Id;
|
|
32
|
+
const sp = await post(`SuperAdmin/users/${siteId}/${userId}/set-password`, { newPassword: args.password });
|
|
33
|
+
if (sp && sp.success === false) throw new Error(`set-password failed: ${sp.error || 'unknown error'}`);
|
|
34
|
+
return { success: true, siteId, userId, email, created, roles: user.roles || [ROLE_MAP[role]], passwordSet: true };
|
|
35
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { ensureSiteUser } from './siteUser.js';
|
|
4
|
+
|
|
5
|
+
const base = { siteId: 100160, email: 'R@Velaro.com', password: 'S3cretPassw0rd!', role: 'agent' };
|
|
6
|
+
function fakes(existing) {
|
|
7
|
+
const calls = [];
|
|
8
|
+
return {
|
|
9
|
+
calls,
|
|
10
|
+
get: async (p) => { calls.push(['GET', p]); return existing ? [{ id: 7, email: 'r@velaro.com', roles: ['Agent'] }] : []; },
|
|
11
|
+
post: async (p, b) => { calls.push(['POST', p, b]); return p.endsWith('set-password') ? { success: true } : { success: true, user: { id: 9, roles: ['Agent'] } }; },
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test('creates user then sets password, never returns the password', async () => {
|
|
16
|
+
const f = fakes(false);
|
|
17
|
+
const r = await ensureSiteUser(f, base);
|
|
18
|
+
assert.equal(r.created, true);
|
|
19
|
+
assert.equal(r.userId, 9);
|
|
20
|
+
assert.ok(!JSON.stringify(r).includes(base.password));
|
|
21
|
+
assert.equal(f.calls[1][2].sendEmail, false);
|
|
22
|
+
assert.deepEqual(f.calls[1][2].roles, ['Agent']);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('idempotent: existing user only gets password reset', async () => {
|
|
26
|
+
const f = fakes(true);
|
|
27
|
+
const r = await ensureSiteUser(f, base);
|
|
28
|
+
assert.equal(r.created, false);
|
|
29
|
+
assert.equal(f.calls.filter(c => c[0] === 'POST').length, 1);
|
|
30
|
+
assert.ok(f.calls[1][1].endsWith('/7/set-password'));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('rejects superadmin/staff roles and weak input', async () => {
|
|
34
|
+
const f = fakes(false);
|
|
35
|
+
for (const role of ['superadmin', 'Velaro Admin', 'support']) await assert.rejects(ensureSiteUser(f, { ...base, role }));
|
|
36
|
+
await assert.rejects(ensureSiteUser(f, { ...base, password: 'short' }));
|
|
37
|
+
await assert.rejects(ensureSiteUser(f, { ...base, email: 'nope' }));
|
|
38
|
+
assert.equal(f.calls.length, 0);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('errors never contain the password', async () => {
|
|
42
|
+
const f = fakes(true);
|
|
43
|
+
f.post = async () => ({ success: false, error: 'boom' });
|
|
44
|
+
await assert.rejects(ensureSiteUser(f, base), e => !e.message.includes(base.password));
|
|
45
|
+
});
|
package/package.json
CHANGED