Docker hosts default to UTC, so bare toLocaleTimeString() showed wrong "Last checked" times in avstatus and other commands. Add formatDisplayTime() (default America/New_York, overridable via DISPLAY_TIMEZONE) and use it across renderers and command footers.
147 lines
No EOL
6 KiB
JavaScript
147 lines
No EOL
6 KiB
JavaScript
// src/commands/jiraHistory.js
|
|
import {
|
|
getJiraTicketsForStore,
|
|
getJiraTicketsForComponentWithStore,
|
|
getStatusEmoji,
|
|
calculateDaysOpen
|
|
} from '../services/jiraService.js';
|
|
import { summarizeJiraTicket } from '../services/jiraSummarizer.js';
|
|
import { analyzeCommonIssues } from '../services/jiraSummarizer.js';
|
|
import jira from '../integrations/jira/JiraClient.js';
|
|
import { formatDisplayTime } from '../utils/time.js';
|
|
import { logger } from '../utils/logger.js';
|
|
|
|
const MAX_TICKETS = 20;
|
|
|
|
export async function handleJiraHistory(bot, trigger) {
|
|
logger('jira:history', 'Handler entered', 'debug');
|
|
|
|
// Support both Webex (args) and HTTP (query)
|
|
const query = trigger.query || {};
|
|
const args = trigger.args || [];
|
|
|
|
const arg1 = args[0]?.trim() || query.storeNum || query.store || query.s;
|
|
const arg2 = args[1]?.trim() || query.component || query.components;
|
|
|
|
logger('jira:history', `Request for store/component: ${arg1} | ${arg2 || 'all'}`);
|
|
|
|
if (!arg1) {
|
|
const usage = '**Jira History Usage:**\n' +
|
|
'`/jiraHistory 782` → All tickets (max 20)\n' +
|
|
'`/jiraHistory 782 phone` → Communication Services only\n' +
|
|
'`/jiraHistory 782 av,voice,mobility` → Multiple components';
|
|
|
|
await bot.say('markdown', usage);
|
|
return;
|
|
}
|
|
|
|
if (!/^\d{2,4}$/.test(arg1)) {
|
|
await bot.say('markdown', 'Please provide a valid 2-4 digit store number.');
|
|
return;
|
|
}
|
|
|
|
const storeNum = arg1.padStart(5, '0'); // normalize to 5 digits if needed
|
|
|
|
try {
|
|
let reply = `**Jira Tickets - Store ${storeNum}**\n\n`;
|
|
|
|
let ticketKeys = [];
|
|
|
|
if (!arg2 || arg2.toLowerCase() === 'all') {
|
|
logger('jira:history', `Fetching all tickets for store ${storeNum}`, 'debug');
|
|
const results = await getJiraTicketsForStore(storeNum);
|
|
ticketKeys = results.map(t => t.key);
|
|
} else {
|
|
const components = arg2.split(',').map(c => c.trim().toLowerCase());
|
|
logger('jira:history', `Fetching tickets for components: ${components.join(', ')}`, 'debug');
|
|
|
|
for (const comp of components) {
|
|
const results = await getJiraTicketsForComponentWithStore(storeNum, comp);
|
|
ticketKeys = ticketKeys.concat(results.map(t => t.key));
|
|
}
|
|
}
|
|
|
|
ticketKeys = [...new Set(ticketKeys)]; // dedup across components if any
|
|
|
|
// Limit to most recent
|
|
if (ticketKeys.length > MAX_TICKETS) {
|
|
ticketKeys = ticketKeys.slice(0, MAX_TICKETS);
|
|
}
|
|
|
|
if (ticketKeys.length === 0) {
|
|
reply += 'No matching Jira tickets found.\n';
|
|
} else {
|
|
logger('jira:history', `Fetching details for ${ticketKeys.length} tickets in parallel`, 'debug');
|
|
|
|
const ticketPromises = ticketKeys.map(async (key) => {
|
|
try {
|
|
const fullTicket = await jira.getTicket(key);
|
|
const aiSummary = await summarizeJiraTicket(fullTicket).catch(() =>
|
|
"**Reported Problem:** No details available.\n**Steps Taken:** No information recorded."
|
|
);
|
|
|
|
const fields = fullTicket.fields || {};
|
|
const statusEmoji = getStatusEmoji(fields.status?.name);
|
|
const component = fields.components?.[0]?.name || '—';
|
|
const assignee = fields.assignee?.displayName || '**Unassigned**';
|
|
const days = calculateDaysOpen(fields.created, fields.resolved || fields.updated);
|
|
|
|
return {
|
|
key,
|
|
summary: fields.summary || '—',
|
|
statusEmoji,
|
|
status: fields.status?.name || '—',
|
|
component,
|
|
assignee,
|
|
days,
|
|
aiSummary: aiSummary.substring(0, 800) // Limit summary length
|
|
};
|
|
} catch (err) {
|
|
logger('jira:history', `Failed to process ticket ${key}: ${err.message}`, 'warn');
|
|
return null;
|
|
}
|
|
});
|
|
|
|
const ticketResults = (await Promise.all(ticketPromises)).filter(Boolean);
|
|
|
|
// Build reply with length control
|
|
let currentLength = reply.length;
|
|
|
|
for (const t of ticketResults) {
|
|
let ticketBlock = `**${t.key}** - ${t.summary}\n\n`;
|
|
ticketBlock += `${t.aiSummary}\n\n`;
|
|
ticketBlock += `${t.statusEmoji} **Status:** ${t.status} • Component: ${t.component}\n`;
|
|
ticketBlock += `Assigned: ${t.assignee} • Open for: ${t.days} days\n\n`;
|
|
ticketBlock += `---\n\n`;
|
|
|
|
// Check if adding this ticket would exceed safe limit
|
|
if (currentLength + ticketBlock.length > 6500) {
|
|
reply += `\n**... and ${ticketResults.length - ticketResults.indexOf(t)} more tickets.**\n`;
|
|
reply += `Use /jiraHistory ${storeNum} for full list.\n`;
|
|
break;
|
|
}
|
|
|
|
reply += ticketBlock;
|
|
currentLength += ticketBlock.length;
|
|
}
|
|
|
|
// Common Issues Analysis (only if we have enough tickets and space)
|
|
if (ticketResults.length >= 3 && currentLength < 6000) {
|
|
reply += `**Common Issues Across These Tickets:**\n`;
|
|
const common = await analyzeCommonIssues(ticketResults).catch(() =>
|
|
"Unable to identify common patterns at this time."
|
|
);
|
|
reply += `${common}\n`;
|
|
}
|
|
}
|
|
|
|
reply += `\n*Last checked: ${formatDisplayTime()}*`;
|
|
await bot.say('markdown', reply.trim());
|
|
|
|
} catch (err) {
|
|
logger('jira:history', `Error processing jiraHistory: ${err.message}`, 'error');
|
|
await bot.say('markdown', `❌ Error retrieving Jira history: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
// (helpers imported from jiraService for consistency with jiraTicket etc.)
|