thatcher 1.0.7 → 1.0.34

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 (174) hide show
  1. package/README.md +4 -16
  2. package/package.json +12 -18
  3. package/src/adapters/google-auth.js +4 -43
  4. package/src/adapters/google-drive.js +12 -67
  5. package/src/app/api/audit/dashboard/route.js +14 -11
  6. package/src/app/api/audit/logs/route.js +2 -2
  7. package/src/app/api/audit/permissions/[id]/route.js +6 -3
  8. package/src/app/api/audit/permissions/route.js +10 -7
  9. package/src/app/api/audit/permissions/stats/route.js +8 -5
  10. package/src/app/api/audit/route.js +4 -4
  11. package/src/app/api/audit/stats/route.js +1 -1
  12. package/src/app/api/auth/google/callback/route.js +5 -2
  13. package/src/app/api/auth/google/route.js +4 -1
  14. package/src/app/api/auth/login/route.js +5 -2
  15. package/src/app/api/auth/mwr-bridge/route.js +11 -21
  16. package/src/app/api/auth/password-reset/route.js +15 -14
  17. package/src/app/api/cron/trigger/route.js +4 -1
  18. package/src/app/api/debug/[[...path]]/route.js +6 -5
  19. package/src/app/api/debug/sqlite/route.js +7 -12
  20. package/src/app/api/debug/sync/route.js +5 -5
  21. package/src/app/api/domains/route.js +4 -1
  22. package/src/app/api/email/allocate/batch/route.js +19 -29
  23. package/src/app/api/email/allocate/route.js +16 -17
  24. package/src/app/api/email/receive/route.js +31 -42
  25. package/src/app/api/email/send/route.js +28 -16
  26. package/src/app/api/email/unallocated/route.js +10 -13
  27. package/src/app/api/files/[id]/route.js +4 -1
  28. package/src/app/api/formula/[[...path]]/route.js +0 -4
  29. package/src/app/api/health/route.js +10 -9
  30. package/src/app/api/metrics/route.js +1 -1
  31. package/src/cli.js +4 -4
  32. package/src/config/config-loader.js +1 -21
  33. package/src/config/constants.js +0 -35
  34. package/src/config/env.js +0 -30
  35. package/src/config/spec-helpers.js +6 -95
  36. package/src/engine.js +23 -0
  37. package/src/engine.server.js +8 -78
  38. package/src/index.js +42 -67
  39. package/src/lib/alert-manager.js +7 -3
  40. package/src/lib/api-helpers.js +4 -3
  41. package/src/lib/api.js +10 -10
  42. package/src/lib/audit-logger-enhanced.js +77 -83
  43. package/src/lib/auth-middleware.js +1 -32
  44. package/src/lib/auth-route-helpers.js +5 -0
  45. package/src/lib/busybase-adapter.js +0 -5
  46. package/src/lib/busybase-audit-reads.js +142 -0
  47. package/src/lib/busybase-audit.js +41 -0
  48. package/src/lib/busybase-lucia-adapter.js +72 -0
  49. package/src/lib/busybase-store.js +338 -0
  50. package/src/lib/config-field-helpers.js +0 -17
  51. package/src/lib/config-generator-engine.js +19 -36
  52. package/src/lib/config-helpers.js +0 -21
  53. package/src/lib/crud-action-helpers.js +2 -2
  54. package/src/lib/crud-factory.js +1 -16
  55. package/src/lib/crud-handlers.js +17 -52
  56. package/src/lib/csrf-protection.js +5 -2
  57. package/src/lib/date-utils.js +3 -127
  58. package/src/lib/debug-registry.js +0 -5
  59. package/src/lib/domain-loader.js +4 -1
  60. package/src/lib/email-sender.js +25 -23
  61. package/src/lib/error-handler.js +5 -41
  62. package/src/lib/error-recovery.js +10 -7
  63. package/src/lib/error-resilience.js +7 -5
  64. package/src/lib/events-engine.js +9 -6
  65. package/src/lib/export-sink.js +0 -5
  66. package/src/lib/field-iterator.js +7 -76
  67. package/src/lib/field-registry.js +0 -22
  68. package/src/lib/generic-crud-handler.js +0 -16
  69. package/src/lib/hook-engine.js +5 -53
  70. package/src/lib/hot-reload/checkpoint.js +5 -2
  71. package/src/lib/hot-reload/promise-container.js +7 -3
  72. package/src/lib/hot-reload/route-wrapper.js +4 -1
  73. package/src/lib/hot-reload/supervisor.js +6 -3
  74. package/src/lib/hyperformula-service.js +0 -5
  75. package/src/lib/id-helpers.js +10 -0
  76. package/src/lib/index.js +4 -7
  77. package/src/lib/log-aggregator.js +0 -1
  78. package/src/lib/logger.js +0 -15
  79. package/src/lib/metrics-collector.js +2 -46
  80. package/src/lib/next-compat.js +15 -1
  81. package/src/lib/observability-bootstrap.js +0 -5
  82. package/src/lib/perf-monitor.js +4 -1
  83. package/src/lib/perf-profiler.js +0 -5
  84. package/src/lib/query-string-adapter.js +5 -47
  85. package/src/lib/realtime-server.js +4 -26
  86. package/src/lib/request-tracing.js +0 -5
  87. package/src/lib/resource-monitor.js +4 -1
  88. package/src/lib/response-formatter.js +0 -38
  89. package/src/lib/route-helpers.js +3 -1
  90. package/src/lib/safe-json.js +5 -1
  91. package/src/lib/server-bootstrap.js +10 -9
  92. package/src/lib/stage-pipeline.js +2 -2
  93. package/src/lib/state-transport-client.js +4 -1
  94. package/src/lib/static-server.js +2 -2
  95. package/src/lib/status-helpers.js +2 -51
  96. package/src/lib/tracing.js +0 -5
  97. package/src/lib/utils.js +6 -67
  98. package/src/lib/validate.js +5 -88
  99. package/src/lib/validators.js +0 -38
  100. package/src/lib/with-audit-logging.js +1 -1
  101. package/src/lib/workflow-engine.js +10 -77
  102. package/src/lib/xstate-workflow-engine.js +3 -9
  103. package/src/plugins/index.js +5 -3
  104. package/src/server/server.js +26 -27
  105. package/src/services/collaborator-role.service.js +21 -77
  106. package/src/services/email-sender.js +7 -34
  107. package/src/services/notification-engine.js +12 -33
  108. package/src/services/permission.service.js +10 -73
  109. package/src/ui/advanced-search-renderer.js +3 -2
  110. package/src/ui/advanced-widgets.js +1 -1
  111. package/src/ui/auth-pages.js +10 -9
  112. package/src/ui/auth-styles.js +1 -1
  113. package/src/ui/client.js +1 -1
  114. package/src/ui/component-engine.js +1 -1
  115. package/src/ui/dashboard-renderer.js +5 -5
  116. package/src/ui/dialog-engine.js +6 -9
  117. package/src/ui/dialog-factory.js +2 -2
  118. package/src/ui/engagement-cards.js +7 -7
  119. package/src/ui/engagement-dialogs.js +2 -3
  120. package/src/ui/engagement-grid-renderer.js +20 -19
  121. package/src/ui/entity-renderer.js +56 -42
  122. package/src/ui/file-dialogs.js +25 -9
  123. package/src/ui/flexup-report-renderer.js +21 -18
  124. package/src/ui/format-helpers.js +27 -28
  125. package/src/ui/highlight-threading-renderer.js +9 -9
  126. package/src/ui/job-management-renderer.js +18 -16
  127. package/src/ui/layout.js +51 -25
  128. package/src/ui/ml-console-renderer.js +15 -16
  129. package/src/ui/monitoring-dashboard-client.js +9 -8
  130. package/src/ui/notifications-renderer.js +11 -10
  131. package/src/ui/page-handler-admin.js +32 -23
  132. package/src/ui/page-handler-helpers.js +27 -23
  133. package/src/ui/page-handler-reviews.js +71 -35
  134. package/src/ui/page-handler-rfi.js +28 -14
  135. package/src/ui/page-handler.js +65 -48
  136. package/src/ui/picker-dialogs.js +26 -19
  137. package/src/ui/render-helpers.js +84 -46
  138. package/src/ui/review-comparison-renderer.js +8 -7
  139. package/src/ui/review-detail-panels.js +8 -9
  140. package/src/ui/review-detail-renderer.js +3 -3
  141. package/src/ui/review-detail-script.js +13 -12
  142. package/src/ui/review-mwr-renderer.js +6 -5
  143. package/src/ui/review-renderer.js +7 -6
  144. package/src/ui/review-widgets.js +3 -3
  145. package/src/ui/rfi-detail-renderer.js +37 -20
  146. package/src/ui/rfi-report-renderer.js +14 -9
  147. package/src/ui/settings-renderer-advanced.js +21 -18
  148. package/src/ui/settings-renderer-advanced2.js +20 -19
  149. package/src/ui/settings-renderer-teams.js +4 -4
  150. package/src/ui/settings-renderer.js +4 -4
  151. package/src/ui/spacing-system.js +5 -2
  152. package/src/ui/standalone-login.js +2 -2
  153. package/src/ui/styles.css +2 -1
  154. package/src/ui/styles2.css +75 -5
  155. package/src/ui/test-page.js +4 -4
  156. package/src/ui/widgets.js +20 -15
  157. package/src/lib/audit-logger.js +0 -193
  158. package/src/lib/database-core.js +0 -255
  159. package/src/lib/database-migrations.js +0 -350
  160. package/src/lib/db-backup.js +0 -97
  161. package/src/lib/error-boundary.js +0 -134
  162. package/src/lib/errors.js +0 -69
  163. package/src/lib/health-monitor.js +0 -134
  164. package/src/lib/index-optimizer.js +0 -96
  165. package/src/lib/query-engine-write.js +0 -221
  166. package/src/lib/query-engine.js +0 -446
  167. package/src/ui/collaboration-dialogs.js +0 -31
  168. package/src/ui/global-tags.js +0 -144
  169. package/src/ui/idle-logout.js +0 -156
  170. package/src/ui/letter-dialogs.js +0 -37
  171. package/src/ui/password-reset-page.js +0 -146
  172. package/src/ui/perf-renderer.js +0 -71
  173. package/src/ui/rfi-renderer.js +0 -194
  174. package/src/ui/validation-ui.js +0 -147
