@equilateral_ai/mindmeld 3.0.0
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/README.md +300 -0
- package/hooks/README.md +494 -0
- package/hooks/pre-compact.js +392 -0
- package/hooks/session-start.js +264 -0
- package/package.json +90 -0
- package/scripts/harvest.js +561 -0
- package/scripts/init-project.js +437 -0
- package/scripts/inject.js +388 -0
- package/src/collaboration/CollaborationPrompt.js +460 -0
- package/src/core/AlertEngine.js +813 -0
- package/src/core/AlertNotifier.js +363 -0
- package/src/core/CorrelationAnalyzer.js +774 -0
- package/src/core/CurationEngine.js +688 -0
- package/src/core/LLMPatternDetector.js +508 -0
- package/src/core/LoadBearingDetector.js +242 -0
- package/src/core/NotificationService.js +1032 -0
- package/src/core/PatternValidator.js +355 -0
- package/src/core/README.md +160 -0
- package/src/core/RapportOrchestrator.js +446 -0
- package/src/core/RelevanceDetector.js +577 -0
- package/src/core/StandardsIngestion.js +575 -0
- package/src/core/TeamLoadBearingDetector.js +431 -0
- package/src/database/dbOperations.js +105 -0
- package/src/handlers/activity/activityGetMe.js +98 -0
- package/src/handlers/activity/activityGetTeam.js +130 -0
- package/src/handlers/alerts/alertsAcknowledge.js +91 -0
- package/src/handlers/alerts/alertsGet.js +250 -0
- package/src/handlers/collaborators/collaboratorAdd.js +201 -0
- package/src/handlers/collaborators/collaboratorInvite.js +218 -0
- package/src/handlers/collaborators/collaboratorList.js +88 -0
- package/src/handlers/collaborators/collaboratorRemove.js +127 -0
- package/src/handlers/collaborators/inviteAccept.js +122 -0
- package/src/handlers/context/contextGet.js +57 -0
- package/src/handlers/context/invariantsGet.js +74 -0
- package/src/handlers/context/loopsGet.js +82 -0
- package/src/handlers/context/notesCreate.js +74 -0
- package/src/handlers/context/purposeGet.js +78 -0
- package/src/handlers/correlations/correlationsDeveloperGet.js +226 -0
- package/src/handlers/correlations/correlationsGet.js +93 -0
- package/src/handlers/correlations/correlationsProjectGet.js +161 -0
- package/src/handlers/github/githubConnectionStatus.js +49 -0
- package/src/handlers/github/githubDiscoverPatterns.js +364 -0
- package/src/handlers/github/githubOAuthCallback.js +166 -0
- package/src/handlers/github/githubOAuthStart.js +59 -0
- package/src/handlers/github/githubPatternsReview.js +109 -0
- package/src/handlers/github/githubReposList.js +105 -0
- package/src/handlers/helpers/checkSuperAdmin.js +85 -0
- package/src/handlers/helpers/dbOperations.js +53 -0
- package/src/handlers/helpers/errorHandler.js +49 -0
- package/src/handlers/helpers/index.js +106 -0
- package/src/handlers/helpers/lambdaWrapper.js +60 -0
- package/src/handlers/helpers/responseUtil.js +55 -0
- package/src/handlers/helpers/subscriptionTiers.js +1168 -0
- package/src/handlers/notifications/getPreferences.js +84 -0
- package/src/handlers/notifications/sendNotification.js +170 -0
- package/src/handlers/notifications/updatePreferences.js +316 -0
- package/src/handlers/patterns/patternUsagePost.js +182 -0
- package/src/handlers/patterns/patternViolationPost.js +185 -0
- package/src/handlers/projects/projectCreate.js +107 -0
- package/src/handlers/projects/projectDelete.js +82 -0
- package/src/handlers/projects/projectGet.js +95 -0
- package/src/handlers/projects/projectUpdate.js +118 -0
- package/src/handlers/reports/aiLeverage.js +206 -0
- package/src/handlers/reports/engineeringInvestment.js +132 -0
- package/src/handlers/reports/riskForecast.js +186 -0
- package/src/handlers/reports/standardsRoi.js +162 -0
- package/src/handlers/scheduled/analyzeCorrelations.js +178 -0
- package/src/handlers/scheduled/analyzeGitHistory.js +510 -0
- package/src/handlers/scheduled/generateAlerts.js +135 -0
- package/src/handlers/scheduled/refreshActivity.js +21 -0
- package/src/handlers/scheduled/scanCompliance.js +334 -0
- package/src/handlers/sessions/sessionEndPost.js +180 -0
- package/src/handlers/sessions/sessionStandardsPost.js +135 -0
- package/src/handlers/stripe/addonManagePost.js +240 -0
- package/src/handlers/stripe/billingPortalPost.js +93 -0
- package/src/handlers/stripe/enterpriseCheckoutPost.js +272 -0
- package/src/handlers/stripe/seatsUpdatePost.js +185 -0
- package/src/handlers/stripe/subscriptionCancelDelete.js +169 -0
- package/src/handlers/stripe/subscriptionCreatePost.js +221 -0
- package/src/handlers/stripe/subscriptionUpdatePut.js +163 -0
- package/src/handlers/stripe/webhookPost.js +454 -0
- package/src/handlers/users/cognitoPostConfirmation.js +150 -0
- package/src/handlers/users/userEntitlementsGet.js +89 -0
- package/src/handlers/users/userGet.js +114 -0
- package/src/handlers/webhooks/githubWebhook.js +223 -0
- package/src/index.js +969 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity Get Team Handler
|
|
3
|
+
* Retrieves team activity summary (managers only)
|
|
4
|
+
*
|
|
5
|
+
* GET /api/activity/team
|
|
6
|
+
* Auth: Cognito JWT required, Manager or Admin role
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
|
|
10
|
+
|
|
11
|
+
exports.handler = wrapHandler(async ({ requestContext, queryStringParameters }) => {
|
|
12
|
+
const Request_ID = requestContext.requestId;
|
|
13
|
+
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
14
|
+
|
|
15
|
+
if (!email) {
|
|
16
|
+
return createErrorResponse(401, 'Authentication required');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Check if user is a manager or admin
|
|
20
|
+
const managerCheck = await executeQuery(`
|
|
21
|
+
SELECT ue."Company_ID", ue."Manager", ue."Admin", u."Super_Admin"
|
|
22
|
+
FROM "UserEntitlements" ue
|
|
23
|
+
JOIN "Users" u ON ue."Email_Address" = u."Email_Address"
|
|
24
|
+
WHERE ue."Email_Address" = $1
|
|
25
|
+
`, [email]);
|
|
26
|
+
|
|
27
|
+
if (managerCheck.rowCount === 0) {
|
|
28
|
+
return createErrorResponse(403, 'Access denied');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const userRole = managerCheck.rows[0];
|
|
32
|
+
const isAuthorized = userRole.Manager || userRole.Admin || userRole.Super_Admin;
|
|
33
|
+
|
|
34
|
+
if (!isAuthorized) {
|
|
35
|
+
return createErrorResponse(403, 'Manager or Admin access required');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const companyId = queryStringParameters?.company_id || userRole.Company_ID;
|
|
39
|
+
|
|
40
|
+
// Get team summary
|
|
41
|
+
const summaryResult = await executeQuery(`
|
|
42
|
+
SELECT * FROM rapport.v_team_activity_summary
|
|
43
|
+
WHERE company_id = $1
|
|
44
|
+
`, [companyId]);
|
|
45
|
+
|
|
46
|
+
// Get individual developer activity
|
|
47
|
+
const developersResult = await executeQuery(`
|
|
48
|
+
SELECT
|
|
49
|
+
email_address,
|
|
50
|
+
display_name,
|
|
51
|
+
sessions_30d,
|
|
52
|
+
sessions_7d,
|
|
53
|
+
last_session,
|
|
54
|
+
commits_30d,
|
|
55
|
+
commits_7d,
|
|
56
|
+
last_commit,
|
|
57
|
+
days_since_commit,
|
|
58
|
+
prs_merged_30d,
|
|
59
|
+
session_to_commit_conversion_pct
|
|
60
|
+
FROM rapport.mv_developer_activity
|
|
61
|
+
WHERE company_id = $1
|
|
62
|
+
ORDER BY
|
|
63
|
+
CASE
|
|
64
|
+
WHEN days_since_commit IS NULL THEN 999
|
|
65
|
+
ELSE days_since_commit
|
|
66
|
+
END DESC,
|
|
67
|
+
sessions_30d DESC
|
|
68
|
+
`, [companyId]);
|
|
69
|
+
|
|
70
|
+
const summary = summaryResult.rows[0] || {
|
|
71
|
+
total_developers: 0,
|
|
72
|
+
active_developers: 0,
|
|
73
|
+
stale_developers: 0,
|
|
74
|
+
very_stale_developers: 0,
|
|
75
|
+
avg_sessions_30d: 0,
|
|
76
|
+
avg_commits_30d: 0,
|
|
77
|
+
avg_conversion_pct: 0
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const developers = developersResult.rows.map(dev => ({
|
|
81
|
+
email_address: dev.email_address,
|
|
82
|
+
display_name: dev.display_name,
|
|
83
|
+
sessions: {
|
|
84
|
+
last_30_days: parseInt(dev.sessions_30d) || 0,
|
|
85
|
+
last_7_days: parseInt(dev.sessions_7d) || 0,
|
|
86
|
+
last_session: dev.last_session
|
|
87
|
+
},
|
|
88
|
+
commits: {
|
|
89
|
+
last_30_days: parseInt(dev.commits_30d) || 0,
|
|
90
|
+
last_7_days: parseInt(dev.commits_7d) || 0,
|
|
91
|
+
last_commit: dev.last_commit,
|
|
92
|
+
days_since_commit: parseInt(dev.days_since_commit) || null
|
|
93
|
+
},
|
|
94
|
+
prs_merged_30d: parseInt(dev.prs_merged_30d) || 0,
|
|
95
|
+
session_to_commit_conversion_pct: parseFloat(dev.session_to_commit_conversion_pct) || 0,
|
|
96
|
+
status: getDevStatus(dev)
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
return createSuccessResponse(
|
|
100
|
+
{
|
|
101
|
+
Records: [{
|
|
102
|
+
company_id: companyId,
|
|
103
|
+
summary: {
|
|
104
|
+
total_developers: parseInt(summary.total_developers) || 0,
|
|
105
|
+
active_developers: parseInt(summary.active_developers) || 0,
|
|
106
|
+
stale_developers: parseInt(summary.stale_developers) || 0,
|
|
107
|
+
very_stale_developers: parseInt(summary.very_stale_developers) || 0,
|
|
108
|
+
avg_sessions_30d: parseFloat(summary.avg_sessions_30d)?.toFixed(1) || 0,
|
|
109
|
+
avg_commits_30d: parseFloat(summary.avg_commits_30d)?.toFixed(1) || 0,
|
|
110
|
+
avg_conversion_pct: parseFloat(summary.avg_conversion_pct)?.toFixed(1) || 0
|
|
111
|
+
},
|
|
112
|
+
developers
|
|
113
|
+
}]
|
|
114
|
+
},
|
|
115
|
+
'Team activity retrieved',
|
|
116
|
+
{ Total_Records: developers.length, Request_ID, Timestamp: new Date().toISOString() }
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
function getDevStatus(dev) {
|
|
121
|
+
const daysSinceCommit = parseInt(dev.days_since_commit);
|
|
122
|
+
const sessions30d = parseInt(dev.sessions_30d) || 0;
|
|
123
|
+
const commits30d = parseInt(dev.commits_30d) || 0;
|
|
124
|
+
|
|
125
|
+
if (daysSinceCommit > 14) return 'very_stale';
|
|
126
|
+
if (daysSinceCommit > 7) return 'stale';
|
|
127
|
+
if (sessions30d === 0 && commits30d > 0) return 'no_ai_usage';
|
|
128
|
+
if (sessions30d > 0 && commits30d === 0) return 'low_output';
|
|
129
|
+
return 'active';
|
|
130
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alerts Acknowledge Handler
|
|
3
|
+
* Acknowledges or resolves an attention alert
|
|
4
|
+
*
|
|
5
|
+
* PUT /api/alerts/{alert_id}/acknowledge
|
|
6
|
+
* Auth: Cognito JWT required, Manager or Admin role
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
|
|
10
|
+
|
|
11
|
+
exports.handler = wrapHandler(async ({ requestContext, pathParameters, body }) => {
|
|
12
|
+
const Request_ID = requestContext.requestId;
|
|
13
|
+
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
14
|
+
|
|
15
|
+
if (!email) {
|
|
16
|
+
return createErrorResponse(401, 'Authentication required');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const alertId = pathParameters?.alert_id;
|
|
20
|
+
if (!alertId) {
|
|
21
|
+
return createErrorResponse(400, 'Alert ID required');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check user role
|
|
25
|
+
const userCheck = await executeQuery(`
|
|
26
|
+
SELECT ue."Company_ID", ue."Manager", ue."Admin", u."Super_Admin"
|
|
27
|
+
FROM "UserEntitlements" ue
|
|
28
|
+
JOIN "Users" u ON ue."Email_Address" = u."Email_Address"
|
|
29
|
+
WHERE ue."Email_Address" = $1
|
|
30
|
+
`, [email]);
|
|
31
|
+
|
|
32
|
+
if (userCheck.rowCount === 0) {
|
|
33
|
+
return createErrorResponse(403, 'Access denied');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const userRole = userCheck.rows[0];
|
|
37
|
+
const isManager = userRole.Manager || userRole.Admin || userRole.Super_Admin;
|
|
38
|
+
|
|
39
|
+
if (!isManager) {
|
|
40
|
+
return createErrorResponse(403, 'Manager or Admin access required');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const action = body?.action || 'acknowledge'; // acknowledge or resolve
|
|
44
|
+
|
|
45
|
+
// Verify alert exists and belongs to user's company
|
|
46
|
+
const alertCheck = await executeQuery(`
|
|
47
|
+
SELECT alert_id, company_id, status
|
|
48
|
+
FROM rapport.attention_alerts
|
|
49
|
+
WHERE alert_id = $1
|
|
50
|
+
`, [alertId]);
|
|
51
|
+
|
|
52
|
+
if (alertCheck.rowCount === 0) {
|
|
53
|
+
return createErrorResponse(404, 'Alert not found');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const alert = alertCheck.rows[0];
|
|
57
|
+
if (alert.company_id !== userRole.Company_ID && !userRole.Super_Admin) {
|
|
58
|
+
return createErrorResponse(403, 'Access denied to this alert');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Update alert
|
|
62
|
+
let result;
|
|
63
|
+
if (action === 'resolve') {
|
|
64
|
+
result = await executeQuery(`
|
|
65
|
+
UPDATE rapport.attention_alerts
|
|
66
|
+
SET
|
|
67
|
+
status = 'resolved',
|
|
68
|
+
acknowledged_by = $1,
|
|
69
|
+
acknowledged_at = COALESCE(acknowledged_at, NOW()),
|
|
70
|
+
resolved_at = NOW()
|
|
71
|
+
WHERE alert_id = $2
|
|
72
|
+
RETURNING *
|
|
73
|
+
`, [email, alertId]);
|
|
74
|
+
} else {
|
|
75
|
+
result = await executeQuery(`
|
|
76
|
+
UPDATE rapport.attention_alerts
|
|
77
|
+
SET
|
|
78
|
+
status = 'acknowledged',
|
|
79
|
+
acknowledged_by = $1,
|
|
80
|
+
acknowledged_at = NOW()
|
|
81
|
+
WHERE alert_id = $2
|
|
82
|
+
RETURNING *
|
|
83
|
+
`, [email, alertId]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return createSuccessResponse(
|
|
87
|
+
{ Records: result.rows },
|
|
88
|
+
`Alert ${action}d`,
|
|
89
|
+
{ Total_Records: 1, Request_ID, Timestamp: new Date().toISOString() }
|
|
90
|
+
);
|
|
91
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alerts Get Handler
|
|
3
|
+
* Retrieves attention alerts (own alerts or team alerts for managers)
|
|
4
|
+
*
|
|
5
|
+
* GET /api/alerts
|
|
6
|
+
* Auth: Cognito JWT required
|
|
7
|
+
*
|
|
8
|
+
* Query Parameters:
|
|
9
|
+
* - status: 'active' (default), 'acknowledged', 'resolved', or 'all'
|
|
10
|
+
* - type: Filter by alert type (optional)
|
|
11
|
+
* - severity: Filter by severity (optional)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
|
|
15
|
+
|
|
16
|
+
exports.handler = wrapHandler(async ({ requestContext, queryStringParameters }) => {
|
|
17
|
+
const Request_ID = requestContext.requestId;
|
|
18
|
+
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
19
|
+
|
|
20
|
+
if (!email) {
|
|
21
|
+
return createErrorResponse(401, 'Authentication required');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check user role
|
|
25
|
+
const userCheck = await executeQuery(`
|
|
26
|
+
SELECT
|
|
27
|
+
ue."Company_ID",
|
|
28
|
+
ue."Manager",
|
|
29
|
+
ue."Admin",
|
|
30
|
+
u."Super_Admin"
|
|
31
|
+
FROM "UserEntitlements" ue
|
|
32
|
+
JOIN "Users" u ON ue."Email_Address" = u."Email_Address"
|
|
33
|
+
WHERE ue."Email_Address" = $1
|
|
34
|
+
`, [email]);
|
|
35
|
+
|
|
36
|
+
if (userCheck.rowCount === 0) {
|
|
37
|
+
return createErrorResponse(403, 'Access denied');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const userRole = userCheck.rows[0];
|
|
41
|
+
const isManager = userRole.Manager || userRole.Admin || userRole.Super_Admin;
|
|
42
|
+
const companyId = userRole.Company_ID;
|
|
43
|
+
|
|
44
|
+
// Parse query parameters
|
|
45
|
+
const status = queryStringParameters?.status || 'active';
|
|
46
|
+
const alertType = queryStringParameters?.type || null;
|
|
47
|
+
const severity = queryStringParameters?.severity || null;
|
|
48
|
+
|
|
49
|
+
let result;
|
|
50
|
+
|
|
51
|
+
if (isManager) {
|
|
52
|
+
// Get team alerts with optional filters
|
|
53
|
+
let query = `
|
|
54
|
+
SELECT
|
|
55
|
+
aa.alert_id,
|
|
56
|
+
aa.email_address,
|
|
57
|
+
u."User_Display_Name" as user_name,
|
|
58
|
+
aa.alert_type,
|
|
59
|
+
aa.severity,
|
|
60
|
+
aa.details,
|
|
61
|
+
aa.status,
|
|
62
|
+
aa.acknowledged_by,
|
|
63
|
+
aa.acknowledged_at,
|
|
64
|
+
aa.expires_at,
|
|
65
|
+
aa.created_at
|
|
66
|
+
FROM rapport.attention_alerts aa
|
|
67
|
+
JOIN "Users" u ON aa.email_address = u."Email_Address"
|
|
68
|
+
WHERE aa.company_id = $1
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
const params = [companyId];
|
|
72
|
+
let paramIndex = 2;
|
|
73
|
+
|
|
74
|
+
// Status filter
|
|
75
|
+
if (status !== 'all') {
|
|
76
|
+
query += ` AND aa.status = $${paramIndex}`;
|
|
77
|
+
params.push(status);
|
|
78
|
+
paramIndex++;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Alert type filter
|
|
82
|
+
if (alertType) {
|
|
83
|
+
query += ` AND aa.alert_type = $${paramIndex}`;
|
|
84
|
+
params.push(alertType);
|
|
85
|
+
paramIndex++;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Severity filter
|
|
89
|
+
if (severity) {
|
|
90
|
+
query += ` AND aa.severity = $${paramIndex}`;
|
|
91
|
+
params.push(severity);
|
|
92
|
+
paramIndex++;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Order by severity (critical > warning > info) then by date
|
|
96
|
+
query += `
|
|
97
|
+
ORDER BY
|
|
98
|
+
CASE aa.severity
|
|
99
|
+
WHEN 'critical' THEN 1
|
|
100
|
+
WHEN 'warning' THEN 2
|
|
101
|
+
ELSE 3
|
|
102
|
+
END,
|
|
103
|
+
aa.created_at DESC
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
result = await executeQuery(query, params);
|
|
107
|
+
} else {
|
|
108
|
+
// Get own alerts only
|
|
109
|
+
let query = `
|
|
110
|
+
SELECT
|
|
111
|
+
aa.alert_id,
|
|
112
|
+
aa.email_address,
|
|
113
|
+
aa.alert_type,
|
|
114
|
+
aa.severity,
|
|
115
|
+
aa.details,
|
|
116
|
+
aa.status,
|
|
117
|
+
aa.expires_at,
|
|
118
|
+
aa.created_at
|
|
119
|
+
FROM rapport.attention_alerts aa
|
|
120
|
+
WHERE aa.email_address = $1
|
|
121
|
+
`;
|
|
122
|
+
|
|
123
|
+
const params = [email];
|
|
124
|
+
let paramIndex = 2;
|
|
125
|
+
|
|
126
|
+
if (status !== 'all') {
|
|
127
|
+
query += ` AND aa.status = $${paramIndex}`;
|
|
128
|
+
params.push(status);
|
|
129
|
+
paramIndex++;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (alertType) {
|
|
133
|
+
query += ` AND aa.alert_type = $${paramIndex}`;
|
|
134
|
+
params.push(alertType);
|
|
135
|
+
paramIndex++;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (severity) {
|
|
139
|
+
query += ` AND aa.severity = $${paramIndex}`;
|
|
140
|
+
params.push(severity);
|
|
141
|
+
paramIndex++;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
query += ` ORDER BY aa.created_at DESC`;
|
|
145
|
+
|
|
146
|
+
result = await executeQuery(query, params);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const alerts = result.rows.map(alert => ({
|
|
150
|
+
alert_id: alert.alert_id,
|
|
151
|
+
email_address: alert.email_address,
|
|
152
|
+
user_name: alert.user_name || null,
|
|
153
|
+
alert_type: alert.alert_type,
|
|
154
|
+
severity: alert.severity,
|
|
155
|
+
details: alert.details,
|
|
156
|
+
status: alert.status,
|
|
157
|
+
acknowledged_by: alert.acknowledged_by || null,
|
|
158
|
+
acknowledged_at: alert.acknowledged_at || null,
|
|
159
|
+
expires_at: alert.expires_at || null,
|
|
160
|
+
created_at: alert.created_at,
|
|
161
|
+
message: getAlertMessage(alert.alert_type, alert.details),
|
|
162
|
+
recommendation: getAlertRecommendation(alert.alert_type, alert.severity)
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
// Calculate summary statistics
|
|
166
|
+
const summary = {
|
|
167
|
+
total: alerts.length,
|
|
168
|
+
by_severity: {
|
|
169
|
+
critical: alerts.filter(a => a.severity === 'critical').length,
|
|
170
|
+
warning: alerts.filter(a => a.severity === 'warning').length,
|
|
171
|
+
info: alerts.filter(a => a.severity === 'info').length
|
|
172
|
+
},
|
|
173
|
+
by_type: {}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
for (const alert of alerts) {
|
|
177
|
+
summary.by_type[alert.alert_type] = (summary.by_type[alert.alert_type] || 0) + 1;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return createSuccessResponse(
|
|
181
|
+
{ Records: alerts, summary, is_manager_view: isManager },
|
|
182
|
+
'Alerts retrieved',
|
|
183
|
+
{ Total_Records: alerts.length, Request_ID, Timestamp: new Date().toISOString() }
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Generate human-readable message for alert
|
|
189
|
+
*/
|
|
190
|
+
function getAlertMessage(alertType, details) {
|
|
191
|
+
switch (alertType) {
|
|
192
|
+
case 'stale_commits':
|
|
193
|
+
return `No commits in ${details.days_since_commit} days`;
|
|
194
|
+
|
|
195
|
+
case 'low_conversion':
|
|
196
|
+
return `Low session-to-commit conversion: ${details.conversion_pct}%`;
|
|
197
|
+
|
|
198
|
+
case 'no_ai_usage':
|
|
199
|
+
return `Active committer not using AI assistance (${details.commits_30d} commits, 0 sessions)`;
|
|
200
|
+
|
|
201
|
+
case 'high_violation_rate':
|
|
202
|
+
return `High standards violation rate: ${details.violation_rate}% (${details.violations}/${details.standards_shown})`;
|
|
203
|
+
|
|
204
|
+
case 'stalled_patterns':
|
|
205
|
+
return `Stalled patterns: ${details.stale_patterns} patterns unused, ${details.provisional_patterns} stuck in provisional`;
|
|
206
|
+
|
|
207
|
+
case 'declining_activity':
|
|
208
|
+
const decline = details.decline_pct || 0;
|
|
209
|
+
return `Activity declined ${decline}% (was ${details.avg_previous_weeks} sessions/week, now ${details.current_week})`;
|
|
210
|
+
|
|
211
|
+
default:
|
|
212
|
+
return `Alert: ${alertType}`;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Generate actionable recommendation for alert
|
|
218
|
+
*/
|
|
219
|
+
function getAlertRecommendation(alertType, severity) {
|
|
220
|
+
const recommendations = {
|
|
221
|
+
stale_commits: {
|
|
222
|
+
critical: 'Schedule a 1:1 to check in on progress and identify blockers',
|
|
223
|
+
warning: 'Consider reaching out to offer assistance',
|
|
224
|
+
info: 'Monitor for continued inactivity'
|
|
225
|
+
},
|
|
226
|
+
low_conversion: {
|
|
227
|
+
critical: 'Review session recordings to identify workflow issues',
|
|
228
|
+
warning: 'Check if developer needs additional training on AI tools',
|
|
229
|
+
info: 'Provide tips for effective AI-assisted development'
|
|
230
|
+
},
|
|
231
|
+
no_ai_usage: {
|
|
232
|
+
info: 'Share AI adoption resources and success stories from team'
|
|
233
|
+
},
|
|
234
|
+
high_violation_rate: {
|
|
235
|
+
critical: 'Schedule standards review session with developer',
|
|
236
|
+
warning: 'Share relevant standards documentation',
|
|
237
|
+
info: 'Consider adding standards to onboarding materials'
|
|
238
|
+
},
|
|
239
|
+
stalled_patterns: {
|
|
240
|
+
warning: 'Review patterns for promotion or deprecation',
|
|
241
|
+
info: 'Encourage pattern documentation and sharing'
|
|
242
|
+
},
|
|
243
|
+
declining_activity: {
|
|
244
|
+
warning: 'Check in on workload and well-being',
|
|
245
|
+
info: 'Monitor for continued decline'
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
return recommendations[alertType]?.[severity] || recommendations[alertType]?.info || 'Monitor and follow up as needed';
|
|
250
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collaborator Add Handler
|
|
3
|
+
* Adds a collaborator to a project
|
|
4
|
+
*
|
|
5
|
+
* POST /api/projects/{projectId}/collaborators
|
|
6
|
+
* Body: { email, role }
|
|
7
|
+
* Auth: Cognito JWT required
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError, checkCollaboratorBillingLimits, checkEnterpriseSeatLimits } = require('./helpers');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Add collaborator to project
|
|
14
|
+
* Requires owner or admin role on project
|
|
15
|
+
*/
|
|
16
|
+
async function addCollaborator({ pathParameters = {}, body: requestBody = {}, requestContext }) {
|
|
17
|
+
try {
|
|
18
|
+
const Request_ID = requestContext.requestId;
|
|
19
|
+
const inviterEmail = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
20
|
+
const { projectId } = pathParameters;
|
|
21
|
+
const { email, role = 'collaborator' } = requestBody;
|
|
22
|
+
|
|
23
|
+
if (!inviterEmail) {
|
|
24
|
+
return createErrorResponse(401, 'Authentication required');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!projectId) {
|
|
28
|
+
return createErrorResponse(400, 'projectId is required');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!email) {
|
|
32
|
+
return createErrorResponse(400, 'email is required');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Validate role
|
|
36
|
+
const validRoles = ['admin', 'collaborator', 'viewer'];
|
|
37
|
+
if (!validRoles.includes(role)) {
|
|
38
|
+
return createErrorResponse(400, `Invalid role. Must be one of: ${validRoles.join(', ')}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check inviter has permission (owner or admin)
|
|
42
|
+
const accessQuery = `
|
|
43
|
+
SELECT
|
|
44
|
+
pc.role,
|
|
45
|
+
p.company_id,
|
|
46
|
+
c.client_id,
|
|
47
|
+
c.subscription_tier,
|
|
48
|
+
c.billing_type,
|
|
49
|
+
c.billable_users,
|
|
50
|
+
c.seat_count,
|
|
51
|
+
c.enterprise_package
|
|
52
|
+
FROM rapport.projects p
|
|
53
|
+
JOIN rapport.project_collaborators pc
|
|
54
|
+
ON p.project_id = pc.project_id
|
|
55
|
+
AND pc.email_address = $1
|
|
56
|
+
JOIN rapport.clients c ON p.company_id = c.client_id
|
|
57
|
+
WHERE p.project_id = $2
|
|
58
|
+
`;
|
|
59
|
+
const accessCheck = await executeQuery(accessQuery, [inviterEmail, projectId]);
|
|
60
|
+
|
|
61
|
+
if (accessCheck.rowCount === 0) {
|
|
62
|
+
return createErrorResponse(403, 'You do not have access to this project');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const access = accessCheck.rows[0];
|
|
66
|
+
if (access.role !== 'owner' && access.role !== 'admin') {
|
|
67
|
+
return createErrorResponse(403, 'Only project owners and admins can add collaborators');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check billing/subscription limits based on billing type
|
|
71
|
+
const collaboratorCountQuery = `
|
|
72
|
+
SELECT COUNT(*) as count
|
|
73
|
+
FROM rapport.project_collaborators
|
|
74
|
+
WHERE project_id = $1
|
|
75
|
+
`;
|
|
76
|
+
const countResult = await executeQuery(collaboratorCountQuery, [projectId]);
|
|
77
|
+
const currentCount = parseInt(countResult.rows[0].count) || 0;
|
|
78
|
+
|
|
79
|
+
// For enterprise tier, check seat limits
|
|
80
|
+
if (access.subscription_tier === 'enterprise') {
|
|
81
|
+
// Count total users across all projects for this client
|
|
82
|
+
const totalUsersQuery = `
|
|
83
|
+
SELECT COUNT(DISTINCT pc.email_address) as total_users
|
|
84
|
+
FROM rapport.project_collaborators pc
|
|
85
|
+
JOIN rapport.projects p ON pc.project_id = p.project_id
|
|
86
|
+
WHERE p.company_id = $1
|
|
87
|
+
`;
|
|
88
|
+
const totalUsersResult = await executeQuery(totalUsersQuery, [access.client_id]);
|
|
89
|
+
const totalUsers = parseInt(totalUsersResult.rows[0].total_users) || 0;
|
|
90
|
+
|
|
91
|
+
const seatCheck = checkEnterpriseSeatLimits(
|
|
92
|
+
{
|
|
93
|
+
subscription_tier: access.subscription_tier,
|
|
94
|
+
seat_count: access.seat_count
|
|
95
|
+
},
|
|
96
|
+
totalUsers
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (!seatCheck.allowed) {
|
|
100
|
+
return createErrorResponse(403, seatCheck.message, {
|
|
101
|
+
code: 'ENTERPRISE_SEAT_LIMIT',
|
|
102
|
+
seatsUsed: seatCheck.seatsUsed,
|
|
103
|
+
seatCount: seatCheck.seatCount,
|
|
104
|
+
billingAction: seatCheck.billingAction
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const billingCheck = checkCollaboratorBillingLimits(
|
|
110
|
+
{
|
|
111
|
+
subscription_tier: access.subscription_tier,
|
|
112
|
+
billing_type: access.billing_type
|
|
113
|
+
},
|
|
114
|
+
currentCount
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (!billingCheck.allowed) {
|
|
118
|
+
return createErrorResponse(403, billingCheck.message, {
|
|
119
|
+
code: 'SUBSCRIPTION_LIMIT',
|
|
120
|
+
current: currentCount,
|
|
121
|
+
limit: billingCheck.limit
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check if collaborator already exists
|
|
126
|
+
const existsQuery = `
|
|
127
|
+
SELECT email_address FROM rapport.project_collaborators
|
|
128
|
+
WHERE project_id = $1 AND email_address = $2
|
|
129
|
+
`;
|
|
130
|
+
const existsCheck = await executeQuery(existsQuery, [projectId, email]);
|
|
131
|
+
|
|
132
|
+
if (existsCheck.rowCount > 0) {
|
|
133
|
+
return createErrorResponse(409, 'User is already a collaborator on this project');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check if user exists in system (internal) or is external
|
|
137
|
+
const userQuery = `SELECT email_address FROM rapport.users WHERE email_address = $1`;
|
|
138
|
+
const userCheck = await executeQuery(userQuery, [email]);
|
|
139
|
+
const isExternal = userCheck.rowCount === 0;
|
|
140
|
+
|
|
141
|
+
// Add collaborator
|
|
142
|
+
const insertQuery = `
|
|
143
|
+
INSERT INTO rapport.project_collaborators
|
|
144
|
+
(project_id, email_address, role, invited_by, invited_at, is_external, accepted_at)
|
|
145
|
+
VALUES
|
|
146
|
+
($1, $2, $3, $4, NOW(), $5, ${isExternal ? 'NULL' : 'NOW()'})
|
|
147
|
+
RETURNING
|
|
148
|
+
project_id,
|
|
149
|
+
email_address,
|
|
150
|
+
role,
|
|
151
|
+
invited_by,
|
|
152
|
+
invited_at,
|
|
153
|
+
is_external,
|
|
154
|
+
accepted_at
|
|
155
|
+
`;
|
|
156
|
+
|
|
157
|
+
const result = await executeQuery(insertQuery, [
|
|
158
|
+
projectId,
|
|
159
|
+
email,
|
|
160
|
+
role,
|
|
161
|
+
inviterEmail,
|
|
162
|
+
isExternal
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
const collaborator = result.rows[0];
|
|
166
|
+
|
|
167
|
+
// For enterprise invoice billing, increment billable_users count
|
|
168
|
+
if (billingCheck.billingAction === 'increment_billable_users') {
|
|
169
|
+
await executeQuery(`
|
|
170
|
+
UPDATE rapport.clients
|
|
171
|
+
SET billable_users = COALESCE(billable_users, 0) + 1,
|
|
172
|
+
last_updated = NOW()
|
|
173
|
+
WHERE client_id = $1
|
|
174
|
+
`, [access.client_id]);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return createSuccessResponse(
|
|
178
|
+
{
|
|
179
|
+
Records: [{
|
|
180
|
+
...collaborator,
|
|
181
|
+
status: isExternal ? 'pending_invite' : 'active',
|
|
182
|
+
message: isExternal
|
|
183
|
+
? 'Collaborator added. Send invite to complete setup.'
|
|
184
|
+
: 'Collaborator added successfully.'
|
|
185
|
+
}]
|
|
186
|
+
},
|
|
187
|
+
isExternal ? 'Collaborator added (pending invite)' : 'Collaborator added',
|
|
188
|
+
{
|
|
189
|
+
Total_Records: 1,
|
|
190
|
+
Request_ID,
|
|
191
|
+
Timestamp: new Date().toISOString()
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error('Handler Error:', error);
|
|
197
|
+
return handleError(error);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
exports.handler = wrapHandler(addCollaborator);
|