// src/commands/help.js // // Help renderer. // // /help → short, scannable list of every command (one line each) // with grouped headings. // /help → 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 `. 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)', voicestatus: 'Quick DECT + IP phone status for a store', wanstatus: 'Prisma SD-WAN health + voice traffic quality', atlasdiag: 'Live AZM report via Atlas tunnel (sources, zones, accessories)', phonediag: 'MPP desk phone relay diagnostics (CP-7841)', dectdiag: 'Full DECT base dump via relay (handsets, RSSI, reboot cards)', voicediag: 'Deep voice diagnostic: features + WAN + relay probes 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 ', '/avstatus detailed'], examples: ['/avstatus 782', '/avstatus 782 detailed'], notes: [ 'Aliases: `/wostatus`.', 'HTTP equivalent: `?storeNum=&detailed=true`.', 'Includes Meraki deep links per device. For interactive topology, open `/av-store-dashboard.html`.', ], }, voicestatus: { title: '/voicestatus', usage: [ '/voicestatus ', '/voicestatus detailed', ], examples: [ '/voicestatus 782', '/voicestatus 782 detailed', ], notes: [ 'Shows DECT basestations + IP phones with Meraki links — quick scan only, no follow-up messages.', 'Detailed mode adds firmware, serial, SIP details and errors.', 'For MPP desk phone relay diagnostics, use `/phonediag `.', 'For Prisma SD-WAN metrics, use `/wanstatus `.', 'For full DECT base dump + reboot/factory-reset controls, use `/dectdiag `.', 'For an in-depth voice diagnostic with per-user Webex Calling checks + fix cards, use `/voicediag `.', 'Web dashboard: `/phone-store-dashboard.html`.', ], }, atlasdiag: { title: '/atlasdiag', usage: [ '/atlasdiag ', '/atlasdiag ', ], examples: [ '/atlasdiag 2547', '/atlasdiag US002547AMP', ], notes: [ 'Live AZM diagnostic via Atlas UI tunnel + WebSocket (30–90s).', 'Reports sources, zones, accessories, uptime, and monitor speaker status.', 'Sources with input level between -80 and -70 dB are flagged as low.', 'Monitor speaker is auto-disabled when left on (should only be on during onsite troubleshooting).', 'Requires ATLAS_AUTH_KEY, UI session creds (ATLAS_UI_EMAIL/PASSWORD or static ATLAS_UI_* headers), and ATLAS_DEVICE_USERNAME/PASSWORD.', 'HTTP: `?storeNum=2547`.', ], }, wanstatus: { title: '/wanstatus', usage: [ '/wanstatus ', '/wanstatus --window 24h', ], examples: [ '/wanstatus 782', '/wanstatus 782 --window 7d', ], notes: [ 'Prisma SD-WAN diagnostics for stores managed under site name `CG` (padded to 5 digits).', 'Shows healthscore, per-path latency/jitter/loss/MOS, active alarms — default window 7 days.', 'When `PRISMA_APP_ID_VOICE` is configured, includes **Voice Traffic Quality** (DPI-measured MOS / loss / jitter).', 'Override window via `--window 24h`, `7d`, etc. HTTP: `?storeNum=&window=24h`.', ], }, phonediag: { title: '/phonediag', usage: [ '/phonediag ', '/phonediag verbose', ], examples: [ '/phonediag 782', '/phonediag 782 verbose', '/phonediag 782 debug', ], notes: [ 'MPP desk phone relay diagnostics for CP-7841 phones on 10.x: registration, switch LLDP, provisioning, issues.', 'Pass `verbose` or `debug` for SIP counters, extra extensions, and probe path table.', 'Requires `DECT_RELAY_AGENT_TOKEN` and a live dect-relay-agent.', ], }, dectdiag: { title: '/dectdiag', usage: [ '/dectdiag ', '/dectdiag ', ], examples: [ '/dectdiag 782', '/dectdiag 782 10.12.34.56', '/dectdiag 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, handsets & RF, reboot log, network stats, RTP, health verdict).', 'Also shows **Webex handset registrations** per base (extension, last registration) and **RF signal (RSSI)** from the base when handsets are registered.', '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.', 'HTTP equivalent: `?storeNum=[&base=]` — markdown dump only (no action cards).', ], }, voicediag: { title: '/voicediag', usage: [ '/voicediag ', '/voicediag detail', '/voicediag --only ', '/voicediag --window ', '/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 per-user Webex Calling checks, SD-WAN checks, and relay probes (MPP desk phones + DECT bases when `DECT_RELAY_AGENT_TOKEN` is set).', '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=[&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 '], 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 ', '/provision-vc '], examples: ['/provision-vc FOC2419NTN2', '/provision-vc FOC2419NTN2 todd'], notes: [ 'Aliases: `/vcprovision` (legacy).', '`` may be partial name (e.g. "todd", "american") or an org ID.', ], }, vcmonitor: { title: '/vcmonitor', usage: ['/vcmonitor [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 ', '/callreport ', '/callreport ', '/callreport today', '/callreport YYYY-MM-DD', '/callreport --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 ', '/calltest store [+1...]', '/calltest dial ', '/calltest status ', ], 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 '], examples: ['/wohistory 782'], notes: ['In a store-linked space, the store can be omitted.'], }, wosummary: { title: '/wosummary', usage: ['/wosummary '], examples: ['/wosummary 12345678'], }, woattachments: { title: '/woattachments', usage: ['/woattachments '], examples: ['/woattachments 12345678'], }, jirahistory: { title: '/jirahistory', usage: [ '/jirahistory ', '/jirahistory ', '/jirahistory 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 '], 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 **voicestatus + 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 ', '/webexhost list', '/webexhost debug ', ], examples: [ '/webexhost jdoe@company.com', '/webexhost list', '/webexhost debug jdoe@company.com', ], notes: [ '`/webexhost ` 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 ` 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 '], 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', 'voicestatus', 'wanstatus', 'atlasdiag', 'phonediag', 'dectdiag', '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 ` 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)); }