Adds three new SD-WAN checks (wanAppRtpMos/Loss/Jitter) that measure
REAL voice-traffic quality on actual RTP frames via Prisma DPI, not
synthetic link probes. Graded against the WORST 5-minute window so
transient degradation the 24h link-probe averages smooth away
actually surfaces.
Voice-app selection is tenant-configurable via PRISMA_APP_ID_VOICE +
PRISMA_APP_NAME_VOICE (Webex_Calling_RTP recommended for Webex
Calling shops — the Webex-specific DPI signature excludes non-Webex
UDP noise). Legacy PRISMA_APP_ID_RTP_BASE still honored with a
one-time deprecation warning.
Widens the default WAN look-back from 24h to 7 days: per-app metrics
only get datapoints when calls actually happen, so sporadic Webex
Calling stores (3-4 calls/day) need a wider window for worst-window
statistics to be meaningful. Interval picker snaps 7d to 1hour
buckets (168 pts) to keep payloads bounded while preserving
worst-hour granularity. Hard-capped at 7d — beyond that Prisma
downsamples to 1-day buckets and the signal collapses.
Also:
- Client-side concurrency limiter (PRISMA_MAX_INFLIGHT, default 3)
to prevent 429 cascades when /voicediag fans out 10+ parallel
metric fetches
- "View in Prisma UI" deep links in both /phonestatus WAN follow-up
and /voicediag details, threading through a new
integrations/paloalto/urls.js builder
- humanizeMetricUnit maps raw API unit strings ("percentage",
"milliseconds") to display symbols ("%", "ms") to fix
"11.83percentage" leaking to the UI
- getAppAudio envelope distinguishes not-configured / fetch-failed /
no-traffic states so misleading "set env var" messages don't fire
when the real problem is a 429
Co-authored-by: Cursor <cursoragent@cursor.com>
306 lines
15 KiB
JavaScript
306 lines
15 KiB
JavaScript
// src/commands/help.js
|
|
//
|
|
// Help renderer.
|
|
//
|
|
// /help → short, scannable list of every command (one line each)
|
|
// with grouped headings.
|
|
// /help <command> → detailed usage + examples for one command.
|
|
//
|
|
// The previous version was a single 5KB blob of markdown with hard-to-read
|
|
// inline URLs. This split keeps the top-level message short for both 1:1 and
|
|
// group spaces, and pushes verbose detail behind `/help <command>`.
|
|
|
|
const SHORT_HELP = {
|
|
// Work orders
|
|
wohistory: 'Recent work order history for a store',
|
|
wosummary: 'AI summary of a specific work order',
|
|
woattachments: 'Download all attachments for a work order',
|
|
|
|
// AV / phones
|
|
avstatus: 'AV / device status for a store (alias: /wostatus)',
|
|
phonestatus: 'DECT + IP phone status for a store (plus 7d WAN follow-up)',
|
|
voicediag: 'Deep voice diagnostic: Webex Calling features + SD-WAN quality with fix cards',
|
|
|
|
// Jira
|
|
jirahistory: 'Recent Jira tickets for a store (optionally filtered by component)',
|
|
jiraticket: 'Detailed AI summary of one Jira ticket',
|
|
jirapoll: 'Run the hourly Jira poller on demand (enrich unassigned AV/phone tickets)',
|
|
|
|
// Provisioning / mutating
|
|
'provision-dect': 'Interactive DECT provisioning card (add/remove bases & handsets)',
|
|
'provision-vc': 'Fully provision a video conferencing device',
|
|
vcmonitor: 'On-demand VC packet capture (start/stop/status)',
|
|
offboarduser: 'Offboard user: revoke Webex OAuth tokens + wipe MDM CORP devices',
|
|
webexhost: 'Check / assign Webex Meetings host license on aeo2go.webex.com',
|
|
|
|
// Bulk / utility
|
|
bulkavstatuscsv: 'Generate full AV devices report (CSV)',
|
|
bulkavswitchcsv: 'Generate rear-switch port report (CSV)',
|
|
devicesbymodel: 'Meraki devices grouped by model (CSV)',
|
|
};
|
|
|
|
const LONG_HELP = {
|
|
avstatus: {
|
|
title: '/avstatus',
|
|
usage: ['/avstatus <store>', '/avstatus <store> detailed'],
|
|
examples: ['/avstatus 782', '/avstatus 782 detailed'],
|
|
notes: [
|
|
'Aliases: `/wostatus`.',
|
|
'HTTP equivalent: `?storeNum=<n>&detailed=true`.',
|
|
'Includes Meraki deep links per device. For interactive topology, open `/av-store-dashboard.html`.',
|
|
],
|
|
},
|
|
phonestatus: {
|
|
title: '/phonestatus',
|
|
usage: ['/phonestatus <store>', '/phonestatus <store> detailed'],
|
|
examples: ['/phonestatus 782', '/phonestatus 782 detailed'],
|
|
notes: [
|
|
'Shows DECT basestations + IP phones with Meraki links.',
|
|
'Detailed mode adds firmware, serial, SIP details and errors.',
|
|
'When the store is Prisma SD-WAN managed (site name `CG<store>` padded to 5 digits), a follow-up **WAN Diagnostics** message arrives with per-path latency/jitter/loss/MOS, site healthscore, and any alarms — averaged over the last 7 days by default (widened from 24h so sporadic Webex Calling stores get enough call samples; override via `WAN_STANDARD_WINDOW_MINUTES` or pass `--window 24h` on /voicediag).',
|
|
'If `PRISMA_APP_ID_VOICE` is configured (e.g. pointing at `Webex_Calling_RTP` for Webex Calling shops), the follow-up also includes a **Voice Traffic Quality** section with real DPI-measured MOS / packet loss / jitter for that app — surfaces transient degradation the 7d link-probe averages smooth away.',
|
|
'For an in-depth voice diagnostic with per-user Webex Calling checks + fix cards, use `/voicediag <store>`.',
|
|
'Web dashboard: `/phone-store-dashboard.html`.',
|
|
],
|
|
},
|
|
voicediag: {
|
|
title: '/voicediag',
|
|
usage: [
|
|
'/voicediag <store>',
|
|
'/voicediag <store> detail',
|
|
'/voicediag <store> --only <check1,check2>',
|
|
'/voicediag <store> --window <duration>',
|
|
'/voicediag list-checks',
|
|
],
|
|
examples: [
|
|
'/voicediag 782',
|
|
'/voicediag 782 detail',
|
|
'/voicediag 782 --window 24h',
|
|
'/voicediag 782 --window 15m --only wanLatency,wanJitter,wanLoss,wanMos',
|
|
'/voicediag 782 --only wanAppRtpMos,wanAppRtpLoss,wanAppRtpJitter',
|
|
'/voicediag 782 --only dnd,callForwarding,voicemail',
|
|
'/voicediag list-checks',
|
|
],
|
|
notes: [
|
|
'Runs a full battery of per-user Webex Calling checks (DND, call forwarding, voicemail, call intercept, call waiting, outgoing permission, etc.) plus eleven SD-WAN checks (site, healthscore, link state, latency, jitter, loss, MOS, per-app voice MOS/loss/jitter, alarms) against the store\'s Prisma tenant.',
|
|
'The per-app checks measure REAL voice-traffic quality via Prisma DPI (worst 5-minute window over the configured look-back, default 7d), which catches transient degradation the link-probe averages smooth away. Feature-gated on `PRISMA_APP_ID_VOICE` env (set to a Prisma app id like `Webex_Calling_RTP` or `rtp-base`) — checks return skipped with an explanation when not configured.',
|
|
'Default view hides OK checks and highlights errors/warnings/skipped. Pass `detail` (or `detailed`) to also see OK checks with expanded per-link tables + thresholds + roll-ups.',
|
|
'Fixable issues (e.g. DND on, forwarding to wrong number) post a per-issue confirmation card. A single "apply all" card lets you fix everything at once after reviewing.',
|
|
'`--only` restricts the run to specific check ids (comma-separated). Use `/voicediag list-checks` to see every registered id + its scope.',
|
|
'`--window` overrides the WAN look-back window (accepts `15m`, `1h`, `6h`, `24h`, `1d`, `7d`, or a bare minute count; hard-capped at 7d). Applies to healthscore + LQM (latency/jitter/loss/MOS) + per-app voice fetches + alarms. Alarms are floored at 60m. Default is 7d — pass `--window 24h` for a tighter view during live-incident triage, or set `WAN_STANDARD_WINDOW_MINUTES` for a different global default.',
|
|
'Voice-quality thresholds default to ITU-T G.114 / RFC 3550. Override any of them via `WAN_STANDARD_*` env vars (see `.env.example`). Per-app thresholds live under `WAN_STANDARD_APP_*`. Kill-switch: `WAN_STANDARD_ENABLED=false` silences the whole WAN bucket.',
|
|
'HTTP equivalent: `?storeNum=<n>[&detailed=true][&only=id1,id2][&window=15m]`. HTTP callers see the markdown snapshot only — cards are chat-only.',
|
|
'Audit log: every remediation apply/cancel is logged under `voicediag:audit` with the requester identity.',
|
|
],
|
|
},
|
|
'provision-dect': {
|
|
title: '/provision-dect',
|
|
usage: ['/provision-dect <store>'],
|
|
examples: ['/provision-dect 782'],
|
|
notes: [
|
|
'Aliases: `/provisiondect`.',
|
|
'Posts an interactive card to add/remove bases and handsets.',
|
|
'Looks up "Store XXXX" network using 5-digit person email; access codes are auto-generated.',
|
|
],
|
|
},
|
|
'provision-vc': {
|
|
title: '/provision-vc',
|
|
usage: ['/provision-vc <serial>', '/provision-vc <serial> <org>'],
|
|
examples: ['/provision-vc FOC2419NTN2', '/provision-vc FOC2419NTN2 todd'],
|
|
notes: [
|
|
'Aliases: `/vcprovision` (legacy).',
|
|
'`<org>` may be partial name (e.g. "todd", "american") or an org ID.',
|
|
],
|
|
},
|
|
vcmonitor: {
|
|
title: '/vcmonitor',
|
|
usage: ['/vcmonitor <serial> [start|stop|status] [Full|Limited|FullRotate]'],
|
|
examples: [
|
|
'/vcmonitor FOC2419NTN2',
|
|
'/vcmonitor FOC2419NTN2 Limited',
|
|
'/vcmonitor FOC2419NTN2 stop',
|
|
'/vcmonitor FOC2419NTN2 status',
|
|
],
|
|
notes: [
|
|
'Default action: `start Full` (~3 min capture including RTP).',
|
|
'PCAPs land in the System Log bundle downloadable from Control Hub diagnostics.',
|
|
],
|
|
},
|
|
wohistory: {
|
|
title: '/wohistory',
|
|
usage: ['/wohistory <store>'],
|
|
examples: ['/wohistory 782'],
|
|
notes: ['In a store-linked space, the store can be omitted.'],
|
|
},
|
|
wosummary: {
|
|
title: '/wosummary',
|
|
usage: ['/wosummary <work-order-id>'],
|
|
examples: ['/wosummary 12345678'],
|
|
},
|
|
woattachments: {
|
|
title: '/woattachments',
|
|
usage: ['/woattachments <work-order-id>'],
|
|
examples: ['/woattachments 12345678'],
|
|
},
|
|
jirahistory: {
|
|
title: '/jirahistory',
|
|
usage: [
|
|
'/jirahistory <store>',
|
|
'/jirahistory <store> <component>',
|
|
'/jirahistory <store> all',
|
|
],
|
|
examples: [
|
|
'/jirahistory 782',
|
|
'/jirahistory 782 phone',
|
|
'/jirahistory 782 av,voice,mobility',
|
|
],
|
|
notes: [
|
|
'Component shortcuts: `av` → Audio Visual, `phone`/`voice` → Communication Services, `mobility` → Mobility.',
|
|
],
|
|
},
|
|
jiraticket: {
|
|
title: '/jiraticket',
|
|
usage: ['/jiraticket <KEY>'],
|
|
examples: ['/jiraticket SUPPORT-817694'],
|
|
},
|
|
jirapoll: {
|
|
title: '/jirapoll',
|
|
usage: ['/jirapoll', '/jirapoll prime'],
|
|
examples: ['/jirapoll', '/jirapoll prime'],
|
|
notes: [
|
|
'Triggers the same Jira poller that normally runs at the top of every hour. Enriches any unlabeled matching tickets with a phone/av snapshot comment and labels them `bot-enriched`. Tickets the AI classifier decides are out-of-scope get labeled `bot-skipped` so they aren\'t re-classified every hour.',
|
|
'Idempotent — labels + JQL prevent double-processing, so running multiple times in a row is safe.',
|
|
'`/jirapoll prime` labels every matching ticket without enriching or notifying. Use once after adopting the poller to skip enriching the existing backlog. Same as `JIRA_POLLER_PRIME_ON_START=true` at startup.',
|
|
'A summary of enriched tickets goes to the configured `JIRA_POLLER_ROOM_ID`. The invoking chat also gets a compact result line.',
|
|
],
|
|
},
|
|
webexhost: {
|
|
title: '/webexhost',
|
|
usage: [
|
|
'/webexhost <email>',
|
|
'/webexhost list',
|
|
'/webexhost debug <email>',
|
|
],
|
|
examples: [
|
|
'/webexhost jdoe@company.com',
|
|
'/webexhost list',
|
|
'/webexhost debug jdoe@company.com',
|
|
],
|
|
notes: [
|
|
'`/webexhost <email>` checks whether the user holds any Webex Meetings host license on the configured site (default `aeo2go.webex.com`, override via `WEBEX_HOST_SITE_URL`).',
|
|
'If they already have one, the command reports which license. If not — and `WEBEX_HOST_LICENSE_ID` is set in `.env` — it posts a confirmation card to assign that license.',
|
|
'`/webexhost list` is a discovery helper: lists every meeting license on the site with id + remaining seats, marking the one currently configured for auto-assign.',
|
|
'`/webexhost debug <email>` dumps the raw Webex payload for the user (search-endpoint vs `GET /people/{id}` license counts, intersection with site licenses) — use this when Control Hub disagrees with what the bot says.',
|
|
'Requires service-app scopes: `spark-admin:licenses_read` + `spark-admin:people_write`. Scope/role failures surface inline.',
|
|
'There is no Webex API for the "host vs attendee" account flag itself — host status is determined entirely by holding a meeting license on the site (confirmed via Cisco docs + wxc_sdk source).',
|
|
'Audit log: requests + outcomes are emitted under the `webexhost:audit` scope with the requester identity (chat email or `via HTTP API`).',
|
|
],
|
|
},
|
|
offboarduser: {
|
|
title: '/offboarduser',
|
|
usage: ['/offboarduser <email>'],
|
|
examples: ['/offboarduser jdoe@company.com'],
|
|
notes: [
|
|
'Posts a confirmation card showing the Webex user + MDM CORP devices that will be acted on.',
|
|
'Confirming revokes every Webex OAuth authorization for the user (signs them out of all Webex clients) and enterprise-wipes each listed MDM CORP device — both run in parallel.',
|
|
'Requires the Webex service app to hold `identity:tokens_read` + `identity:tokens_write` scopes and an admin with Full / User / Device Admin role. Scope/role failures surface inline in the success message.',
|
|
'`Hide from search` is **not** part of this command — Cisco only exposes that toggle in Control Hub, not via any Webex API. Set it manually if your offboarding policy requires it.',
|
|
'Every request and outcome is logged under the `offboard:audit` scope with the requesting user (chat email or `via HTTP API`).',
|
|
],
|
|
},
|
|
bulkavstatuscsv: {
|
|
title: '/bulkavstatuscsv',
|
|
usage: ['/bulkavstatuscsv'],
|
|
notes: ['Long-running. Posts CSV attachment back to the calling room.'],
|
|
},
|
|
bulkavswitchcsv: {
|
|
title: '/bulkavswitchcsv',
|
|
usage: ['/bulkavswitchcsv'],
|
|
notes: [
|
|
'Long-running. Walks every rear switch in Meraki for VLAN 340/145 ports.',
|
|
'Posts CSV attachment back to the calling room.',
|
|
],
|
|
},
|
|
devicesbymodel: {
|
|
title: '/devicesbymodel',
|
|
usage: ['/devicesbymodel'],
|
|
notes: ['Posts a Meraki by-model count + CSV to the calling room.'],
|
|
},
|
|
};
|
|
|
|
const GROUPS = [
|
|
{ title: 'Work orders', keys: ['wohistory', 'wosummary', 'woattachments'] },
|
|
{ title: 'AV & phones', keys: ['avstatus', 'phonestatus', 'voicediag'] },
|
|
{ title: 'Jira', keys: ['jirahistory', 'jiraticket', 'jirapoll'] },
|
|
{ title: 'Provisioning', keys: ['provision-dect', 'provision-vc', 'vcmonitor'] },
|
|
{ title: 'Admin & bulk', keys: ['offboarduser', 'webexhost', 'bulkavstatuscsv', 'bulkavswitchcsv', 'devicesbymodel'] },
|
|
];
|
|
|
|
function renderTopLevelHelp(isGroup) {
|
|
const lines = ['### CollabSupport Bot Help', ''];
|
|
for (const g of GROUPS) {
|
|
lines.push(`**${g.title}**`);
|
|
for (const key of g.keys) {
|
|
const short = SHORT_HELP[key];
|
|
if (!short) continue;
|
|
lines.push(`- \`/${key}\` — ${short}`);
|
|
}
|
|
lines.push('');
|
|
}
|
|
lines.push('Type `/help <command>` for usage and examples (e.g. `/help avstatus`).');
|
|
if (isGroup) {
|
|
lines.push('In a space linked to a store, most commands accept no arguments and pick up the store automatically.');
|
|
}
|
|
lines.push('Web dashboards: `/av-store-dashboard.html`, `/phone-store-dashboard.html`.');
|
|
return lines.join('\n');
|
|
}
|
|
|
|
function renderCommandHelp(name) {
|
|
const detail = LONG_HELP[name];
|
|
if (!detail) return null;
|
|
const lines = [`### ${detail.title}`];
|
|
if (SHORT_HELP[name]) {
|
|
lines.push('', SHORT_HELP[name]);
|
|
}
|
|
if (detail.usage?.length) {
|
|
lines.push('', '**Usage:**');
|
|
for (const u of detail.usage) lines.push(`- \`${u}\``);
|
|
}
|
|
if (detail.examples?.length) {
|
|
lines.push('', '**Examples:**');
|
|
for (const ex of detail.examples) lines.push(`- \`${ex}\``);
|
|
}
|
|
if (detail.notes?.length) {
|
|
lines.push('', '**Notes:**');
|
|
for (const n of detail.notes) lines.push(`- ${n}`);
|
|
}
|
|
return lines.join('\n');
|
|
}
|
|
|
|
export async function handleHelp(bot, trigger) {
|
|
const isGroup = trigger.message?.roomType === 'group';
|
|
|
|
// First arg (after `/help`) selects a specific command's detail page.
|
|
// Strip a leading slash so both `/help avstatus` and `/help /avstatus` work.
|
|
const args = trigger.args || [];
|
|
const requested = (args[0] || trigger.query?.command || '')
|
|
.toString()
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/^\//, '');
|
|
|
|
if (requested) {
|
|
const detail = renderCommandHelp(requested);
|
|
if (detail) {
|
|
await bot.say('markdown', detail);
|
|
return;
|
|
}
|
|
await bot.say(
|
|
'markdown',
|
|
`Unknown command: \`${requested}\`.\n\n${renderTopLevelHelp(isGroup)}`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
await bot.say('markdown', renderTopLevelHelp(isGroup));
|
|
}
|