// src/commands/jiraPoll.js // // /jirapoll — run the hourly Jira poller ON DEMAND. Same code // path as the cron job. Enriches any unlabeled // matching tickets with a phone/av snapshot comment, // labels them, and posts the standard summary to // JIRA_POLLER_ROOM_ID if any were enriched. Also // replies in the invoking chat with the counts so // you get immediate feedback. // // /jirapoll prime — run in PRIME mode (labels every matching ticket // without enriching or notifying). Equivalent to a // one-off JIRA_POLLER_PRIME_ON_START=true restart. // // Auth model // The command is `mutating: true` in the registry, so hitting the // HTTP path requires HTTP_API_TOKEN. From Webex chat any user who // can talk to the bot can run it — same trust model as the other // mutating chat commands (`offboarduser`, `webexhost`, etc.). // // Concurrency // `pollNewTickets` uses Jira labels for idempotency, so overlapping // invocations are safe (each ticket can only be enriched once). If // two people trigger `/jirapoll` at the same second, they'll each // process disjoint slices of the label-race — no double comments. import { pollNewTickets } from '../services/jiraPollerService.js'; import { extractRequester, describeRequester } from '../utils/requester.js'; import { logger } from '../utils/logger.js'; export async function handleJiraPoll(bot, trigger) { const requester = extractRequester(trigger); const args = trigger.args || []; const query = trigger.query || {}; const primeArg = (args[0] || query.mode || '').toLowerCase(); const isPrime = primeArg === 'prime'; logger('jira:poll:cmd', `Requested by ${describeRequester(requester)}${isPrime ? ' (PRIME mode)' : ''}`); // Acknowledge immediately — a full poll can run 15-30s at N=7 or // 60s+ at N=50, and the operator shouldn't stare at a blank chat. const ackLines = [ isPrime ? '⏳ Running Jira poller in **PRIME mode** — will label matching tickets without enriching…' : '⏳ Running Jira poller on demand — this may take up to a minute for a full 50-ticket batch…', ]; await bot.say('markdown', ackLines.join('\n')); const startedAt = Date.now(); let result; try { result = await pollNewTickets({ prime: isPrime }); } catch (err) { logger('jira:poll:cmd', `Poll failed: ${err.message}`, 'error'); await bot.say('markdown', `❌ Poll failed: \`${err.message}\``); return; } const elapsedSec = ((Date.now() - startedAt) / 1000).toFixed(1); if (isPrime) { const primed = result.primed ?? 0; const skipped = result.skipped ?? 0; await bot.say( 'markdown', `✅ **Prime pass complete** (${elapsedSec}s)\n` + `Labeled **${primed}** ticket(s) as \`bot-enriched\` without enrichment.` + (skipped ? ` ${skipped} labeling failure(s) — check logs.` : ''), ); return; } const enriched = result.enriched ?? 0; const skipped = result.skipped ?? 0; const tokens = result.tokensUsed ?? 0; const lines = [`✅ **Poll complete** (${elapsedSec}s, ${tokens} AI tokens)`]; lines.push(`Enriched: **${enriched}**, skipped: **${skipped}**`); if (enriched > 0 && process.env.JIRA_POLLER_ROOM_ID) { // Note the standard summary that already went to the configured // room so the invoker knows where the per-ticket detail lives. lines.push(''); lines.push(`_Per-ticket detail posted to the configured summary room._`); } else if (enriched === 0) { lines.push(''); lines.push('_Nothing to enrich right now._'); } await bot.say('markdown', lines.join('\n')); }