@@ -1,4 +1,7 @@
1
1
  import { NextResponse } from '@/lib/next-polyfills';
2
+ import { createLogger } from '@/lib/logger.js';
3
+
4
+ const log = createLogger('[OAuth]');
2
5
  import { google } from '@/engine.server';
3
6
  import { Google } from 'arctic';
4
7
  import { generateState, generateCodeVerifier } from 'arctic';
@@ -52,7 +55,7 @@ export async function HEAD(request) {
52
55
  const { valid } = validateOAuthProvider(google);
53
56
  return new Response(null, { status: valid ? 200 : 503 });
54
57
  } catch (error) {
55
- console.error('[OAuth HEAD] Error:', error);
58
+ log.error('HEAD error:', { message: error.message });
56
59
  return new Response(null, { status: 500 });
57
60
  }
58
61
  }
@@ -1,4 +1,7 @@
1
+ import { createLogger } from '@/lib/logger.js';
1
2
  import { getBy, verifyPassword, migrate } from '@/engine';
3
+
4
+ const log = createLogger('[Login]');
2
5
  import { initializeSystemConfig } from '@/config/system-config-loader';
3
6
  import { withErrorHandler } from '@/lib/with-error-handler';
4
7
  import { lucia } from '@/engine.server';
@@ -40,7 +43,7 @@ export const POST = withErrorHandler(async (request) => {
40
43
  await initializeSystemConfig();
41
44
  migrate();
42
45
  } catch (e) {
43
- console.error('[Login] Init failed:', e.message);
46
+ log.error('init failed:', { message: e.message });
44
47
  return new Response(JSON.stringify({ error: 'System init failed' }), { status: 500, headers: { 'Content-Type': 'application/json' } });
45
48
  }
46
49
  }
