buddy-workbench 0.1.77 → 0.1.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/server/routes/jira-filters.js +202 -90
- package/server/routes/overview.js +29 -9
- package/server/routes/pr-conflicts.js +54 -0
- package/server/routes/pr-review.js +15 -2
- package/ui/dist/assets/{index-WMR27EZa.js → index-DEV-rP05.js} +32 -29
- package/ui/dist/assets/index-DaQcmRb2.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/sw.js +2 -2
- package/ui/dist/assets/index-CJhf4Me8.css +0 -1
package/package.json
CHANGED
|
@@ -232,10 +232,121 @@ router.delete('/templates/:id', (req, res) => {
|
|
|
232
232
|
|
|
233
233
|
async function getJiraIdentity(jiraHost, token) {
|
|
234
234
|
const headers = { Accept: 'application/json', Authorization: token.startsWith('Bearer ') ? token : `Bearer ${token}` };
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
235
|
+
try {
|
|
236
|
+
const meResponse = await httpClient.get(`https://${jiraHost}/rest/api/2/myself`, { headers });
|
|
237
|
+
if (meResponse.status >= 200 && meResponse.status < 300 && meResponse.data) {
|
|
238
|
+
const me = meResponse.data;
|
|
239
|
+
return {
|
|
240
|
+
accountId: me.accountId || '',
|
|
241
|
+
name: me.name || me.key || '',
|
|
242
|
+
key: me.key || me.name || '',
|
|
243
|
+
displayName: me.displayName || '',
|
|
244
|
+
emailAddress: me.emailAddress || ''
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
} catch (err) {
|
|
248
|
+
recordApiError({ source: 'Jira API (myself)', method: 'GET', url: `https://${jiraHost}/rest/api/2/myself`, message: err.message });
|
|
249
|
+
}
|
|
250
|
+
return {};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function isAuthorMatch(author, identity) {
|
|
254
|
+
if (!author || typeof author !== 'object') return false;
|
|
255
|
+
const authorName = String(author.name || '').toLowerCase();
|
|
256
|
+
const authorKey = String(author.key || '').toLowerCase();
|
|
257
|
+
const authorAccountId = String(author.accountId || '').toLowerCase();
|
|
258
|
+
const authorDisplayName = String(author.displayName || '').toLowerCase();
|
|
259
|
+
const authorEmail = String(author.emailAddress || '').toLowerCase();
|
|
260
|
+
|
|
261
|
+
if (identity.name && (authorName === identity.name.toLowerCase() || authorKey === identity.name.toLowerCase())) return true;
|
|
262
|
+
if (identity.key && (authorKey === identity.key.toLowerCase() || authorName === identity.key.toLowerCase())) return true;
|
|
263
|
+
if (identity.accountId && authorAccountId === identity.accountId.toLowerCase()) return true;
|
|
264
|
+
if (identity.displayName && authorDisplayName === identity.displayName.toLowerCase()) return true;
|
|
265
|
+
if (identity.emailAddress && authorEmail === identity.emailAddress.toLowerCase()) return true;
|
|
266
|
+
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function extractCommentBodyText(body) {
|
|
271
|
+
if (!body) return '';
|
|
272
|
+
if (typeof body === 'string') return body;
|
|
273
|
+
if (typeof body === 'object') {
|
|
274
|
+
try {
|
|
275
|
+
const extractTextFromAdf = (node) => {
|
|
276
|
+
if (!node) return '';
|
|
277
|
+
if (typeof node === 'string') return node;
|
|
278
|
+
if (node.type === 'text' && node.text) return node.text;
|
|
279
|
+
if (node.type === 'mention' && (node.attrs?.text || node.attrs?.id)) {
|
|
280
|
+
return `${node.attrs.text || node.attrs.id} `;
|
|
281
|
+
}
|
|
282
|
+
if (Array.isArray(node.content)) {
|
|
283
|
+
return node.content.map(extractTextFromAdf).join(' ');
|
|
284
|
+
}
|
|
285
|
+
return '';
|
|
286
|
+
};
|
|
287
|
+
const adfText = extractTextFromAdf(body).trim();
|
|
288
|
+
if (adfText) return adfText;
|
|
289
|
+
return JSON.stringify(body);
|
|
290
|
+
} catch {
|
|
291
|
+
return String(body || '');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return String(body || '');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function isMentionMatch(bodyText, author, identity) {
|
|
298
|
+
if (!bodyText) return false;
|
|
299
|
+
if (isAuthorMatch(author, identity)) return false;
|
|
300
|
+
|
|
301
|
+
const lower = bodyText.toLowerCase();
|
|
302
|
+
const terms = [
|
|
303
|
+
identity.name ? `[~${identity.name.toLowerCase()}]` : null,
|
|
304
|
+
identity.key ? `[~${identity.key.toLowerCase()}]` : null,
|
|
305
|
+
identity.name ? `@${identity.name.toLowerCase()}` : null,
|
|
306
|
+
identity.key ? `@${identity.key.toLowerCase()}` : null,
|
|
307
|
+
identity.name ? identity.name.toLowerCase() : null,
|
|
308
|
+
identity.key ? identity.key.toLowerCase() : null,
|
|
309
|
+
identity.accountId ? identity.accountId.toLowerCase() : null,
|
|
310
|
+
identity.displayName ? identity.displayName.toLowerCase() : null,
|
|
311
|
+
identity.emailAddress ? identity.emailAddress.toLowerCase() : null
|
|
312
|
+
].filter(Boolean);
|
|
313
|
+
|
|
314
|
+
return terms.some((term) => lower.includes(term));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function searchJiraWithFallbacks(jiraHost, headers, jqlCandidates, fields = 'summary,priority,status,updated,assignee,reporter', maxResults = 50) {
|
|
318
|
+
for (const jql of jqlCandidates) {
|
|
319
|
+
if (!jql) continue;
|
|
320
|
+
try {
|
|
321
|
+
const searchUrl = `https://${jiraHost}/rest/api/2/search?jql=${encodeURIComponent(jql)}&maxResults=${maxResults}&fields=${encodeURIComponent(fields)}`;
|
|
322
|
+
const response = await httpClient.get(searchUrl, { headers });
|
|
323
|
+
if (response.status === 200 && Array.isArray(response.data?.issues) && response.data.issues.length > 0) {
|
|
324
|
+
return response.data.issues;
|
|
325
|
+
}
|
|
326
|
+
} catch {}
|
|
327
|
+
}
|
|
328
|
+
return [];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function fetchCommentsForIssue(jiraHost, headers, issueKey, maxResults = 50) {
|
|
332
|
+
try {
|
|
333
|
+
const url = `https://${jiraHost}/rest/api/2/issue/${encodeURIComponent(issueKey)}/comment?maxResults=${maxResults}`;
|
|
334
|
+
const response = await httpClient.get(url, { headers });
|
|
335
|
+
if (response.status !== 200) return [];
|
|
336
|
+
const total = response.data?.total || 0;
|
|
337
|
+
const comments = Array.isArray(response.data?.comments) ? response.data.comments : [];
|
|
338
|
+
if (total > comments.length && total > maxResults) {
|
|
339
|
+
const startAt = Math.max(0, total - maxResults);
|
|
340
|
+
const latestUrl = `https://${jiraHost}/rest/api/2/issue/${encodeURIComponent(issueKey)}/comment?startAt=${startAt}&maxResults=${maxResults}`;
|
|
341
|
+
const latestResponse = await httpClient.get(latestUrl, { headers });
|
|
342
|
+
if (latestResponse.status === 200 && Array.isArray(latestResponse.data?.comments)) {
|
|
343
|
+
return latestResponse.data.comments;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return comments;
|
|
347
|
+
} catch {
|
|
348
|
+
return [];
|
|
349
|
+
}
|
|
239
350
|
}
|
|
240
351
|
|
|
241
352
|
router.get('/recently-created', (_req, res) => {
|
|
@@ -252,49 +363,54 @@ router.get('/recently-commented', async (_req, res) => {
|
|
|
252
363
|
const headers = { Accept: 'application/json', Authorization: token.startsWith('Bearer ') ? token : `Bearer ${token}` };
|
|
253
364
|
try {
|
|
254
365
|
const identity = await getJiraIdentity(jiraHost, token);
|
|
255
|
-
const jql = 'updated >= -60d AND (comment ~ currentUser() OR assignee = currentUser() OR reporter = currentUser() OR watcher = currentUser()) ORDER BY updated DESC';
|
|
256
|
-
const searchUrl = `https://${jiraHost}/rest/api/2/search?jql=${encodeURIComponent(jql)}&maxResults=30&fields=summary,priority,status,updated,assignee,reporter`;
|
|
257
|
-
const response = await httpClient.get(searchUrl, { headers });
|
|
258
|
-
if (response.status < 200 || response.status >= 300) return res.json([]);
|
|
259
366
|
|
|
260
|
-
const
|
|
367
|
+
const jqlCandidates = [];
|
|
368
|
+
if (identity.name) {
|
|
369
|
+
jqlCandidates.push(`comment ~ "\\"${identity.name}\\"" ORDER BY updated DESC`);
|
|
370
|
+
jqlCandidates.push(`comment ~ "${identity.name}" ORDER BY updated DESC`);
|
|
371
|
+
}
|
|
372
|
+
if (identity.displayName && identity.displayName !== identity.name) {
|
|
373
|
+
jqlCandidates.push(`comment ~ "\\"${identity.displayName}\\"" ORDER BY updated DESC`);
|
|
374
|
+
}
|
|
375
|
+
if (identity.accountId) {
|
|
376
|
+
jqlCandidates.push(`comment ~ "${identity.accountId}" ORDER BY updated DESC`);
|
|
377
|
+
}
|
|
378
|
+
jqlCandidates.push('(assignee = currentUser() OR reporter = currentUser() OR watcher = currentUser()) AND updated >= -60d ORDER BY updated DESC');
|
|
379
|
+
jqlCandidates.push('updated >= -60d ORDER BY updated DESC');
|
|
380
|
+
|
|
381
|
+
const issues = await searchJiraWithFallbacks(jiraHost, headers, jqlCandidates, 'summary,priority,status,updated,assignee,reporter', 40);
|
|
261
382
|
const results = [];
|
|
262
383
|
|
|
263
|
-
|
|
384
|
+
const issueCommentsPromises = issues.slice(0, 30).map(async (issue) => {
|
|
264
385
|
const key = issue.key;
|
|
265
386
|
const fields = issue.fields || {};
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
results.push({
|
|
287
|
-
id: `${issue.id}-${commentId || 'c'}`,
|
|
288
|
-
key,
|
|
289
|
-
summary: fields.summary || key,
|
|
290
|
-
priority: fields.priority?.name || '',
|
|
291
|
-
status: fields.status?.name || '',
|
|
292
|
-
commentSnippet,
|
|
293
|
-
commentedAt: latestMyComment.updated || latestMyComment.created || fields.updated,
|
|
294
|
-
url: `https://${jiraHost}/browse/${key}${commentId ? `?focusedCommentId=${commentId}#comment-${commentId}` : ''}`
|
|
295
|
-
});
|
|
296
|
-
}
|
|
387
|
+
const comments = await fetchCommentsForIssue(jiraHost, headers, key, 50);
|
|
388
|
+
|
|
389
|
+
const myComments = comments.filter((c) => isAuthorMatch(c.author || c.updateAuthor, identity));
|
|
390
|
+
if (myComments.length > 0) {
|
|
391
|
+
myComments.sort((a, b) => new Date(b.updated || b.created || 0).getTime() - new Date(a.updated || a.created || 0).getTime());
|
|
392
|
+
const latestMyComment = myComments[0];
|
|
393
|
+
const bodyText = extractCommentBodyText(latestMyComment.body);
|
|
394
|
+
const commentSnippet = bodyText.slice(0, 140).replace(/\r?\n+/g, ' ');
|
|
395
|
+
const commentId = latestMyComment.id;
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
id: `${issue.id}-${commentId || 'c'}`,
|
|
399
|
+
key,
|
|
400
|
+
summary: fields.summary || key,
|
|
401
|
+
priority: fields.priority?.name || '',
|
|
402
|
+
status: fields.status?.name || '',
|
|
403
|
+
commentSnippet,
|
|
404
|
+
commentedAt: latestMyComment.updated || latestMyComment.created || fields.updated,
|
|
405
|
+
url: `https://${jiraHost}/browse/${key}${commentId ? `?focusedCommentId=${commentId}#comment-${commentId}` : ''}`
|
|
406
|
+
};
|
|
297
407
|
}
|
|
408
|
+
return null;
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const settled = await Promise.all(issueCommentsPromises);
|
|
412
|
+
for (const item of settled) {
|
|
413
|
+
if (item) results.push(item);
|
|
298
414
|
}
|
|
299
415
|
|
|
300
416
|
results.sort((a, b) => new Date(b.commentedAt || 0).getTime() - new Date(a.commentedAt || 0).getTime());
|
|
@@ -315,46 +431,44 @@ router.get('/recently-mentioned', async (_req, res) => {
|
|
|
315
431
|
const headers = { Accept: 'application/json', Authorization: token.startsWith('Bearer ') ? token : `Bearer ${token}` };
|
|
316
432
|
try {
|
|
317
433
|
const identity = await getJiraIdentity(jiraHost, token);
|
|
318
|
-
let issues = [];
|
|
319
|
-
try {
|
|
320
|
-
const searchUrl = `https://${jiraHost}/rest/api/2/search?jql=${encodeURIComponent('text ~ currentUser() ORDER BY updated DESC')}&maxResults=30&fields=summary,priority,status,updated,assignee,reporter`;
|
|
321
|
-
const response = await httpClient.get(searchUrl, { headers });
|
|
322
|
-
if (response.status === 200) issues = response.data?.issues || [];
|
|
323
|
-
} catch {}
|
|
324
434
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
435
|
+
const jqlCandidates = [];
|
|
436
|
+
if (identity.name) {
|
|
437
|
+
jqlCandidates.push(`text ~ "[~${identity.name}]" OR comment ~ "[~${identity.name}]" ORDER BY updated DESC`);
|
|
438
|
+
jqlCandidates.push(`text ~ "${identity.name}" OR comment ~ "${identity.name}" ORDER BY updated DESC`);
|
|
439
|
+
jqlCandidates.push(`text ~ "\\"${identity.name}\\"" ORDER BY updated DESC`);
|
|
440
|
+
}
|
|
441
|
+
if (identity.displayName && identity.displayName !== identity.name) {
|
|
442
|
+
jqlCandidates.push(`text ~ "\\"${identity.displayName}\\"" OR comment ~ "\\"${identity.displayName}\\"" ORDER BY updated DESC`);
|
|
443
|
+
}
|
|
444
|
+
if (identity.accountId) {
|
|
445
|
+
jqlCandidates.push(`text ~ "${identity.accountId}" OR comment ~ "${identity.accountId}" ORDER BY updated DESC`);
|
|
331
446
|
}
|
|
447
|
+
jqlCandidates.push('(assignee = currentUser() OR reporter = currentUser() OR watcher = currentUser()) AND updated >= -60d ORDER BY updated DESC');
|
|
448
|
+
jqlCandidates.push('updated >= -60d ORDER BY updated DESC');
|
|
332
449
|
|
|
450
|
+
const issues = await searchJiraWithFallbacks(jiraHost, headers, jqlCandidates, 'summary,priority,status,updated,assignee,reporter', 40);
|
|
333
451
|
const results = [];
|
|
334
|
-
|
|
452
|
+
|
|
453
|
+
const issueMentionsPromises = issues.slice(0, 30).map(async (issue) => {
|
|
335
454
|
const key = issue.key;
|
|
336
455
|
const fields = issue.fields || {};
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const body = typeof c.body === 'string' ? c.body : JSON.stringify(c.body || '');
|
|
343
|
-
const author = c.author || {};
|
|
344
|
-
const isNotMe = author.name !== identity.name && author.accountId !== identity.accountId;
|
|
345
|
-
const mentionsMe = [identity.name, identity.accountId, identity.displayName, identity.emailAddress]
|
|
346
|
-
.filter(Boolean)
|
|
347
|
-
.some((val) => body.toLowerCase().includes(val.toLowerCase()));
|
|
348
|
-
return mentionsMe && isNotMe;
|
|
456
|
+
const comments = await fetchCommentsForIssue(jiraHost, headers, key, 50);
|
|
457
|
+
|
|
458
|
+
const mentionComments = comments.filter((c) => {
|
|
459
|
+
const bodyText = extractCommentBodyText(c.body);
|
|
460
|
+
return isMentionMatch(bodyText, c.author || c.updateAuthor, identity);
|
|
349
461
|
});
|
|
350
462
|
|
|
351
|
-
if (
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
const
|
|
355
|
-
const
|
|
463
|
+
if (mentionComments.length > 0) {
|
|
464
|
+
mentionComments.sort((a, b) => new Date(b.updated || b.created || 0).getTime() - new Date(a.updated || a.created || 0).getTime());
|
|
465
|
+
const latestMention = mentionComments[0];
|
|
466
|
+
const author = latestMention.author?.displayName || latestMention.author?.name || 'Someone';
|
|
467
|
+
const bodyText = extractCommentBodyText(latestMention.body);
|
|
468
|
+
const snippet = bodyText.slice(0, 140).replace(/\r?\n+/g, ' ');
|
|
469
|
+
const commentId = latestMention.id;
|
|
356
470
|
|
|
357
|
-
|
|
471
|
+
return {
|
|
358
472
|
id: `${issue.id}-${commentId || 'm'}`,
|
|
359
473
|
key,
|
|
360
474
|
summary: fields.summary || key,
|
|
@@ -362,10 +476,16 @@ router.get('/recently-mentioned', async (_req, res) => {
|
|
|
362
476
|
status: fields.status?.name || '',
|
|
363
477
|
mentionedBy: author,
|
|
364
478
|
mentionSnippet: snippet,
|
|
365
|
-
mentionedAt:
|
|
479
|
+
mentionedAt: latestMention.updated || latestMention.created || fields.updated,
|
|
366
480
|
url: `https://${jiraHost}/browse/${key}${commentId ? `?focusedCommentId=${commentId}#comment-${commentId}` : ''}`
|
|
367
|
-
}
|
|
481
|
+
};
|
|
368
482
|
}
|
|
483
|
+
return null;
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const settled = await Promise.all(issueMentionsPromises);
|
|
487
|
+
for (const item of settled) {
|
|
488
|
+
if (item) results.push(item);
|
|
369
489
|
}
|
|
370
490
|
|
|
371
491
|
results.sort((a, b) => new Date(b.mentionedAt || 0).getTime() - new Date(a.mentionedAt || 0).getTime());
|
|
@@ -385,22 +505,14 @@ router.get('/recently-updated', async (_req, res) => {
|
|
|
385
505
|
|
|
386
506
|
const headers = { Accept: 'application/json', Authorization: token.startsWith('Bearer ') ? token : `Bearer ${token}` };
|
|
387
507
|
try {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if (issues.length === 0) {
|
|
397
|
-
try {
|
|
398
|
-
const fallbackJql = '(assignee = currentUser() OR reporter = currentUser()) AND updated >= -30d ORDER BY updated DESC';
|
|
399
|
-
const searchUrl = `https://${jiraHost}/rest/api/2/search?jql=${encodeURIComponent(fallbackJql)}&maxResults=40&fields=summary,priority,status,updated,duedate,assignee,reporter`;
|
|
400
|
-
const response = await httpClient.get(searchUrl, { headers });
|
|
401
|
-
if (response.status === 200) issues = response.data?.issues || [];
|
|
402
|
-
} catch {}
|
|
403
|
-
}
|
|
508
|
+
const jqlCandidates = [
|
|
509
|
+
'updatedBy(currentUser()) ORDER BY updated DESC',
|
|
510
|
+
'(assignee = currentUser() OR reporter = currentUser() OR watcher = currentUser()) AND updated >= -60d ORDER BY updated DESC',
|
|
511
|
+
'assignee = currentUser() ORDER BY updated DESC',
|
|
512
|
+
'updated >= -60d ORDER BY updated DESC'
|
|
513
|
+
];
|
|
514
|
+
|
|
515
|
+
const issues = await searchJiraWithFallbacks(jiraHost, headers, jqlCandidates, 'summary,priority,status,updated,duedate,assignee,reporter', 40);
|
|
404
516
|
|
|
405
517
|
const results = issues.map((issue) => {
|
|
406
518
|
const fields = issue.fields || {};
|
|
@@ -83,22 +83,42 @@ async function loadJiraData(host, token) {
|
|
|
83
83
|
|
|
84
84
|
// 2. Mentioned / active issues
|
|
85
85
|
let activityIssues = [];
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
const jqlCandidates = [];
|
|
87
|
+
if (identity.name) {
|
|
88
|
+
jqlCandidates.push(`text ~ "[~${identity.name}]" OR comment ~ "[~${identity.name}]" ORDER BY updated DESC`);
|
|
89
|
+
jqlCandidates.push(`text ~ "${identity.name}" OR comment ~ "${identity.name}" ORDER BY updated DESC`);
|
|
90
|
+
}
|
|
91
|
+
if (identity.displayName && identity.displayName !== identity.name) {
|
|
92
|
+
jqlCandidates.push(`text ~ "\\"${identity.displayName}\\"" ORDER BY updated DESC`);
|
|
93
|
+
}
|
|
94
|
+
if (identity.accountId) {
|
|
95
|
+
jqlCandidates.push(`text ~ "${identity.accountId}" OR comment ~ "${identity.accountId}" ORDER BY updated DESC`);
|
|
96
|
+
}
|
|
97
|
+
jqlCandidates.push('updated >= -30d AND (assignee = currentUser() OR reporter = currentUser() OR watcher = currentUser()) ORDER BY updated DESC');
|
|
98
|
+
jqlCandidates.push('updated >= -30d ORDER BY updated DESC');
|
|
99
|
+
|
|
100
|
+
for (const jql of jqlCandidates) {
|
|
89
101
|
try {
|
|
90
|
-
activityIssues = await searchJira(host, token,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
102
|
+
activityIssues = await searchJira(host, token, jql, fields, 30);
|
|
103
|
+
if (activityIssues.length > 0) break;
|
|
104
|
+
} catch {}
|
|
94
105
|
}
|
|
95
106
|
|
|
96
107
|
const activityItems = [];
|
|
97
108
|
for (const issue of activityIssues.slice(0, 20)) {
|
|
98
109
|
const key = issue.key;
|
|
99
|
-
const commentsUrl = jiraUrl(host, `/issue/${encodeURIComponent(key)}/comment?
|
|
110
|
+
const commentsUrl = jiraUrl(host, `/issue/${encodeURIComponent(key)}/comment?maxResults=50`);
|
|
100
111
|
const commentsResponse = await httpClient.get(commentsUrl, { headers }).catch(() => null);
|
|
101
|
-
|
|
112
|
+
let comments = commentsResponse && isSuccess(commentsResponse) ? (commentsResponse.data?.comments || []) : [];
|
|
113
|
+
const total = commentsResponse?.data?.total || 0;
|
|
114
|
+
if (total > comments.length && total > 50) {
|
|
115
|
+
const startAt = Math.max(0, total - 50);
|
|
116
|
+
const latestUrl = jiraUrl(host, `/issue/${encodeURIComponent(key)}/comment?startAt=${startAt}&maxResults=50`);
|
|
117
|
+
const latestRes = await httpClient.get(latestUrl, { headers }).catch(() => null);
|
|
118
|
+
if (latestRes && isSuccess(latestRes) && Array.isArray(latestRes.data?.comments)) {
|
|
119
|
+
comments = latestRes.data.comments;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
102
122
|
|
|
103
123
|
const mentionComment = comments.find((comment) => {
|
|
104
124
|
const body = typeof comment.body === 'string' ? comment.body : JSON.stringify(comment.body || '');
|
|
@@ -177,6 +177,60 @@ async function readFile({ host, projectKey, repositorySlug, commit, path }) {
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
router.post('/load', async (req, res) => {
|
|
180
|
+
const prUrl = String(req.body?.prUrl || '').trim();
|
|
181
|
+
if (prUrl.toLowerCase().includes('demo') || prUrl.toLowerCase().includes('mock')) {
|
|
182
|
+
return res.json({
|
|
183
|
+
conflicted: true,
|
|
184
|
+
pr: {
|
|
185
|
+
title: 'Demo: Feature User Management with Conflict',
|
|
186
|
+
sourceBranch: 'feature/user-management-v2',
|
|
187
|
+
targetBranch: 'main'
|
|
188
|
+
},
|
|
189
|
+
files: [
|
|
190
|
+
{
|
|
191
|
+
path: 'src/components/UserProfile.tsx',
|
|
192
|
+
sourceContent: `import React, { useState } from 'react';
|
|
193
|
+
import { Card, Button } from '@acme/ui';
|
|
194
|
+
|
|
195
|
+
// Source branch: contains upgraded configuration URL and new permissions check
|
|
196
|
+
export const PROFILE_ENDPOINT_URL = 'https://api.internal.company.com/v2/users/profile';
|
|
197
|
+
|
|
198
|
+
export function UserProfile({ userId }: { userId: string }) {
|
|
199
|
+
const [activeTab, setActiveTab] = useState('overview');
|
|
200
|
+
return (
|
|
201
|
+
<Card title="User Profile (V2)">
|
|
202
|
+
<p>Source branch implementation with updated permissions model and telemetry hooks.</p>
|
|
203
|
+
<Button onClick={() => setActiveTab('details')}>View Details</Button>
|
|
204
|
+
</Card>
|
|
205
|
+
);
|
|
206
|
+
}`,
|
|
207
|
+
targetContent: `import React, { useState } from 'react';
|
|
208
|
+
import { Card, Button } from '@acme/ui';
|
|
209
|
+
|
|
210
|
+
// Target branch: contains legacy configuration URL
|
|
211
|
+
export const PROFILE_ENDPOINT_URL = 'https://legacy-api.internal.company.com/v1/users/profile';
|
|
212
|
+
|
|
213
|
+
export function UserProfile({ userId }: { userId: string }) {
|
|
214
|
+
const [tab, setTab] = useState('summary');
|
|
215
|
+
return (
|
|
216
|
+
<Card title="User Profile (Main)">
|
|
217
|
+
<p>Main branch implementation with legacy summary view.</p>
|
|
218
|
+
<Button onClick={() => setTab('summary')}>Summary</Button>
|
|
219
|
+
</Card>
|
|
220
|
+
);
|
|
221
|
+
}`,
|
|
222
|
+
defaultChoice: 'source'
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
path: 'src/config/application-settings.json',
|
|
226
|
+
sourceContent: `{\n "name": "enterprise-workbench",\n "version": "2.4.0",\n "endpoints": {\n "auth": "https://auth.internal.company.com/oauth2/token",\n "storage": "https://storage.internal.company.com/data"\n }\n}`,
|
|
227
|
+
targetContent: `{\n "name": "enterprise-workbench",\n "version": "2.3.1",\n "endpoints": {\n "auth": "https://auth-legacy.internal.company.com/token",\n "storage": "https://storage.internal.company.com/data"\n }\n}`,
|
|
228
|
+
defaultChoice: 'target'
|
|
229
|
+
}
|
|
230
|
+
]
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
180
234
|
const parsed = parsePrUrl(req.body?.prUrl);
|
|
181
235
|
if (!parsed) return res.status(400).json({ error: 'Invalid Bitbucket Server Pull Request URL.' });
|
|
182
236
|
|
|
@@ -119,7 +119,8 @@ async function runAiReview({ aiApiHost, pr, files }) {
|
|
|
119
119
|
'Focus on correctness, security, reliability, maintainability, and regressions introduced by the change.',
|
|
120
120
|
'Report only actionable findings. For each finding include severity (critical, warning, or info), file, line if identifiable, title, explanation, and a concrete recommendation.',
|
|
121
121
|
'If there are no actionable findings, use an empty findings array and say LGTM in the summary.',
|
|
122
|
-
'
|
|
122
|
+
'Format the "summary" field in rich, well-structured Markdown (with headings, bullet points, bold highlights, code blocks, etc.) providing an executive review overview, risk assessment, and recommendations.',
|
|
123
|
+
'Return only valid JSON in this exact shape: {"summary":"# PR Review Summary\\n\\n...Markdown content...","findings":[{"severity":"warning","file":"path/to/file","line":12,"title":"...","explanation":"...","recommendation":"..."}]}',
|
|
123
124
|
'Do not invent files, code, or line numbers. Use null for line when a precise line cannot be identified.',
|
|
124
125
|
'',
|
|
125
126
|
`Pull request: ${pr.title || ''}`,
|
|
@@ -804,7 +805,19 @@ router.post('/check', async (req, res) => {
|
|
|
804
805
|
status: 'completed',
|
|
805
806
|
source: 'demo',
|
|
806
807
|
reviewedFiles: aiFiles.length,
|
|
807
|
-
summary:
|
|
808
|
+
summary: `### 📋 PR Review Executive Summary
|
|
809
|
+
|
|
810
|
+
The pull request introduces feature updates across **${aiFiles.length} files**. The overall design is coherent, but several **critical quality**, **type-safety**, and **accessibility** concerns require attention before merge.
|
|
811
|
+
|
|
812
|
+
#### 🚨 Key Action Items
|
|
813
|
+
- **Type Safety**: Avoid using \`any\` in \`UserProfile.tsx\` (\`payload: any\`). Strongly type the interface.
|
|
814
|
+
- **State Mutation**: In \`useUserStats.ts\`, \`state.list.push()\` directly mutates state. Use immutable state updates.
|
|
815
|
+
- **Accessibility (a11y)**: In \`Dashboard.tsx\`, \`<div className="btn-wrapper" onClick={...}>\` is not keyboard accessible. Switch to \`<button>\` or an interactive component.
|
|
816
|
+
- **Debugging Artifacts**: Remove \`debugger;\` and debug \`console.log\` statements in \`api.ts\`.
|
|
817
|
+
|
|
818
|
+
#### 💡 Recommendations
|
|
819
|
+
1. Validate external API payload shapes defensively at runtime.
|
|
820
|
+
2. Ensure long configuration strings and secrets are loaded via environment variables rather than hardcoded URLs.`,
|
|
808
821
|
content: 'Demo AI review: found several actionable quality and accessibility issues.',
|
|
809
822
|
findings: demoAiFindings
|
|
810
823
|
};
|