buddy-workbench 0.1.76 → 0.1.77
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 +189 -0
- package/server/routes/overview.js +125 -31
- package/ui/dist/assets/index-CJhf4Me8.css +1 -0
- package/ui/dist/assets/index-WMR27EZa.js +582 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/sw.js +2 -2
- package/ui/dist/assets/index-CIElY9EY.css +0 -1
- package/ui/dist/assets/index-Ci4SjAkd.js +0 -582
package/package.json
CHANGED
|
@@ -230,10 +230,199 @@ router.delete('/templates/:id', (req, res) => {
|
|
|
230
230
|
res.status(204).end();
|
|
231
231
|
});
|
|
232
232
|
|
|
233
|
+
async function getJiraIdentity(jiraHost, token) {
|
|
234
|
+
const headers = { Accept: 'application/json', Authorization: token.startsWith('Bearer ') ? token : `Bearer ${token}` };
|
|
235
|
+
const meResponse = await httpClient.get(`https://${jiraHost}/rest/api/2/myself`, { headers });
|
|
236
|
+
if (meResponse.status < 200 || meResponse.status >= 300) return {};
|
|
237
|
+
const me = meResponse.data || {};
|
|
238
|
+
return { accountId: me.accountId || '', name: me.name || '', displayName: me.displayName || '', emailAddress: me.emailAddress || '' };
|
|
239
|
+
}
|
|
240
|
+
|
|
233
241
|
router.get('/recently-created', (_req, res) => {
|
|
234
242
|
res.json(listRecentlyCreatedJiraIssues());
|
|
235
243
|
});
|
|
236
244
|
|
|
245
|
+
router.get('/recently-commented', async (_req, res) => {
|
|
246
|
+
const jiraHost = getJiraHost();
|
|
247
|
+
if (!jiraHost) return res.json([]);
|
|
248
|
+
const settings = readSettings();
|
|
249
|
+
const token = settings.jiraAccessToken;
|
|
250
|
+
if (!token) return res.json([]);
|
|
251
|
+
|
|
252
|
+
const headers = { Accept: 'application/json', Authorization: token.startsWith('Bearer ') ? token : `Bearer ${token}` };
|
|
253
|
+
try {
|
|
254
|
+
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
|
+
|
|
260
|
+
const issues = response.data?.issues || [];
|
|
261
|
+
const results = [];
|
|
262
|
+
|
|
263
|
+
for (const issue of issues.slice(0, 25)) {
|
|
264
|
+
const key = issue.key;
|
|
265
|
+
const fields = issue.fields || {};
|
|
266
|
+
const commentsUrl = `https://${jiraHost}/rest/api/2/issue/${encodeURIComponent(key)}/comment?orderBy=-updated&maxResults=10`;
|
|
267
|
+
const commentsResponse = await httpClient.get(commentsUrl, { headers }).catch(() => null);
|
|
268
|
+
const comments = commentsResponse?.status === 200 ? (commentsResponse.data?.comments || []) : [];
|
|
269
|
+
|
|
270
|
+
if (comments.length > 0) {
|
|
271
|
+
const myComments = comments.filter((c) => {
|
|
272
|
+
const author = c.author || {};
|
|
273
|
+
return (
|
|
274
|
+
(identity.name && author.name === identity.name) ||
|
|
275
|
+
(identity.accountId && author.accountId === identity.accountId) ||
|
|
276
|
+
(identity.displayName && author.displayName === identity.displayName)
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
if (myComments.length > 0) {
|
|
281
|
+
const latestMyComment = myComments[0];
|
|
282
|
+
const body = typeof latestMyComment.body === 'string' ? latestMyComment.body : JSON.stringify(latestMyComment.body || '');
|
|
283
|
+
const commentSnippet = body.slice(0, 140).replace(/\r?\n+/g, ' ');
|
|
284
|
+
const commentId = latestMyComment.id;
|
|
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
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
results.sort((a, b) => new Date(b.commentedAt || 0).getTime() - new Date(a.commentedAt || 0).getTime());
|
|
301
|
+
res.json(results);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
recordApiError({ source: 'Jira (Recently Commented)', message: error.message });
|
|
304
|
+
res.json([]);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
router.get('/recently-mentioned', async (_req, res) => {
|
|
309
|
+
const jiraHost = getJiraHost();
|
|
310
|
+
if (!jiraHost) return res.json([]);
|
|
311
|
+
const settings = readSettings();
|
|
312
|
+
const token = settings.jiraAccessToken;
|
|
313
|
+
if (!token) return res.json([]);
|
|
314
|
+
|
|
315
|
+
const headers = { Accept: 'application/json', Authorization: token.startsWith('Bearer ') ? token : `Bearer ${token}` };
|
|
316
|
+
try {
|
|
317
|
+
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
|
+
|
|
325
|
+
if (issues.length === 0) {
|
|
326
|
+
try {
|
|
327
|
+
const fallbackUrl = `https://${jiraHost}/rest/api/2/search?jql=${encodeURIComponent('updated >= -30d AND (assignee = currentUser() OR reporter = currentUser() OR watcher = currentUser()) ORDER BY updated DESC')}&maxResults=30&fields=summary,priority,status,updated,assignee,reporter`;
|
|
328
|
+
const response = await httpClient.get(fallbackUrl, { headers });
|
|
329
|
+
if (response.status === 200) issues = response.data?.issues || [];
|
|
330
|
+
} catch {}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const results = [];
|
|
334
|
+
for (const issue of issues.slice(0, 25)) {
|
|
335
|
+
const key = issue.key;
|
|
336
|
+
const fields = issue.fields || {};
|
|
337
|
+
const commentsUrl = `https://${jiraHost}/rest/api/2/issue/${encodeURIComponent(key)}/comment?orderBy=-updated&maxResults=10`;
|
|
338
|
+
const commentsResponse = await httpClient.get(commentsUrl, { headers }).catch(() => null);
|
|
339
|
+
const comments = commentsResponse?.status === 200 ? (commentsResponse.data?.comments || []) : [];
|
|
340
|
+
|
|
341
|
+
const mentionComment = comments.find((c) => {
|
|
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;
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
if (mentionComment) {
|
|
352
|
+
const author = mentionComment.author?.displayName || mentionComment.author?.name || '';
|
|
353
|
+
const body = typeof mentionComment.body === 'string' ? mentionComment.body : JSON.stringify(mentionComment.body || '');
|
|
354
|
+
const snippet = body.slice(0, 140).replace(/\r?\n+/g, ' ');
|
|
355
|
+
const commentId = mentionComment.id;
|
|
356
|
+
|
|
357
|
+
results.push({
|
|
358
|
+
id: `${issue.id}-${commentId || 'm'}`,
|
|
359
|
+
key,
|
|
360
|
+
summary: fields.summary || key,
|
|
361
|
+
priority: fields.priority?.name || '',
|
|
362
|
+
status: fields.status?.name || '',
|
|
363
|
+
mentionedBy: author,
|
|
364
|
+
mentionSnippet: snippet,
|
|
365
|
+
mentionedAt: mentionComment.updated || mentionComment.created || fields.updated,
|
|
366
|
+
url: `https://${jiraHost}/browse/${key}${commentId ? `?focusedCommentId=${commentId}#comment-${commentId}` : ''}`
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
results.sort((a, b) => new Date(b.mentionedAt || 0).getTime() - new Date(a.mentionedAt || 0).getTime());
|
|
372
|
+
res.json(results);
|
|
373
|
+
} catch (error) {
|
|
374
|
+
recordApiError({ source: 'Jira (Recently Mentioned)', message: error.message });
|
|
375
|
+
res.json([]);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
router.get('/recently-updated', async (_req, res) => {
|
|
380
|
+
const jiraHost = getJiraHost();
|
|
381
|
+
if (!jiraHost) return res.json([]);
|
|
382
|
+
const settings = readSettings();
|
|
383
|
+
const token = settings.jiraAccessToken;
|
|
384
|
+
if (!token) return res.json([]);
|
|
385
|
+
|
|
386
|
+
const headers = { Accept: 'application/json', Authorization: token.startsWith('Bearer ') ? token : `Bearer ${token}` };
|
|
387
|
+
try {
|
|
388
|
+
let issues = [];
|
|
389
|
+
try {
|
|
390
|
+
const jql = 'updatedBy(currentUser()) ORDER BY updated DESC';
|
|
391
|
+
const searchUrl = `https://${jiraHost}/rest/api/2/search?jql=${encodeURIComponent(jql)}&maxResults=40&fields=summary,priority,status,updated,duedate,assignee,reporter`;
|
|
392
|
+
const response = await httpClient.get(searchUrl, { headers });
|
|
393
|
+
if (response.status === 200) issues = response.data?.issues || [];
|
|
394
|
+
} catch {}
|
|
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
|
+
}
|
|
404
|
+
|
|
405
|
+
const results = issues.map((issue) => {
|
|
406
|
+
const fields = issue.fields || {};
|
|
407
|
+
return {
|
|
408
|
+
id: issue.id,
|
|
409
|
+
key: issue.key,
|
|
410
|
+
summary: fields.summary || issue.key,
|
|
411
|
+
priority: fields.priority?.name || '',
|
|
412
|
+
status: fields.status?.name || '',
|
|
413
|
+
updatedAt: fields.updated || null,
|
|
414
|
+
dueDate: fields.duedate || null,
|
|
415
|
+
url: `https://${jiraHost}/browse/${issue.key}`
|
|
416
|
+
};
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
res.json(results);
|
|
420
|
+
} catch (error) {
|
|
421
|
+
recordApiError({ source: 'Jira (Recently Updated)', message: error.message });
|
|
422
|
+
res.json([]);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
|
|
237
426
|
// Create filter
|
|
238
427
|
router.post('/', (req, res) => {
|
|
239
428
|
const { name, filterId } = req.body || {};
|
|
@@ -27,6 +27,12 @@ function bitbucketHost(domain) {
|
|
|
27
27
|
return clean.includes('bitbucket') ? clean : `bitbucket.${clean}`;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function confluenceHost(domain) {
|
|
31
|
+
const clean = String(domain || '').replace(/^https?:\/\//i, '').replace(/\/+$/, '');
|
|
32
|
+
if (!clean) return '';
|
|
33
|
+
return clean.includes('confluence') ? clean : `confluence.${clean}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
30
36
|
function jiraUrl(host, path) { return `https://${host}/rest/api/2${path}`; }
|
|
31
37
|
|
|
32
38
|
async function requestSection(name, loader) {
|
|
@@ -39,8 +45,9 @@ async function requestSection(name, loader) {
|
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
async function searchJira(host, token, jql, fields, maxResults = 25) {
|
|
48
|
+
const authHeader = token.startsWith('Bearer ') ? token : `Bearer ${token}`;
|
|
42
49
|
const url = jiraUrl(host, `/search?jql=${encodeURIComponent(jql)}&maxResults=${maxResults}&fields=${encodeURIComponent(fields)}`);
|
|
43
|
-
const response = await httpClient.get(url, { headers: { Accept: 'application/json', Authorization:
|
|
50
|
+
const response = await httpClient.get(url, { headers: { Accept: 'application/json', Authorization: authHeader } });
|
|
44
51
|
if (!isSuccess(response)) throw new Error(`Jira search failed (${response.status}): ${apiMessage(response, 'Request failed')}`);
|
|
45
52
|
return response.data?.issues || [];
|
|
46
53
|
}
|
|
@@ -62,55 +69,128 @@ function mapJiraIssue(issue) {
|
|
|
62
69
|
function issueUrl(host, key) { return `https://${host}/browse/${encodeURIComponent(key)}`; }
|
|
63
70
|
|
|
64
71
|
async function loadJiraData(host, token) {
|
|
65
|
-
const
|
|
72
|
+
const authHeader = token.startsWith('Bearer ') ? token : `Bearer ${token}`;
|
|
73
|
+
const headers = { Accept: 'application/json', Authorization: authHeader };
|
|
66
74
|
const meUrl = jiraUrl(host, '/myself');
|
|
67
75
|
const meResponse = await httpClient.get(meUrl, { headers });
|
|
68
76
|
if (!isSuccess(meResponse)) throw new Error(`Jira identity lookup failed (${meResponse.status}): ${apiMessage(meResponse, 'Request failed')}`);
|
|
69
77
|
const me = meResponse.data || {};
|
|
70
|
-
const identity = { accountId: me.accountId || '', name: me.name || '', displayName: me.displayName || '' };
|
|
78
|
+
const identity = { accountId: me.accountId || '', name: me.name || '', displayName: me.displayName || '', emailAddress: me.emailAddress || '' };
|
|
71
79
|
const fields = 'summary,status,priority,duedate,updated,assignee,reporter';
|
|
72
80
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
// 1. Due today issues
|
|
82
|
+
const due = await searchJira(host, token, 'assignee = currentUser() AND due = startOfDay() AND resolution = Unresolved ORDER BY priority DESC', fields).catch(() => []);
|
|
83
|
+
|
|
84
|
+
// 2. Mentioned / active issues
|
|
85
|
+
let activityIssues = [];
|
|
86
|
+
try {
|
|
87
|
+
activityIssues = await searchJira(host, token, 'text ~ currentUser() ORDER BY updated DESC', fields, 30);
|
|
88
|
+
} catch {
|
|
89
|
+
try {
|
|
90
|
+
activityIssues = await searchJira(host, token, 'updated >= -30d AND (assignee = currentUser() OR reporter = currentUser() OR watcher = currentUser()) ORDER BY updated DESC', fields, 30);
|
|
91
|
+
} catch {
|
|
92
|
+
activityIssues = [];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
77
95
|
|
|
78
96
|
const activityItems = [];
|
|
79
|
-
for (const issue of
|
|
97
|
+
for (const issue of activityIssues.slice(0, 20)) {
|
|
80
98
|
const key = issue.key;
|
|
81
99
|
const commentsUrl = jiraUrl(host, `/issue/${encodeURIComponent(key)}/comment?orderBy=-updated&maxResults=10`);
|
|
82
|
-
const commentsResponse = await httpClient.get(commentsUrl, { headers });
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
const comments = commentsResponse.data?.comments || [];
|
|
88
|
-
const relevant = comments.find((comment) => {
|
|
89
|
-
const author = comment.author || {};
|
|
100
|
+
const commentsResponse = await httpClient.get(commentsUrl, { headers }).catch(() => null);
|
|
101
|
+
const comments = commentsResponse && isSuccess(commentsResponse) ? (commentsResponse.data?.comments || []) : [];
|
|
102
|
+
|
|
103
|
+
const mentionComment = comments.find((comment) => {
|
|
90
104
|
const body = typeof comment.body === 'string' ? comment.body : JSON.stringify(comment.body || '');
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
105
|
+
const author = comment.author || {};
|
|
106
|
+
const mentionsMe = [identity.name, identity.accountId, identity.displayName, identity.emailAddress]
|
|
107
|
+
.filter(Boolean)
|
|
108
|
+
.some((value) => body.toLowerCase().includes(value.toLowerCase()));
|
|
109
|
+
const isNotSelf = author.name !== identity.name && author.accountId !== identity.accountId;
|
|
110
|
+
return mentionsMe && isNotSelf;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const relevantComment = mentionComment || comments.find((comment) => {
|
|
114
|
+
const author = comment.author || {};
|
|
115
|
+
return author.name !== identity.name && author.accountId !== identity.accountId;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const isMention = Boolean(mentionComment);
|
|
119
|
+
const authorName = relevantComment?.author?.displayName || relevantComment?.author?.name || issue.fields?.reporter?.displayName || '';
|
|
120
|
+
const date = relevantComment?.updated || relevantComment?.created || issue.fields?.updated;
|
|
121
|
+
const commentId = relevantComment?.id;
|
|
122
|
+
const itemUrl = `https://${host}/browse/${issue.key}${commentId ? `?focusedCommentId=${commentId}#comment-${commentId}` : ''}`;
|
|
123
|
+
|
|
124
|
+
activityItems.push({
|
|
125
|
+
...mapJiraIssue(issue),
|
|
126
|
+
source: 'Jira',
|
|
127
|
+
sourceType: 'jira',
|
|
128
|
+
author: authorName,
|
|
129
|
+
activityType: isMention ? 'Mentioned in Jira' : 'Commented in Jira',
|
|
130
|
+
activityAt: date,
|
|
131
|
+
url: itemUrl
|
|
94
132
|
});
|
|
95
|
-
if (relevant) {
|
|
96
|
-
activityItems.push({
|
|
97
|
-
...mapJiraIssue(issue),
|
|
98
|
-
activityType: [identity.name, identity.accountId, identity.displayName].filter(Boolean).some((value) => String(relevant.body || '').includes(value)) ? 'Mentioned' : 'Commented',
|
|
99
|
-
activityAt: relevant.updated || relevant.created || issue.fields?.updated
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
133
|
}
|
|
103
134
|
|
|
104
135
|
return {
|
|
105
136
|
identity,
|
|
106
137
|
dueToday: due.map((issue) => ({ ...mapJiraIssue(issue), url: issueUrl(host, issue.key) })),
|
|
107
|
-
activity: activityItems
|
|
138
|
+
activity: activityItems
|
|
108
139
|
};
|
|
109
140
|
}
|
|
110
141
|
|
|
142
|
+
async function loadConfluenceMentions(host, token) {
|
|
143
|
+
if (!host || !token) return [];
|
|
144
|
+
const authHeader = token.startsWith('Bearer ') ? token : `Bearer ${token}`;
|
|
145
|
+
const headers = { Accept: 'application/json', Authorization: authHeader };
|
|
146
|
+
|
|
147
|
+
const cqlQueries = [
|
|
148
|
+
'mention = currentUser() order by lastmodified desc',
|
|
149
|
+
'type in (page, blogpost, comment) and text ~ currentUser() order by lastmodified desc'
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
let results = [];
|
|
153
|
+
for (const cql of cqlQueries) {
|
|
154
|
+
const url = `https://${host}/rest/api/content/search?cql=${encodeURIComponent(cql)}&expand=history.lastUpdated,version,space,container&limit=25`;
|
|
155
|
+
try {
|
|
156
|
+
const response = await httpClient.get(url, { headers });
|
|
157
|
+
if (isSuccess(response) && Array.isArray(response.data?.results) && response.data.results.length > 0) {
|
|
158
|
+
results = response.data.results;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
} catch {}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return results.map((item) => {
|
|
165
|
+
const spaceKey = item.space?.key || item.space?.name || 'Confluence';
|
|
166
|
+
const containerTitle = item.container?.title || '';
|
|
167
|
+
const itemTitle = item.title || (item.type === 'comment' ? 'Comment' : 'Page');
|
|
168
|
+
const displayTitle = containerTitle ? `${containerTitle} (${itemTitle})` : itemTitle;
|
|
169
|
+
const authorName = item.history?.lastUpdated?.by?.displayName || item.version?.by?.displayName || item.history?.createdBy?.displayName || '';
|
|
170
|
+
const date = item.history?.lastUpdated?.when || item.version?.when || item.history?.createdDate;
|
|
171
|
+
const webui = item._links?.webui || '';
|
|
172
|
+
const fullUrl = webui ? (webui.startsWith('http') ? webui : `https://${host}${webui}`) : `https://${host}/pages/viewpage.action?pageId=${item.id}`;
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
id: `confluence-${item.id}`,
|
|
176
|
+
key: spaceKey,
|
|
177
|
+
title: displayTitle,
|
|
178
|
+
summary: displayTitle,
|
|
179
|
+
source: 'Confluence',
|
|
180
|
+
sourceType: 'confluence',
|
|
181
|
+
author: authorName,
|
|
182
|
+
activityType: 'Mentioned in Confluence',
|
|
183
|
+
activityAt: date ? new Date(date).toISOString() : null,
|
|
184
|
+
updated: date ? new Date(date).toISOString() : null,
|
|
185
|
+
url: fullUrl
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
111
190
|
async function loadAuthoredPullRequests(host, token) {
|
|
191
|
+
const authHeader = token.startsWith('Bearer ') ? token : `Bearer ${token}`;
|
|
112
192
|
const url = `https://${host}/rest/api/latest/dashboard/pull-requests?role=author&state=OPEN&limit=100`;
|
|
113
|
-
const response = await httpClient.get(url, { headers: { Accept: 'application/json', Authorization:
|
|
193
|
+
const response = await httpClient.get(url, { headers: { Accept: 'application/json', Authorization: authHeader } });
|
|
114
194
|
if (!isSuccess(response)) throw new Error(`Bitbucket author pull requests failed (${response.status}): ${apiMessage(response, 'Request failed')}`);
|
|
115
195
|
return (response.data?.values || []).map((pr) => ({
|
|
116
196
|
id: pr.id,
|
|
@@ -130,21 +210,35 @@ router.get('/', async (_req, res) => {
|
|
|
130
210
|
}
|
|
131
211
|
const jira = jiraHost(settings.domain);
|
|
132
212
|
const bitbucket = bitbucketHost(settings.domain);
|
|
213
|
+
const confluence = confluenceHost(settings.domain);
|
|
214
|
+
const confluenceToken = settings.confluenceAccessToken || settings.jiraAccessToken;
|
|
215
|
+
|
|
133
216
|
if (!jira || !bitbucket) return res.json({ configured: false, missingTokens: ['domain'], fetchedAt: new Date().toISOString(), categories: {} });
|
|
134
217
|
|
|
135
218
|
const sections = await Promise.all([
|
|
136
219
|
requestSection('Jira', () => loadJiraData(jira, settings.jiraAccessToken)),
|
|
137
|
-
requestSection('Bitbucket Author', () => loadAuthoredPullRequests(bitbucket, settings.bitbucketAccessToken))
|
|
220
|
+
requestSection('Bitbucket Author', () => loadAuthoredPullRequests(bitbucket, settings.bitbucketAccessToken)),
|
|
221
|
+
requestSection('Confluence Mentions', () => loadConfluenceMentions(confluence, confluenceToken))
|
|
138
222
|
]);
|
|
139
|
-
const [jiraData, authoredPrs] = sections;
|
|
223
|
+
const [jiraData, authoredPrs, confluenceData] = sections;
|
|
140
224
|
const todos = listTodos().filter((todo) => !todo.completed && !todo.archived).map((todo) => ({ ...todo, url: '#/todo-list' }));
|
|
141
225
|
const errors = sections.filter((section) => section.error).map((section) => section.error);
|
|
226
|
+
|
|
227
|
+
const allActivity = [
|
|
228
|
+
...(jiraData.items?.activity || []),
|
|
229
|
+
...(confluenceData.items || [])
|
|
230
|
+
].sort((a, b) => {
|
|
231
|
+
const timeA = new Date(a.activityAt || a.updated || 0).getTime();
|
|
232
|
+
const timeB = new Date(b.activityAt || b.updated || 0).getTime();
|
|
233
|
+
return timeB - timeA;
|
|
234
|
+
});
|
|
235
|
+
|
|
142
236
|
res.json({
|
|
143
237
|
configured: true,
|
|
144
238
|
missingTokens: [],
|
|
145
239
|
fetchedAt: new Date().toISOString(),
|
|
146
240
|
categories: {
|
|
147
|
-
activity:
|
|
241
|
+
activity: allActivity,
|
|
148
242
|
jiraDueToday: jiraData.items?.dueToday || [],
|
|
149
243
|
assignedTodos: todos,
|
|
150
244
|
authoredPrs: authoredPrs.items || []
|