@equilateral_ai/mindmeld 3.3.1 → 3.5.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.
Files changed (72) hide show
  1. package/README.md +1 -10
  2. package/hooks/pre-compact.js +213 -25
  3. package/hooks/session-end.js +112 -3
  4. package/hooks/session-start.js +635 -41
  5. package/hooks/subagent-start.js +150 -0
  6. package/hooks/subagent-stop.js +184 -0
  7. package/package.json +8 -7
  8. package/scripts/init-project.js +74 -33
  9. package/scripts/mcp-bridge.js +220 -0
  10. package/src/core/CorrelationAnalyzer.js +157 -0
  11. package/src/core/LLMPatternDetector.js +198 -0
  12. package/src/core/RelevanceDetector.js +123 -36
  13. package/src/core/StandardsIngestion.js +119 -18
  14. package/src/handlers/activity/activityGetMe.js +1 -1
  15. package/src/handlers/activity/activityGetTeam.js +100 -55
  16. package/src/handlers/admin/adminSetup.js +216 -0
  17. package/src/handlers/alerts/alertsAcknowledge.js +6 -6
  18. package/src/handlers/alerts/alertsGet.js +11 -11
  19. package/src/handlers/analytics/activitySummaryGet.js +34 -35
  20. package/src/handlers/analytics/coachingGet.js +11 -11
  21. package/src/handlers/analytics/convergenceGet.js +236 -0
  22. package/src/handlers/analytics/developerScoreGet.js +41 -111
  23. package/src/handlers/collaborators/collaboratorInvite.js +1 -1
  24. package/src/handlers/company/companyUsersDelete.js +141 -0
  25. package/src/handlers/company/companyUsersGet.js +90 -0
  26. package/src/handlers/company/companyUsersPost.js +267 -0
  27. package/src/handlers/company/companyUsersPut.js +76 -0
  28. package/src/handlers/correlations/correlationsDeveloperGet.js +12 -12
  29. package/src/handlers/correlations/correlationsGet.js +8 -8
  30. package/src/handlers/correlations/correlationsProjectGet.js +5 -5
  31. package/src/handlers/enterprise/controlTowerGet.js +224 -0
  32. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +48 -9
  33. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +1 -3
  34. package/src/handlers/github/githubConnectionStatus.js +1 -1
  35. package/src/handlers/github/githubDiscoverPatterns.js +4 -2
  36. package/src/handlers/github/githubPatternsReview.js +7 -36
  37. package/src/handlers/health/healthGet.js +55 -0
  38. package/src/handlers/helpers/checkSuperAdmin.js +13 -14
  39. package/src/handlers/helpers/mindmeldMcpCore.js +594 -0
  40. package/src/handlers/helpers/subscriptionTiers.js +27 -27
  41. package/src/handlers/mcp/mcpHandler.js +569 -0
  42. package/src/handlers/mcp/mindmeldMcpHandler.js +124 -0
  43. package/src/handlers/mcp/mindmeldMcpStreamHandler.js +243 -0
  44. package/src/handlers/notifications/sendNotification.js +18 -18
  45. package/src/handlers/patterns/patternEvaluatePromotionPost.js +173 -0
  46. package/src/handlers/projects/projectCreate.js +124 -10
  47. package/src/handlers/projects/projectDelete.js +4 -4
  48. package/src/handlers/projects/projectGet.js +8 -8
  49. package/src/handlers/projects/projectUpdate.js +4 -4
  50. package/src/handlers/reports/aiLeverage.js +34 -30
  51. package/src/handlers/reports/engineeringInvestment.js +16 -16
  52. package/src/handlers/reports/riskForecast.js +41 -21
  53. package/src/handlers/reports/standardsRoi.js +101 -9
  54. package/src/handlers/scheduled/maturityUpdateJob.js +166 -0
  55. package/src/handlers/sessions/sessionStandardsPost.js +43 -7
  56. package/src/handlers/standards/discoveriesGet.js +93 -0
  57. package/src/handlers/standards/projectStandardsGet.js +2 -2
  58. package/src/handlers/standards/projectStandardsPut.js +2 -2
  59. package/src/handlers/standards/standardsRelevantPost.js +107 -12
  60. package/src/handlers/standards/standardsTransition.js +112 -15
  61. package/src/handlers/stripe/billingPortalPost.js +1 -1
  62. package/src/handlers/stripe/enterpriseCheckoutPost.js +2 -2
  63. package/src/handlers/stripe/subscriptionCreatePost.js +2 -2
  64. package/src/handlers/stripe/webhookPost.js +42 -14
  65. package/src/handlers/user/apiTokenCreate.js +71 -0
  66. package/src/handlers/user/apiTokenList.js +64 -0
  67. package/src/handlers/user/userSplashGet.js +90 -73
  68. package/src/handlers/users/cognitoPostConfirmation.js +37 -1
  69. package/src/handlers/users/cognitoPreSignUp.js +114 -0
  70. package/src/handlers/users/userGet.js +15 -11
  71. package/src/handlers/webhooks/githubWebhook.js +117 -125
  72. package/src/index.js +8 -5
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Control Tower Handler
3
+ * Unified org health dashboard for enterprise tier
4
+ *
5
+ * GET /api/enterprise/control-tower
6
+ * Auth: Cognito JWT required, Enterprise tier
7
+ *
8
+ * Aggregates cross-project health metrics:
9
+ * - Organization health score
10
+ * - Standards compliance across projects
11
+ * - Risk indicators (stale devs, violations, coverage gaps)
12
+ * - Governance activity (audit events, knowledge items)
13
+ * - Project-level breakdown
14
+ */
15
+
16
+ const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
17
+
18
+ async function getControlTower({ requestContext, queryStringParameters }) {
19
+ const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
20
+
21
+ if (!email) {
22
+ return createErrorResponse(401, 'Authentication required');
23
+ }
24
+
25
+ // Verify enterprise admin/manager access
26
+ const accessCheck = await executeQuery(`
27
+ SELECT ue.company_id, ue.admin, ue.manager, c.subscription_tier, co.company_name
28
+ FROM rapport.user_entitlements ue
29
+ JOIN rapport.clients c ON ue.client_id = c.client_id
30
+ JOIN rapport.companies co ON ue.company_id = co.company_id
31
+ WHERE ue.email_address = $1
32
+ AND (ue.admin = true OR ue.manager = true)
33
+ LIMIT 1
34
+ `, [email]);
35
+
36
+ if (accessCheck.rows.length === 0) {
37
+ return createErrorResponse(403, 'Admin or Manager access required for Control Tower');
38
+ }
39
+
40
+ const { company_id: companyId, company_name: companyName } = accessCheck.rows[0];
41
+ const periodDays = 30;
42
+ const periodStart = new Date();
43
+ periodStart.setDate(periodStart.getDate() - periodDays);
44
+
45
+ // 1. Team health metrics
46
+ let teamHealth = { total: 0, active: 0, stale: 0, very_stale: 0, no_sessions: 0 };
47
+ try {
48
+ const teamResult = await executeQuery(`
49
+ SELECT
50
+ COUNT(DISTINCT ue.email_address) as total,
51
+ COUNT(DISTINCT CASE WHEN mda.sessions_last_30d > 0 AND mda.commits_last_30d > 0 THEN ue.email_address END) as active,
52
+ COUNT(DISTINCT CASE WHEN mda.days_since_last_commit BETWEEN 8 AND 14 THEN ue.email_address END) as stale,
53
+ COUNT(DISTINCT CASE WHEN mda.days_since_last_commit > 14 THEN ue.email_address END) as very_stale,
54
+ COUNT(DISTINCT CASE WHEN mda.sessions_last_30d = 0 OR mda.sessions_last_30d IS NULL THEN ue.email_address END) as no_sessions
55
+ FROM rapport.user_entitlements ue
56
+ LEFT JOIN rapport.mv_developer_activity mda ON ue.email_address = mda.email_address
57
+ WHERE ue.company_id = $1
58
+ `, [companyId]);
59
+ if (teamResult.rows[0]) {
60
+ teamHealth = {
61
+ total: parseInt(teamResult.rows[0].total) || 0,
62
+ active: parseInt(teamResult.rows[0].active) || 0,
63
+ stale: parseInt(teamResult.rows[0].stale) || 0,
64
+ very_stale: parseInt(teamResult.rows[0].very_stale) || 0,
65
+ no_sessions: parseInt(teamResult.rows[0].no_sessions) || 0
66
+ };
67
+ }
68
+ } catch (e) {
69
+ console.log('[ControlTower] Team health query failed:', e.message);
70
+ }
71
+
72
+ // 2. Project coverage
73
+ let projects = [];
74
+ try {
75
+ const projectResult = await executeQuery(`
76
+ SELECT
77
+ p.project_id,
78
+ p.project_name,
79
+ p.repo_url,
80
+ COUNT(DISTINCT s.session_id) as sessions_30d,
81
+ COUNT(DISTINCT ss.standard_id) as standards_used,
82
+ MAX(s.started_at) as last_session
83
+ FROM rapport.projects p
84
+ LEFT JOIN rapport.sessions s ON p.project_id = s.project_id AND s.started_at >= $2
85
+ LEFT JOIN rapport.session_standards ss ON s.session_id = ss.session_id AND ss.created_at >= $2
86
+ WHERE p.company_id = $1
87
+ GROUP BY p.project_id, p.project_name, p.repo_url
88
+ ORDER BY sessions_30d DESC
89
+ `, [companyId, periodStart]);
90
+ projects = projectResult.rows.map(row => ({
91
+ project_id: row.project_id,
92
+ project_name: row.project_name,
93
+ repo_connected: !!row.repo_url,
94
+ sessions_30d: parseInt(row.sessions_30d) || 0,
95
+ standards_used: parseInt(row.standards_used) || 0,
96
+ last_session: row.last_session,
97
+ status: parseInt(row.sessions_30d) > 0 ? 'active' : 'inactive'
98
+ }));
99
+ } catch (e) {
100
+ console.log('[ControlTower] Projects query failed:', e.message);
101
+ }
102
+
103
+ // 3. Standards coverage (how many unique standards used across org)
104
+ let standardsCoverage = { total_injections: 0, unique_standards: 0, sessions_with_standards: 0 };
105
+ try {
106
+ const stdResult = await executeQuery(`
107
+ SELECT
108
+ COUNT(*) as total_injections,
109
+ COUNT(DISTINCT ss.standard_id) as unique_standards,
110
+ COUNT(DISTINCT ss.session_id) as sessions_with_standards
111
+ FROM rapport.session_standards ss
112
+ JOIN rapport.sessions s ON ss.session_id = s.session_id
113
+ JOIN rapport.projects p ON s.project_id = p.project_id
114
+ WHERE p.company_id = $1
115
+ AND ss.created_at >= $2
116
+ `, [companyId, periodStart]);
117
+ if (stdResult.rows[0]) {
118
+ standardsCoverage = {
119
+ total_injections: parseInt(stdResult.rows[0].total_injections) || 0,
120
+ unique_standards: parseInt(stdResult.rows[0].unique_standards) || 0,
121
+ sessions_with_standards: parseInt(stdResult.rows[0].sessions_with_standards) || 0
122
+ };
123
+ }
124
+ } catch (e) {
125
+ console.log('[ControlTower] Standards coverage query failed:', e.message);
126
+ }
127
+
128
+ // 4. Governance activity (audit events, knowledge items)
129
+ let governance = { audit_events_30d: 0, knowledge_items: 0, published_knowledge: 0 };
130
+ try {
131
+ const auditResult = await executeQuery(`
132
+ SELECT COUNT(*) as count
133
+ FROM rapport.unified_audit_log
134
+ WHERE client_id = (
135
+ SELECT client_id FROM rapport.user_entitlements WHERE company_id = $1 LIMIT 1
136
+ )
137
+ AND created_at >= $2
138
+ `, [companyId, periodStart]);
139
+ governance.audit_events_30d = parseInt(auditResult.rows[0]?.count) || 0;
140
+ } catch (e) {
141
+ console.log('[ControlTower] Audit events query failed:', e.message);
142
+ }
143
+
144
+ try {
145
+ const knowledgeResult = await executeQuery(`
146
+ SELECT
147
+ COUNT(*) as total,
148
+ COUNT(CASE WHEN status = 'published' THEN 1 END) as published
149
+ FROM rapport.curated_knowledge
150
+ WHERE client_id = (
151
+ SELECT client_id FROM rapport.user_entitlements WHERE company_id = $1 LIMIT 1
152
+ )
153
+ `, [companyId]);
154
+ governance.knowledge_items = parseInt(knowledgeResult.rows[0]?.total) || 0;
155
+ governance.published_knowledge = parseInt(knowledgeResult.rows[0]?.published) || 0;
156
+ } catch (e) {
157
+ console.log('[ControlTower] Knowledge query failed:', e.message);
158
+ }
159
+
160
+ // 5. Risk indicators
161
+ const risks = [];
162
+ if (teamHealth.very_stale > 0) {
163
+ risks.push({
164
+ severity: 'high',
165
+ category: 'team',
166
+ message: `${teamHealth.very_stale} developer${teamHealth.very_stale > 1 ? 's' : ''} inactive for 14+ days`
167
+ });
168
+ }
169
+ if (teamHealth.no_sessions > teamHealth.total * 0.3 && teamHealth.total > 1) {
170
+ risks.push({
171
+ severity: 'medium',
172
+ category: 'adoption',
173
+ message: `${teamHealth.no_sessions} of ${teamHealth.total} developers have no AI sessions in 30 days`
174
+ });
175
+ }
176
+ const projectsWithoutStandards = projects.filter(p => p.sessions_30d > 0 && p.standards_used === 0);
177
+ if (projectsWithoutStandards.length > 0) {
178
+ risks.push({
179
+ severity: 'medium',
180
+ category: 'coverage',
181
+ message: `${projectsWithoutStandards.length} active project${projectsWithoutStandards.length > 1 ? 's' : ''} with no standards injection`
182
+ });
183
+ }
184
+ const inactiveProjects = projects.filter(p => p.status === 'inactive');
185
+ if (inactiveProjects.length > projects.length * 0.5 && projects.length > 1) {
186
+ risks.push({
187
+ severity: 'low',
188
+ category: 'projects',
189
+ message: `${inactiveProjects.length} of ${projects.length} projects had no sessions in 30 days`
190
+ });
191
+ }
192
+
193
+ // 6. Calculate org health score (0-100)
194
+ let healthScore = 50; // baseline
195
+ if (teamHealth.total > 0) {
196
+ const activeRate = teamHealth.active / teamHealth.total;
197
+ healthScore = Math.round(activeRate * 40); // up to 40 points for team activity
198
+ }
199
+ if (projects.length > 0) {
200
+ const coveredProjects = projects.filter(p => p.sessions_30d > 0).length;
201
+ healthScore += Math.round((coveredProjects / projects.length) * 30); // up to 30 for project coverage
202
+ }
203
+ if (standardsCoverage.unique_standards > 0) {
204
+ healthScore += Math.min(standardsCoverage.unique_standards * 2, 20); // up to 20 for standards breadth
205
+ }
206
+ if (governance.published_knowledge > 0) {
207
+ healthScore += Math.min(governance.published_knowledge, 10); // up to 10 for knowledge curation
208
+ }
209
+ healthScore = Math.min(healthScore, 100);
210
+
211
+ return createSuccessResponse({
212
+ company_name: companyName,
213
+ health_score: healthScore,
214
+ team: teamHealth,
215
+ projects,
216
+ standards_coverage: standardsCoverage,
217
+ governance,
218
+ risks,
219
+ period_days: periodDays,
220
+ period_start: periodStart.toISOString()
221
+ }, 'Control Tower data retrieved');
222
+ }
223
+
224
+ exports.handler = wrapHandler(getControlTower);
@@ -31,8 +31,8 @@ async function enterpriseOnboardingSetup({ requestContext, body }) {
31
31
  return createErrorResponse(403, 'User not found or no company assigned');
32
32
  }
