Add store/email/phone filtering, richer call-line formatting, CDR feed pagination and queueing, and split Jira poller enrichment into testable modules. Co-authored-by: Cursor <cursoragent@cursor.com>
386 lines
19 KiB
JavaScript
386 lines
19 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)',
|
||
dectstatus: 'Full DECT basestation dump via relay (reboot / factory-reset cards)',
|
||
voicediag: 'Deep voice diagnostic: Webex Calling features + SD-WAN quality with fix cards',
|
||
callreport: 'Daily call digest: correlated CDR calls + per-call Prisma WAN (store, email, or phone)',
|
||
calltest: 'Twilio voice path test (store AA or direct dial, 60s listen)',
|
||
|
||
// 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 the full DECT base dump + reboot/factory-reset controls, use `/dectstatus <store>`.',
|
||
'For an in-depth voice diagnostic with per-user Webex Calling checks + fix cards, use `/voicediag <store>`.',
|
||
'Web dashboard: `/phone-store-dashboard.html`.',
|
||
],
|
||
},
|
||
dectstatus: {
|
||
title: '/dectstatus',
|
||
usage: [
|
||
'/dectstatus <store>',
|
||
'/dectstatus <store> <ip|mac>',
|
||
],
|
||
examples: [
|
||
'/dectstatus 782',
|
||
'/dectstatus 782 10.12.34.56',
|
||
'/dectstatus 782 6c:ab:05:12:34:56',
|
||
],
|
||
notes: [
|
||
'Aliases: `/dect`.',
|
||
'Pulls the full `status.xml` dump from every reachable DBS-210 base at the store via the on-prem DECT relay agent (device, firmware, reboot log, network stats, RTP, security, emergency numbers, health verdict).',
|
||
'Optional second arg filters to one base by IP, MAC, or name substring.',
|
||
'Chat-only action cards per reachable base: **Reboot**, **Force Reboot**, **Factory Reset**. Each requires a confirm click; outcomes are audited under `dect:audit`.',
|
||
'Requires `DECT_RELAY_AGENT_TOKEN` on the bot and a live `dect-relay-agent` in the data center. When the relay is offline the command still reports discovery results and the offline state.',
|
||
'HTTP equivalent: `?storeNum=<n>[&base=<ip|mac>]` — markdown dump only (no action cards).',
|
||
'The compact exception-only DECT follow-up after `/phonestatus` is separate; this command is the full dump that follow-up footer points at.',
|
||
],
|
||
},
|
||
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.',
|
||
],
|
||
},
|
||
callreport: {
|
||
title: '/callreport',
|
||
usage: [
|
||
'/callreport <store>',
|
||
'/callreport <email>',
|
||
'/callreport <phone>',
|
||
'/callreport <target> today',
|
||
'/callreport <target> YYYY-MM-DD',
|
||
'/callreport <target> --detail',
|
||
],
|
||
examples: [
|
||
'/callreport 782',
|
||
'/callreport mcqueenj@ae.com',
|
||
'/callreport 7247795574',
|
||
'/callreport mcqueenj@ae.com today',
|
||
'/callreport 782 2026-07-22',
|
||
'/callreport 782 --detail',
|
||
],
|
||
notes: [
|
||
'**Store** (`782`): full location digest for **9am–9pm local** (default: **yesterday**). `today` uses 9am → 5 minutes ago.',
|
||
'**Email** or **phone number**: resolves the user/line location via Webex, fetches location CDR, then filters to that person/number only.',
|
||
'Groups CDR legs by **Correlation ID** into calls; lists inbound reach, auto-attendant, outbound, and abnormal outcomes.',
|
||
'Answered/connected calls include **Prisma Webex_Calling_RTP** MOS/jitter/loss for the call window when configured.',
|
||
'Requires `spark-admin:calling_cdr_read` + Control Hub role **Webex Calling Detailed Call History API access**.',
|
||
'CDR queries are serialized with a **65s cooldown** — a second request in that window is queued and you get a wait notice.',
|
||
'`--detail` adds correlation ID and raises per-section row cap.',
|
||
'HTTP: `?target=782&date=yesterday&detail=true` (also `storeNum`, `email`, `number`).',
|
||
'Alias: `/voicereport` (legacy).',
|
||
],
|
||
},
|
||
calltest: {
|
||
title: '/calltest',
|
||
usage: [
|
||
'/calltest <store>',
|
||
'/calltest store <store> [+1...]',
|
||
'/calltest dial <e164>',
|
||
'/calltest status <testId>',
|
||
],
|
||
examples: [
|
||
'/calltest 782',
|
||
'/calltest store 782',
|
||
'/calltest dial +12125550100',
|
||
'/calltest status a1b2c3d4-...',
|
||
],
|
||
notes: [
|
||
'Places a **real outbound call** via Twilio from the configured `TWILIO_FROM_NUMBER`.',
|
||
'**Store path:** dials the store main number (Webex `locationMainNumber`), pauses for the AA greeting, sends DTMF `1`, waits for the store leg, then runs the shared **60-second** listen test (intro → pause → thank you).',
|
||
'**Direct dial:** calls any E.164 number; on answer, runs the same 60s test with no AA/DTMF.',
|
||
'Requires `TWILIO_*` env vars, `TWILIO_WEBHOOK_BASE_URL` (public HTTPS), and `CALLTEST_ENABLED=true`.',
|
||
'After the result card, follow-ups post separately: **Twilio details** (~10s), **Twilio Insights** (~6 min, requires Advanced Features), **Webex CDR match** (~6 min when location is known).',
|
||
'CDR match works for store tests and dial tests to a known store main number; legs are matched on **destination (called number)** only.',
|
||
'Per-store entry tuning: `config/calltest-stores.json` (`dtmf`, `greetingPauseSec`, `answerWaitSec`).',
|
||
],
|
||
},
|
||
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.',
|
||
'**Communication Services** tickets mentioning call quality (garbled, static, can\'t connect, …) get **phonestatus + callreport (today)**. Spam/robocall tickets get **callreport** only. See `services/jiraPoller/README.md` for adding more rules.',
|
||
'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', 'dectstatus', 'voicediag', 'callreport', 'calltest'] },
|
||
{ 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));
|
||
}
|