@@ -115,7 +118,7 @@ export const POST = withErrorHandler(async (request) => {
115
118
  }
116
119
  });
117
120
  } catch (err) {
118
- console.error('[Login] Error:', err.message);
121
+ log.error(err.message);
119
122
  return new Response(JSON.stringify({ error: 'Authentication failed' }), { status: 500, headers: { 'Content-Type': 'application/json' } });
120
123
  }
121
124
  }, 'Auth:Login');
@@ -1,6 +1,6 @@
1
- import { getBy } from '@/engine';
1
+ import { getBy, list, update } from '@/engine';
2
2
  import { lucia } from '@/engine.server';
3
- import { getDatabase, now } from '@/lib/database-core';
3
+ import { now } from '@/lib/id-helpers';
4
4
  import { withErrorHandler } from '@/lib/with-error-handler';
5
5
  import crypto from 'crypto';
6
6
 
@@ -21,34 +21,24 @@ export const POST = withErrorHandler(async (request) => {
21
21
  }
22
22
 
23
23
  const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
24
- const db = getDatabase();
25
24
 
26
- let bridgeRecord;
25
+ let bridgeRecord = null;
27
26
  try {
28
- bridgeRecord = db.prepare('SELECT * FROM mwr_bridge_tokens WHERE token_hash = ? AND used = 0').get(tokenHash);
29
- } catch (e) {
30
- db.exec(`CREATE TABLE IF NOT EXISTS mwr_bridge_tokens (
31
- id TEXT PRIMARY KEY,
32
- token_hash TEXT NOT NULL,
33
- email TEXT NOT NULL,
34
- expires_at INTEGER NOT NULL,
35
- used INTEGER DEFAULT 0,
36
- created_at INTEGER NOT NULL
37
- )`);
38
- db.exec('CREATE INDEX IF NOT EXISTS idx_mwr_bridge_token ON mwr_bridge_tokens(token_hash)');
39
- bridgeRecord = null;
40
- }
27
+ const matches = (await list('mwr_bridge_tokens', { token_hash: tokenHash }))
28
+ .filter(r => !r.used || r.used === 0);
29
+ bridgeRecord = matches[0] || null;
30
+ } catch { bridgeRecord = null; }
41
31
 