33
33
 
34
- const { client_id, company_id } = userResult.rows[0];
35
- if (!company_id) {
34
+ const { client_id, company_id: currentCompanyId } = userResult.rows[0];
35
+ if (!currentCompanyId) {
36
36
  return createErrorResponse(400, 'No company associated with this account');
37
37
  }
38
38
 
@@ -55,13 +55,52 @@ async function enterpriseOnboardingSetup({ requestContext, body }) {
55
55
  const industry = body?.industry || null;
56
56
  const companySize = body?.company_size || null;
57
57
 
58
- // Update company profile
59
- await executeQuery(
60
- `UPDATE rapport.companies
61
- SET company_name = $1, domain = $2, industry = $3, company_size = $4, last_updated = NOW()
62
- WHERE company_id = $5`,
63
- [companyName, domain, industry, companySize, company_id]
64
- );
58
+ // Generate a proper enterprise company_id from company name
59
+ const enterpriseCompanyId = `${companyName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}_MAIN`;
60
+ let company_id = currentCompanyId;
61
+
62
+ if (enterpriseCompanyId !== currentCompanyId) {
63
+ // Create the enterprise company (or update if it already exists)
64
+ await executeQuery(
65
+ `INSERT INTO rapport.companies (company_id, client_id, company_name, company_status, domain, industry, company_size)
66
+ VALUES ($1, $2, $3, 'Active', $4, $5, $6)
67
+ ON CONFLICT (company_id) DO UPDATE SET
68
+ company_name = $3, domain = $4, industry = $5, company_size = $6, last_updated = NOW()`,
69
+ [enterpriseCompanyId, client_id, companyName, domain, industry, companySize]
70
+ );
71
+
72
+ // Migrate user entitlement from personal to enterprise company
73
+ await executeQuery(
74
+ `UPDATE rapport.user_entitlements
75
+ SET company_id = $1
76
+ WHERE email_address = $2 AND company_id = $3`,
77
+ [enterpriseCompanyId, email, currentCompanyId]
78
+ );
79
+
80
+ // Clean up orphaned personal company if nothing else references it
81
+ await executeQuery(
82
+ `DELETE FROM rapport.companies
83
+ WHERE company_id = $1
84
+ AND NOT EXISTS (
85
+ SELECT 1 FROM rapport.user_entitlements WHERE company_id = $1
86
+ )
87
+ AND NOT EXISTS (
88
+ SELECT 1 FROM rapport.projects WHERE company_id = $1
89
+ )`,
90
+ [currentCompanyId]
91
+ );
92
+
93
+ company_id = enterpriseCompanyId;
94
+ console.log(`Migrated company: ${currentCompanyId} -> ${enterpriseCompanyId}`);
95
+ } else {
96
+ // Same company_id, just update profile
97
+ await executeQuery(
98
+ `UPDATE rapport.companies
99
+ SET company_name = $1, domain = $2, industry = $3, company_size = $4, last_updated = NOW()
100
+ WHERE company_id = $5`,
101
+ [companyName, domain, industry, companySize, company_id]
102
+ );
103
+ }
65
104
 
66
105
  // Upsert onboarding status
67
106
  await executeQuery(
@@ -59,9 +59,7 @@ async function getEnterpriseOnboardingStatus({ requestContext }) {
59
59
 
60
60
  // Get onboarding status
61
61
  const statusResult = await executeQuery(
62
- `SELECT step_completed, org_configured, invites_sent,
63
- standards_configured, repo_connected, completed,
64
- started_at, completed_at, completed_by
62
+ `SELECT *
65
63
  FROM rapport.enterprise_onboarding_status
66
64
  WHERE company_id = $1`,
67
65
  [company_id]
@@ -30,7 +30,7 @@ async function githubConnectionStatus({ requestContext }) {
30
30
  SELECT COUNT(*) as count FROM rapport.projects p
31
31
  JOIN rapport.project_collaborators pc ON p.project_id = pc.project_id
32
32
  WHERE pc.email_address = $1
33
- AND p.github_owner IS NOT NULL
33
+ AND p.repo_url IS NOT NULL
34
34
  AND p.archived = FALSE
35
35
  `, [email]);
36
36
 
@@ -585,18 +585,20 @@ async function githubDiscoverPatterns({ body, requestContext }) {
585
585
  WHERE project_id = $3
586
586
  `, [owner, repo, project_id]);
587
587
 
588
- // Save discoveries to DB
588
+ // Save discoveries to DB and capture generated IDs
589
589
  for (const discovery of validDiscoveries) {
590
- await executeQuery(`
590
+ const insertResult = await executeQuery(`
591
591
  INSERT INTO rapport.onboarding_discoveries (
592
592
  project_id, email_address, discovery_type, pattern_name,
593
593
  pattern_description, confidence, evidence
594
594
  ) VALUES ($1, $2, $3, $4, $5, $6, $7)
595
+ RETURNING discovery_id
595
596
  `, [
596
597
  project_id, email, discovery.discovery_type, discovery.pattern_name,
597
598
  discovery.pattern_description, discovery.confidence,
598
599
  JSON.stringify(discovery.evidence)
599
600
  ]);
601
+ discovery.discovery_id = insertResult.rows[0]?.discovery_id;
600
602
  }
601
603
 
602
604
  return createSuccessResponse({
@@ -36,46 +36,17 @@ async function githubPatternsReview({ body, requestContext }) {
36
36
 
37
37
  for (const { discovery_id, approved } of approvals) {
38
38
  if (!discovery_id) continue;
39
+ // Skip frontend-generated placeholder IDs (disc_0, disc_1, etc.)
40
+ // These occur when the discover endpoint didn't return DB-generated UUIDs
41
+ if (discovery_id.startsWith('disc_')) continue;
39
42
 
40
43
  if (approved) {
41
- // Get discovery details
42
- const discoveryResult = await executeQuery(`
43
- SELECT discovery_type, pattern_name, pattern_description, evidence
44
- FROM rapport.onboarding_discoveries
45
- WHERE discovery_id = $1 AND project_id = $2
46
- `, [discovery_id, project_id]);
47
-
48
- if (discoveryResult.rowCount === 0) continue;
49
-
50
- const discovery = discoveryResult.rows[0];
51
-
52
- // Create pattern entry
53
- const patternId = `pat_${project_id}_${Date.now()}_${patterns_created}`;
54
- await executeQuery(`
55
- INSERT INTO rapport.patterns (
56
- pattern_id, project_id, intent, constraints, outcome_criteria,
57
- maturity, discovered_by, pattern_data
58
- ) VALUES ($1, $2, $3, $4, $5, 'provisional', $6, $7)
59
- `, [
60
- patternId,
61
- project_id,
62
- discovery.pattern_name,
63
- JSON.stringify([discovery.pattern_description || '']),
64
- JSON.stringify([`Discovered via GitHub onboarding: ${discovery.discovery_type}`]),
65
- email,
66
- JSON.stringify({
67
- source: 'github_onboarding',
68
- discovery_type: discovery.discovery_type,
69
- evidence: discovery.evidence
70
- })
71
- ]);
72
-
73
- // Update discovery status
44
+ // Update discovery status to approved
74
45
  await executeQuery(`
75
46
  UPDATE rapport.onboarding_discoveries
76
- SET status = 'approved', reviewed_at = NOW(), pattern_id = $1
77
- WHERE discovery_id = $2
78
- `, [patternId, discovery_id]);
47
+ SET status = 'approved', reviewed_at = NOW()
48
+ WHERE discovery_id = $1 AND project_id = $2
49
+ `, [discovery_id, project_id]);
79
50
 
80
51
  patterns_created++;
81
52
  } else {
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Health Check Handler
3
+ * Returns system health status for monitoring
4
+ *
5
+ * GET /api/health
6
+ * Auth: None (public endpoint for external monitors)
7
+ */
8
+
9
+ const { executeQuery } = require('./helpers');
10
+
11
+ exports.handler = async (event) => {
12
+ const headers = {
13
+ 'Content-Type': 'application/json',
14
+ 'Access-Control-Allow-Origin': '*',
15
+ 'Access-Control-Allow-Methods': 'GET,OPTIONS',
16
+ 'Access-Control-Allow-Headers': 'Content-Type'
17
+ };
18
+
19
+ const result = {
20
+ status: 'ok',
21
+ timestamp: new Date().toISOString(),
22
+ version: process.env.STACK_VERSION || '1.0.0',
23
+ checks: {}
24
+ };
25
+
26
+ // Database connectivity
27
+ try {
28
+ const start = Date.now();
29
+ await executeQuery('SELECT 1 AS ok');
30
+ result.checks.database = {
31
+ status: 'ok',
32
+ latency_ms: Date.now() - start
33
+ };
34
+ } catch (err) {
35
+ result.status = 'degraded';
36
+ result.checks.database = {
37
+ status: 'error',
38
+ message: 'Database connection failed'
39
+ };
40
+ console.error('[Health] Database check failed:', err.message);
41
+ }
42
+
43
+ // Stripe configuration
44
+ result.checks.stripe = {
45
+ status: process.env.STRIPE_SECRET_KEY ? 'configured' : 'not_configured'
46
+ };
47
+
48
+ const statusCode = result.status === 'ok' ? 200 : 503;
49
+
50
+ return {
51
+ statusCode,
52
+ headers,
53
+ body: JSON.stringify(result)
54
+ };
55
+ };
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * Super Admin Check Helper
3
- * Follows Tim-Combo pattern: simple boolean flag in Users table
4
3
  */
5
4
 
6
5
  const { executeQuery } = require('./dbOperations');
@@ -12,12 +11,12 @@ const { executeQuery } = require('./dbOperations');
12
11
  */
13
12
  async function isSuperAdmin(email) {
14
13
  const query = `
15
- SELECT "Super_Admin"
16
- FROM "Users"
17
- WHERE "Email_Address" = $1
14
+ SELECT super_admin
15
+ FROM rapport.users
16
+ WHERE email_address = $1
18
17
  `;
19
18
  const result = await executeQuery(query, [email]);
20
- return result.rows.length > 0 && result.rows[0].Super_Admin === true;
19
+ return result.rows.length > 0 && result.rows[0].super_admin === true;
21
20
  }
22
21
 
23
22
  /**
@@ -42,15 +41,15 @@ async function requireSuperAdmin(email) {
42
41
  async function getUserWithSuperAdminStatus(email) {
43
42
  const query = `
44
43
  SELECT
45
- "Email_Address",
46
- "Client_ID",
47
- "User_Display_Name",
48
- "First_Name",
49
- "Last_Name",
50
- "Super_Admin",
51
- "User_Status"
52
- FROM "Users"
53
- WHERE "Email_Address" = $1
44
+ email_address,
45
+ client_id,
46
+ CONCAT(first_name, ' ', last_name) as user_display_name,
47
+ first_name,
48
+ last_name,
49
+ super_admin,
50
+ user_status
51
+ FROM rapport.users
52
+ WHERE email_address = $1
54
53
  `;
55
54
  const result = await executeQuery(query, [email]);
56
55
  return result.rows.length > 0 ? result.rows[0] : null;