collabSupport/commands/help.js
jmcqueen 44ab6559ac Default WAN window to 24h + surface /voicediag in /help
Bumps the shared default WAN look-back from 15m to 24h (1440m) so
both /phonestatus follow-ups and /voicediag pick up a full day of
voice-quality signal by default — better for after-the-fact ticket
triage than a live snapshot. Operators wanting real-time behavior
can set WAN_STANDARD_WINDOW_MINUTES=15 or pass --window 15m to
/voicediag.

Also fills in a long-standing help gap: /voicediag was fully
implemented but never listed in /help. Adds it under "AV & phones"
with usage, examples, and notes covering detail mode, --only,
--window, thresholds, kill switch, HTTP shape, and audit logging.
Updates /help phonestatus to call out the 24h WAN follow-up +
cross-link to /voicediag.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 09:50:30 -04:00

303 lines
14 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 24h 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 24 hours (override via `WAN_STANDARD_WINDOW_MINUTES`).',
'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 15m',
'/voicediag 782 --window 6h --only wanLatency,wanJitter,wanLoss,wanMos',
'/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 eight SD-WAN checks (site, healthscore, link state, latency, jitter, loss, MOS, alarms) against the store\'s Prisma tenant.',
'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`, or a bare minute count). Applies to healthscore + LQM (latency/jitter/loss/MOS) fetches. Alarms are floored at 60m. Default is 24h — 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`). 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));
}