42
- const user = getBy('user', 'email', email.toLowerCase().trim());
32
+ const user = await getBy('user', 'email', email.toLowerCase().trim());
43
33
  if (!user) {
44
- return new Response(JSON.stringify({ error: 'User not found in Moonlanding' }), {
34
+ return new Response(JSON.stringify({ error: 'User not found' }), {
45
35
  status: 404, headers: { 'Content-Type': 'application/json' }
46
36
  });
47
37
  }
48
38
 
49
39
  if (bridgeRecord) {
50
40
  if (bridgeRecord.expires_at < now()) {
51
- db.prepare('UPDATE mwr_bridge_tokens SET used = 1 WHERE id = ?').run(bridgeRecord.id);
41
+ await update('mwr_bridge_tokens', bridgeRecord.id, { used: 1 });
52
42
  return new Response(JSON.stringify({ error: 'Bridge token has expired' }), {
53
43
  status: 401, headers: { 'Content-Type': 'application/json' }
54
44
  });
@@ -60,7 +50,7 @@ export const POST = withErrorHandler(async (request) => {
60
50
  });
61
51
  }
62
52
 
63
- db.prepare('UPDATE mwr_bridge_tokens SET used = 1 WHERE id = ?').run(bridgeRecord.id);
53
+ await update('mwr_bridge_tokens', bridgeRecord.id, { used: 1 });
64
54
  }
65
55
 
66
56
  const session = await lucia.createSession(user.id, {});
@@ -1,6 +1,5 @@
1
- import { getBy } from '@/engine';
2
- import { hashPassword } from '@/engine';
3
- import { getDatabase, genId, now } from '@/lib/database-core';
1
+ import { getBy, hashPassword, list, create, update, remove } from '@/engine';
2
+ import { genId, now } from '@/lib/id-helpers';
4
3
  import { withErrorHandler } from '@/lib/with-error-handler';
5
4
  import crypto from 'crypto';
6
5
 
@@ -12,16 +11,16 @@ export const POST = withErrorHandler(async (request) => {
12
11
  return new Response(JSON.stringify({ error: 'Email is required' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
13
12
  }
14
13
 
15
- const user = getBy('user', 'email', email);
14
+ const user = await getBy('user', 'email', email);
16
15
 
17
16
  if (user) {
18
- const db = getDatabase();
19
17
  const token = crypto.randomBytes(32).toString('hex');
20
18
  const expiresAt = now() + 3600;
21
19
 
22
- db.prepare('DELETE FROM password_reset_tokens WHERE user_id = ?').run(user.id);
23
- db.prepare('INSERT INTO password_reset_tokens (id, user_id, token, expires_at, used, created_at) VALUES (?, ?, ?, ?, 0, ?)').run(genId(), user.id, token, expiresAt, now());
24
-
20
+ // Clear any existing reset tokens for this user, then issue a fresh one.
21
+ const existing = await list('password_reset_tokens', { user_id: user.id });
22
+ for (const t of existing) await remove('password_reset_tokens', t.id);
23
+ await create('password_reset_tokens', { id: genId(), user_id: user.id, token, expires_at: expiresAt, used: 0, created_at: now() });
25
24
  }
26
25
 
27
26
  return new Response(JSON.stringify({ status: 'success', message: 'If an account exists with that email, a reset link has been sent.' }), {
@@ -41,23 +40,25 @@ export const PUT = withErrorHandler(async (request) => {
41
40
  return new Response(JSON.stringify({ error: 'Password must be at least 8 characters' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
42
41
  }
43
42
 
44
- const db = getDatabase();
45
- const resetToken = db.prepare('SELECT * FROM password_reset_tokens WHERE token = ? AND used = 0').get(token);
43
+ const matches = (await list('password_reset_tokens', { token })).filter(t => !t.used || t.used === 0);
44
+ const resetToken = matches[0];
46
45
 
47
46
  if (!resetToken) {
48
47
  return new Response(JSON.stringify({ error: 'Invalid or expired reset token' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
49
48
  }
50
49
 
51
50
  if (resetToken.expires_at < now()) {
52
- db.prepare('UPDATE password_reset_tokens SET used = 1 WHERE id = ?').run(resetToken.id);
51
+ await update('password_reset_tokens', resetToken.id, { used: 1 });
53
52
  return new Response(JSON.stringify({ error: 'Reset token has expired. Please request a new one.' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
54
53
  }
55
54
 
56
55
  const passwordHash = await hashPassword(password);
57
56
 
58
- db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(passwordHash, resetToken.user_id);
59
- db.prepare('UPDATE password_reset_tokens SET used = 1 WHERE id = ?').run(resetToken.id);
60
- db.prepare('DELETE FROM sessions WHERE user_id = ?').run(resetToken.user_id);
57
+ await update('user', resetToken.user_id, { password_hash: passwordHash });
58
+ await update('password_reset_tokens', resetToken.id, { used: 1 });
59
+ // Invalidate all sessions for this user.
60
+ const sessions = await list('sessions', { user_id: resetToken.user_id });
61
+ for (const s of sessions) await remove('sessions', s.id);
61
62
 
62
63
  return new Response(JSON.stringify({ status: 'success', message: 'Password updated successfully' }), {
63
64
  status: 200, headers: { 'Content-Type': 'application/json' }
@@ -1,4 +1,7 @@
1
+ import { createLogger } from '@/lib/logger.js';
1
2
  import { runDueJobs } from '@/engine/job-engine';
3
+
4
+ const log = createLogger('[Cron]');
2
5
  import { create } from '@/engine';
3
6
 
4
7
  export const runtime = 'nodejs';
@@ -28,7 +31,7 @@ export async function POST(request) {
28
31
  duration_ms: duration,
29
32
  status: results.failed > 0 ? 'partial_failure' : 'success',
30
33
  error_details: results.errors || null
31
- }).catch(err => console.error('[Cron] Log error:', err.message));
34
+ }).catch(err => log.error('log error:', { message: err.message }));
32
35
 
33
36
  return new Response(
34
37
  JSON.stringify({
@@ -1,8 +1,3 @@
1
- /**
2
- * Debug API Routes - Comprehensive observability endpoints
3
- * Exposes all subsystem state as JSON for debugging and monitoring
4
- */
5
-
6
1
  import { tracer } from '@/lib/tracing.js';
7
2
  import { perfProfiler } from '@/lib/perf-profiler.js';
8
3
  import { debugRegistry } from '@/lib/debug-registry.js';
@@ -27,6 +22,12 @@ function errorResponse(message, status = 400) {
27
22
  }
28
23
 
29
24
  export async function GET(request) {
25
+ // The debug surface exposes memory, CPU, queries, hooks, config and env state.
26
+ // That is operational intelligence an attacker must never get in production, so
27
+ // the endpoint is disabled there outright (opt back in only behind real auth).
28
+ if (process.env.NODE_ENV === 'production') {
29
+ return errorResponse('Debug endpoints are disabled in production', 403);
30
+ }
30
31
  const url = new URL(request.url);
31
32
  const path = url.pathname;
32
33
  const params = url.searchParams;
@@ -1,21 +1,16 @@
1
1
  import { NextResponse } from '@/lib/next-polyfills';
2
- import { getDatabase } from '@/engine';
3
2
 
3
+ // The data layer is busybase (LanceDB), not SQLite — the old pragma/page-count probe
4
+ // no longer applies. This endpoint now reports the configured store kind.
4
5
  export async function GET() {
5
6
  if (process.env.NODE_ENV === 'production') {
6
7
  return NextResponse.json({ error: 'Not available in production' }, { status: 403 });
7
8
  }
8
- try {
9
- const db = getDatabase();
10
- const page_count = db.pragma('page_count', { simple: true });
11
- const page_size = db.pragma('page_size', { simple: true });
12
- const journal_mode = db.pragma('journal_mode', { simple: true });
13
- const wal_autocheckpoint = db.pragma('wal_autocheckpoint', { simple: true });
14
- const path = db.name;
15
- return NextResponse.json({ path, page_count, page_size, bytes: page_count * page_size, journal_mode, wal_autocheckpoint });
16
- } catch (e) {
17
- return NextResponse.json({ error: String(e?.message || e) }, { status: 500 });
18
- }
9
+ return NextResponse.json({
10
+ store: 'busybase',
11
+ dir: process.env.BUSYBASE_DIR || 'busybase_data',
12
+ note: 'SQLite has been replaced by busybase (LanceDB); pragma stats are not applicable.',
13
+ });
19
14
  }
20
15
 
21
16
  export const config = { runtime: 'nodejs' };
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { NextResponse } from '@/lib/next-polyfills';
4
- import { getDatabase } from '@/engine';
4
+ import { count } from '@/engine';
5
5
 
6
6
  export async function GET() {
7
7
  if (process.env.NODE_ENV === 'production') {
@@ -17,10 +17,10 @@ export async function GET() {
17
17
  } catch {}
18
18
  let table_counts = {};
19
19
  try {
20
- const db = getDatabase();
21
- const tables = ['users', 'engagement', 'review', 'rfi_template', 'entity_type', 'engagement_type', 'client'];
22
- for (const t of tables) {
23
- try { table_counts[t] = db.prepare(`SELECT COUNT(*) c FROM ${t}`).get()?.c ?? null; } catch { table_counts[t] = null; }
20
+ // entity names (busybase tables); 'user' maps to the users table in the store
21
+ const entities = ['user', 'engagement', 'review', 'rfi_template', 'entity_type', 'engagement_type', 'client'];
22
+ for (const e of entities) {
23
+ try { table_counts[e] = await count(e, {}); } catch { table_counts[e] = null; }
24
24
  }
25
25
  } catch (e) { table_counts._error = String(e?.message || e); }
26
26
  return NextResponse.json({ last_run_at, table_counts });
@@ -1,4 +1,7 @@
1
+ import { createLogger } from '@/lib/logger.js';
1
2
  import { getDomainLoader } from '@/lib/domain-loader';
3
+
4
+ const log = createLogger('[DomainsAPI]');
2
5
  import { ok } from '@/lib/response-formatter';
3
6
  import { withErrorHandler } from '@/lib/with-error-handler';
4
7
  import { getConfigEngine } from '@/lib/config-generator-engine';
@@ -12,7 +15,7 @@ export const GET = withErrorHandler(async (request) => {
12
15
  try {
13
16
  return domainLoader.getDomainInfo(domainName);
14
17
  } catch (error) {
15
- console.error(`[Domains API] Error loading domain ${domainName}:`, error.message);
18
+ log.error(`error loading domain ${domainName}:`, { message: error.message });
16
19
  return null;
17
20
  }
18
21
  }).filter(Boolean);
@@ -1,5 +1,9 @@
1
1
  import { NextResponse } from '@/lib/next-polyfills';
2
- import { getDatabase, genId, now } from '@/lib/database-core';
2
+ import { createLogger } from '@/lib/logger.js';
3
+
4
+ const log = createLogger('[EmailBatchAllocate]');
5
+ import { genId, now } from '@/lib/id-helpers';
6
+ import { list, create, update } from '@/engine';
3
7
  import { autoAllocateEmail } from '@/lib/email-parser';
4
8
 
5
9
  export async function POST(request) {
@@ -14,14 +18,9 @@ export async function POST(request) {
14
18
  const body = await request.json();
15
19
  const { min_confidence = 70, batch_size = 50 } = body;
16
20
 
17
- const db = getDatabase();
18
-
19
- const unallocatedEmails = db.prepare(`
20
- SELECT * FROM email
21
- WHERE allocated = 0 AND status = 'pending'
22
- ORDER BY received_at DESC
23
- LIMIT ?
24
- `).all(batch_size);
21
+ const unallocatedEmails = (await list('email', { status: 'pending' }, { sort: { field: 'received_at', dir: 'DESC' } }))
22
+ .filter(e => !e.allocated || e.allocated === 0)
23
+ .slice(0, batch_size);
25
24
 
26
25
  const results = {
27
26
  allocated: [],
@@ -38,24 +37,20 @@ export async function POST(request) {
38
37
  const logId = genId();
39
38
  const timestamp = now();
40
39
 
41
- db.prepare(`
42
- INSERT INTO activity_log (
43
- id, entity_type, entity_id, action, message, details, created_at
44
- ) VALUES (?, ?, ?, ?, ?, ?, ?)
45
- `).run(
46
- logId,
47
- 'email',
48
- email.id,
49
- 'batch_allocated',
50
- `Email batch-allocated to ${result.engagement_id ? 'engagement' : 'RFI'}`,
51
- JSON.stringify({
40
+ await create('activity_log', {
41
+ id: logId,
42
+ entity_type: 'email',
43
+ entity_id: email.id,
44
+ action: 'batch_allocated',
45
+ message: `Email batch-allocated to ${result.engagement_id ? 'engagement' : 'RFI'}`,
46
+ details: JSON.stringify({
52
47
  engagement_id: result.engagement_id || null,
53
48
  rfi_id: result.rfi_id || null,
54
49
  confidence: result.confidence,
55
50
  method: 'batch_automatic',
56
51
  }),
57
- timestamp
58
- );
52
+ created_at: timestamp,
53
+ });
59
54
 
60
55
  results.allocated.push({
61
56
  email_id: email.id,
@@ -85,19 +80,14 @@ export async function POST(request) {
85
80
  reason: error.message,
86
81
  });
87
82
 
88
- db.prepare(`
89
- UPDATE email
90
- SET processing_error = ?,
91
- updated_at = ?
92
- WHERE id = ?
93
- `).run(error.message, now(), email.id);
83
+ await update('email', email.id, { processing_error: error.message, updated_at: now() });
94
84
  }
95
85
  }
96
86
 
97
87
  return NextResponse.json(results);
98
88
 
99
89
  } catch (error) {
100
- console.error('[EMAIL_BATCH_ALLOCATE] Error:', error);
90
+ log.error('error:', { message: error.message });
101
91
  return NextResponse.json(
102
92
  { error: 'Internal server error', message: error.message },
103
93
  { status: 500 }
@@ -1,5 +1,9 @@
1
1
  import { NextResponse } from '@/lib/next-polyfills';
2
- import { getDatabase, genId, now } from '@/lib/database-core';
2
+ import { createLogger } from '@/lib/logger.js';
3
+ import { genId, now } from '@/lib/id-helpers';
4
+ import { get, create } from '@/engine';
5
+
6
+ const log = createLogger('[EmailAllocate]');
3
7
  import {
4
8
  allocateEmailToEntity,
5
9
  autoAllocateEmail,
@@ -23,8 +27,7 @@ export async function POST(request) {
23
27
  );
24
28
  }
25
29
 
26
- const db = getDatabase();
27
- const email = db.prepare('SELECT * FROM email WHERE id = ?').get(email_id);
30
+ const email = await get('email', email_id);
28
31
 
29
32
  if (!email) {
30
33
  return NextResponse.json(
@@ -104,24 +107,20 @@ export async function POST(request) {
104
107
  const logId = genId();
105
108
  const timestamp = now();
106
109
 
107
- db.prepare(`
108
- INSERT INTO activity_log (
109
- id, entity_type, entity_id, action, message, details, created_at
110
- ) VALUES (?, ?, ?, ?, ?, ?, ?)
111
- `).run(
112
- logId,
113
- 'email',
114
- email_id,
115
- 'allocated',
116
- `Email allocated to ${result.engagement_id ? 'engagement' : 'RFI'}`,
117
- JSON.stringify({
110
+ await create('activity_log', {
111
+ id: logId,
112
+ entity_type: 'email',
113
+ entity_id: email_id,
114
+ action: 'allocated',
115
+ message: `Email allocated to ${result.engagement_id ? 'engagement' : 'RFI'}`,
116
+ details: JSON.stringify({
118
117
  engagement_id: result.engagement_id || null,
119
118
  rfi_id: result.rfi_id || null,
120
119
  method: auto ? 'automatic' : 'manual',
121
120
  confidence: result.confidence || 100,
122
121
  }),
123
- timestamp
124
- );
122
+ created_at: timestamp,
123
+ });
125
124
 
126
125
  return NextResponse.json({
127
126
  success: true,
@@ -132,7 +131,7 @@ export async function POST(request) {
132
131
  });
133
132
 
134
133
  } catch (error) {
135
- console.error('[EMAIL_ALLOCATE] Error:', error);
134
+ log.error('error:', { message: error.message });
136
135
  return NextResponse.json(
137
136
  { error: 'Internal server error', message: error.message },
138
137
  { status: 500 }
@@ -1,12 +1,16 @@
1
1
  import { NextResponse } from '@/lib/next-polyfills';
2
- import { getDatabase, genId, now } from '@/lib/database-core';
2
+ import { createLogger } from '@/lib/logger.js';
3
+ import { genId, now } from '@/lib/id-helpers';
4
+ import { create } from '@/engine';
5
+
6
+ const log = createLogger('[EmailReceive]');
3
7
  import path from 'path';
4
8
  import fs from 'fs';
5
9
 
6
- const TEMP_EMAIL_ATTACHMENTS_DIR = path.resolve(process.cwd(), 'data', 'temp_email_attachments');
10
+ const EMAIL_ATTACHMENTS_DIR = path.resolve(process.cwd(), 'data', 'temp_email_attachments');
7
11
 
8
- if (!fs.existsSync(TEMP_EMAIL_ATTACHMENTS_DIR)) {
9
- fs.mkdirSync(TEMP_EMAIL_ATTACHMENTS_DIR, { recursive: true });
12
+ if (!fs.existsSync(EMAIL_ATTACHMENTS_DIR)) {
13
+ fs.mkdirSync(EMAIL_ATTACHMENTS_DIR, { recursive: true });
10
14
  }
11
15
 
12
16
  export async function POST(request) {
@@ -49,7 +53,7 @@ export async function POST(request) {
49
53
  const attachmentId = genId();
50
54
  const ext = path.extname(filename);
51
55
  const safeFilename = `${attachmentId}${ext}`;
52
- const filePath = path.join(TEMP_EMAIL_ATTACHMENTS_DIR, safeFilename);
56
+ const filePath = path.join(EMAIL_ATTACHMENTS_DIR, safeFilename);
53
57
 
54
58
  const buffer = Buffer.isBuffer(content)
55
59
  ? content
@@ -86,45 +90,30 @@ export async function POST(request) {
86
90
  processing_error: null,
87
91
  };
88
92
 
89
- const db = getDatabase();
90
- const stmt = db.prepare(`
91
- INSERT INTO email (
92
- id, sender_email, sender_name, subject, body, html_body,
93
- message_id, in_reply_to, references, received_at, allocated,
94
- engagement_id, rfi_id, attachments, status, processed, processing_error,
95
- created_at, updated_at
96
- ) VALUES (
97
- ?, ?, ?, ?, ?, ?,
98
- ?, ?, ?, ?, ?,
99
- ?, ?, ?, ?, ?, ?,
100
- ?, ?
101
- )
102
- `);
103
-
104
93
  const emailId = genId();
105
94
  const timestamp = now();
106
95
 
107
- stmt.run(
108
- emailId,
109
- emailRecord.sender_email,
110
- emailRecord.sender_name,
111
- emailRecord.subject,
112
- emailRecord.body,
113
- emailRecord.html_body,
114
- emailRecord.message_id,
115
- emailRecord.in_reply_to,
116
- emailRecord.references,
117
- emailRecord.received_at,
118
- emailRecord.allocated ? 1 : 0,
119
- emailRecord.engagement_id,
120
- emailRecord.rfi_id,
121
- emailRecord.attachments,
122
- emailRecord.status,
123
- emailRecord.processed ? 1 : 0,
124
- emailRecord.processing_error,
125
- timestamp,
126
- timestamp
127
- );
96
+ await create('email', {
97
+ id: emailId,
98
+ sender_email: emailRecord.sender_email,
99
+ sender_name: emailRecord.sender_name,
100
+ subject: emailRecord.subject,
101
+ body: emailRecord.body,
102
+ html_body: emailRecord.html_body,
103
+ message_id: emailRecord.message_id,
104
+ in_reply_to: emailRecord.in_reply_to || '',
105
+ references: emailRecord.references || '',
106
+ received_at: emailRecord.received_at,
107
+ allocated: emailRecord.allocated ? 1 : 0,
108
+ engagement_id: emailRecord.engagement_id || '',
109
+ rfi_id: emailRecord.rfi_id || '',
110
+ attachments: emailRecord.attachments,
111
+ status: emailRecord.status,
112
+ processed: emailRecord.processed ? 1 : 0,
113
+ processing_error: emailRecord.processing_error || '',
114
+ created_at: timestamp,
115
+ updated_at: timestamp,
116
+ });
128
117
 
129
118
  return NextResponse.json({
130
119
  success: true,
@@ -134,7 +123,7 @@ export async function POST(request) {
134
123
  });
135
124
 
136
125
  } catch (error) {
137
- console.error('[EMAIL_RECEIVE] Error processing webhook:', error);
126
+ log.error('error processing webhook:', { message: error.message });
138
127
 
139
128
  return NextResponse.json(
140
129
  {
@@ -1,5 +1,9 @@
1
1
  import { NextResponse } from '@/lib/next-polyfills';
2
- import { getDatabase, now } from '@/lib/database-core';
2
+ import { createLogger } from '@/lib/logger.js';
3
+ import { now } from '@/lib/id-helpers';
4
+
5
+ const log = createLogger('[Email]');
6
+ import { list, update } from '@/engine';
3
7
  import { getConfigEngine } from '@/lib/config-generator-engine';
4
8
  import { EMAIL_STATUS } from '@/config/constants';
5
9
  import { sendSingleEmail, checkFailureRate } from '@/lib/email-sender';
@@ -19,7 +23,6 @@ export async function POST(request) {
19
23
  if (!token || token !== process.env.CRON_SECRET)
20
24
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
21
25
 
22
- const db = getDatabase();
23
26
  try {
24
27
  const emailCfg = await getEmailConfig();
25
28
  const MAX_RETRIES = emailCfg.send_max_retries || 3;
@@ -27,8 +30,10 @@ export async function POST(request) {
27
30
  const RATE_LIMIT_DELAY = emailCfg.rate_limit_delay_ms || 6000;
28
31
  const MAX_DELAY_MS = emailCfg.retry_max_delay_ms || 30000;
29
32
 
30
- const pendingEmails = db.prepare(`SELECT * FROM email WHERE status=? AND (retry_count IS NULL OR retry_count < ?) ORDER BY created_at ASC LIMIT ?`)
31
- .all(EMAIL_STATUS.PENDING, MAX_RETRIES, BATCH_SIZE);
33
+ // pending emails under the retry cap, oldest first, limited to the batch size
34
+ const pendingEmails = (await list('email', { status: EMAIL_STATUS.PENDING }, { sort: { field: 'created_at', dir: 'ASC' } }))
35
+ .filter(e => (e.retry_count == null || e.retry_count < MAX_RETRIES))
36
+ .slice(0, BATCH_SIZE);
32
37
 
33
38
  if (!pendingEmails.length)
34
39
  return NextResponse.json({ success: true, message: 'No pending emails', processed: 0 });
@@ -38,18 +43,18 @@ export async function POST(request) {
38
43
 
39
44
  for (let i = 0; i < pendingEmails.length; i++) {
40
45
  const email = pendingEmails[i];
41
- db.prepare(`UPDATE email SET status=?, updated_at=? WHERE id=?`).run(EMAIL_STATUS.PROCESSING, now(), email.id);
42
- const result = await sendSingleEmail(db, email, email.retry_count || 1, MAX_RETRIES, MAX_DELAY_MS);
46
+ await update('email', email.id, { status: EMAIL_STATUS.PROCESSING, updated_at: now() });
47
+ const result = await sendSingleEmail(email, email.retry_count || 1, MAX_RETRIES, MAX_DELAY_MS);
43
48
  results.push(result);
44
49
  result.success ? successCount++ : failureCount++;
45
50
  if (i < pendingEmails.length - 1)
46
51
  await new Promise(r => setTimeout(r, RATE_LIMIT_DELAY));
47
52
  }
48
53
 
49
- checkFailureRate(db);
54
+ await checkFailureRate();
50
55
  return NextResponse.json({ success: true, processed: pendingEmails.length, results: { success: successCount, failed: failureCount }, details: results });
51
56
  } catch (error) {
52
- console.error('[EMAIL] Queue processing error:', error);
57
+ log.error('queue processing error:', { message: error.message });
53
58
  return NextResponse.json({ error: 'Email queue processing failed', details: error.message }, { status: 500 });
54
59
  }
55
60
  }
@@ -59,16 +64,23 @@ export async function GET(request) {
59
64
  if (!token || token !== process.env.CRON_SECRET)
60
65
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
61
66
 
62
- const db = getDatabase();
63
67
  try {
64
- const stats = db.prepare(`SELECT status, COUNT(*) as count FROM email GROUP BY status`).all();
65
- const failureStats = db.prepare(`SELECT COUNT(*) as total, SUM(CASE WHEN status=? THEN 1 ELSE 0 END) as failed FROM email WHERE created_at >= ?`)
66
- .get(EMAIL_STATUS.FAILED, now() - 86400);
67
- const recentFailures = db.prepare(`SELECT id, recipient_email, subject, processing_error, retry_count, created_at FROM email WHERE status=? ORDER BY created_at DESC LIMIT 10`)
68
- .all(EMAIL_STATUS.FAILED);
68
+ const emails = await list('email', {});
69
+ // status counts (old GROUP BY status)
70
+ const stats = {};
71
+ for (const e of emails) stats[e.status] = (stats[e.status] || 0) + 1;
72
+ // failure rate over the last 24h
73
+ const cutoff = now() - 86400;
74
+ const recent = emails.filter(e => (e.created_at || 0) >= cutoff);
75
+ const failedTotal = recent.filter(e => e.status === EMAIL_STATUS.FAILED).length;
76
+ const recentFailures = emails
77
+ .filter(e => e.status === EMAIL_STATUS.FAILED)
78
+ .sort((a, b) => (b.created_at || 0) - (a.created_at || 0))
79
+ .slice(0, 10)
80
+ .map(e => ({ id: e.id, recipient_email: e.recipient_email, subject: e.subject, processing_error: e.processing_error, retry_count: e.retry_count, created_at: e.created_at }));
69
81
  return NextResponse.json({
70
- stats: stats.reduce((acc, r) => ({ ...acc, [r.status]: r.count }), {}),
71
- failureRate: failureStats.total > 0 ? failureStats.failed / failureStats.total : 0,
82
+ stats,
83
+ failureRate: recent.length > 0 ? failedTotal / recent.length : 0,
72
84
  recentFailures,
73
85
  });
74
86
  } catch (error) {