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,10 +1,7 @@
1
- /**
2
- * CRUD Handlers - HTTP handler implementations for entity operations
3
- * This is where request/response handling, validation, auth, and business logic meet
4
- */
1
+ import { createLogger } from './logger.js';
2
+ import { get, listWithPagination, searchWithPagination, create, update, remove } from './busybase-store.js';
5
3
 
6
- import { list, get, listWithPagination, searchWithPagination } from './query-engine.js';
7
- import { create, update, remove } from './query-engine-write.js';
4
+ const log = createLogger('[CRUD]');
8
5
  import { validateEntity, validateUpdate, sanitizeData } from './validate.js';
9
6
  import { requirePermission, getSessionToken } from './auth-middleware.js';
10
7
  import { executeHook } from './hook-engine.js';
@@ -13,24 +10,16 @@ import { ok, created, paginated, noContent, error } from './response-formatter.j
13
10
  import { HTTP } from '../config/constants.js';
14
11
  import { permissionService } from '../services/permission.service.js';
15
12
  import { parse as parseQuery } from './query-string-adapter.js';
16
- import { now } from './database-core.js';
13
+ import { now } from './id-helpers.js';
17
14
  import { getConfigEngineSync } from './config-generator-engine.js';
18
- import { logAction } from './audit-logger.js';
15
+ import { logAction } from './busybase-audit.js';
19
16
 
20
- /**
21
- * Build a complete set of CRUD handlers for an entity
22
- * @param {object} spec - Entity specification
23
- * @returns {object}
24
- */
25
17
  export function createCrudHandlers(entityName, spec) {
26
18
  if (!spec) {
27
19
  spec = getConfigEngineSync().generateEntitySpec(entityName);
28
20
  }
29
21
 
30
22
  return {
31
- /**
32
- * List entities (GET /api/:entity)
33
- */
34
23
  list: async (req, context) => {
35
24
  const { user } = context;
36
25
  await requirePermission(user, spec, 'list');
@@ -76,16 +65,13 @@ export function createCrudHandlers(entityName, spec) {
76
65
  return paginated(filteredItems, pagination);
77
66
  },
78
67
 
79
- /**
80
- * Get single entity (GET /api/:entity/:id)
81
- */
82
68
  get: async (id, req, context) => {
83
69
  const { user } = context;
84
70
  await requirePermission(user, spec, 'view');
85
71
 
86
72
  if (!id) throw new AppError('ID required', 'BAD_REQUEST', HTTP.BAD_REQUEST);
87
73
 
88
- const item = get(entityName, id);
74
+ const item = await get(entityName, id);
89
75
  if (!item) throw NotFoundError(entityName, id);
90
76
 
91
77
  if (!permissionService.checkRowAccess(user, spec, item)) {
@@ -95,9 +81,6 @@ export function createCrudHandlers(entityName, spec) {
95
81
  return ok(permissionService.filterFields(user, spec, item));
96
82
  },
97
83
 
98
- /**
99
- * Create entity (POST /api/:entity)
100
- */
101
84
  create: async (req, context) => {
102
85
  const { user } = context;
103
86
  await requirePermission(user, spec, 'create');
@@ -111,32 +94,28 @@ export function createCrudHandlers(entityName, spec) {
111
94
  }
112
95
 
113
96
  const sanitized = sanitizeData(entityName, rawData, spec);
114
- const record = create(entityName, sanitized, user);
97
+ const record = await create(entityName, sanitized, user);
115
98
 
116
99
  // Audit log (ported from moon)
117
100
  logAction(entityName, record.id, 'create', user?.id, null, record);
118
101
 
119
- // Execute hooks
120
102
  executeHook(`create:${entityName}:after`, {
121
103
  entity: entityName,
122
104
  id: record.id,
123
105
  data: record,
124
106
  user,
125
- }).catch(console.error);
107
+ }).catch(e => log.error(e.message));
126
108
 
127
109
  return created(permissionService.filterFields(user, spec, record));
128
110
  },
129
111
 
130
- /**
131
- * Update entity (PUT/PATCH /api/:entity/:id)
132
- */
133
112
  update: async (id, req, context) => {
134
113
  const { user } = context;
135
114
  await requirePermission(user, spec, 'edit');
136
115
 
137
116
  if (!id) throw new AppError('ID required', 'BAD_REQUEST', HTTP.BAD_REQUEST);
138
117
 
139
- const existing = get(entityName, id);
118
+ const existing = await get(entityName, id);
140
119
  if (!existing) throw NotFoundError(entityName, id);
141
120
 
142
121
  if (!permissionService.checkRowAccess(user, spec, existing)) {
@@ -152,7 +131,7 @@ export function createCrudHandlers(entityName, spec) {
152
131
  }
153
132
 
154
133
  const sanitized = sanitizeData(entityName, rawData, spec, existing);
155
- const record = update(entityName, id, sanitized, user);
134
+ const record = await update(entityName, id, sanitized, user);
156
135
 
157
136
  // Audit log (ported from moon)
158
137
  logAction(entityName, id, 'update', user?.id, existing, record);
@@ -164,21 +143,18 @@ export function createCrudHandlers(entityName, spec) {
164
143
  before: existing,
165
144
  after: record,
166
145
  user,
167
- }).catch(console.error);
146
+ }).catch(e => log.error(e.message));
168
147
 
169
148
  return ok(permissionService.filterFields(user, spec, record));
170
149
  },
171
150
 
172
- /**
173
- * Delete entity (DELETE /api/:entity/:id)
174
- */
175
151
  remove: async (id, req, context) => {
176
152
  const { user } = context;
177
153
  await requirePermission(user, spec, 'delete');
178
154
 
179
155
  if (!id) throw new AppError('ID required', 'BAD_REQUEST', HTTP.BAD_REQUEST);
180
156
 
181
- const existing = get(entityName, id);
157
+ const existing = await get(entityName, id);
182
158
  if (!existing) throw NotFoundError(entityName, id);
183
159
 
184
160
  if (!permissionService.checkRowAccess(user, spec, existing)) {
@@ -190,13 +166,13 @@ export function createCrudHandlers(entityName, spec) {
190
166
  let result;
191
167
  if (spec.immutable === true && spec.immutable_strategy === 'move_to_archive') {
192
168
  const archiveData = { archived: true, archived_at: now(), archived_by: user?.id };
193
- result = update(entityName, id, archiveData, user);
169
+ result = await update(entityName, id, archiveData, user);
194
170
  logAction(entityName, id, 'archive', user?.id, existing, archiveData);
195
171
  } else if (spec.fields?.status) {
196
- result = update(entityName, id, { status: 'deleted' }, user);
172
+ result = await update(entityName, id, { status: 'deleted' }, user);
197
173
  logAction(entityName, id, 'delete', user?.id, existing, { status: 'deleted' });
198
174
  } else {
199
- result = remove(entityName, id);
175
+ result = await remove(entityName, id);
200
176
  logAction(entityName, id, 'delete', user?.id, existing, null);
201
177
  }
202
178
 
@@ -205,22 +181,18 @@ export function createCrudHandlers(entityName, spec) {
205
181
  id,
206
182
  data: result,
207
183
  user,
208
- }).catch(console.error);
184
+ }).catch(e => log.error(e.message));
209
185
 
210
186
  return noContent();
211
187
  },
212
188
 
213
- /**
214
- * Custom action handler
215
- */
216
189
  customAction: async (action, id, data, context) => {
217
190
  const { user } = context;
218
191
  await requirePermission(user, spec, 'edit'); // Simplified permission check
219
192
 
220
- const record = get(entityName, id);
193
+ const record = await get(entityName, id);
221
194
  if (!record) throw NotFoundError(entityName, id);
222
195
 
223
- // Custom action logic (upload, manage_flags, etc.)
224
196
  if (action === 'upload_files') {
225
197
  const files = Array.isArray(data.files) ? data.files : [data.files];
226
198
  executeHook(`upload_files:${entityName}:after`, {
@@ -232,18 +204,11 @@ export function createCrudHandlers(entityName, spec) {
232
204
  return ok({ id, uploaded_files: files });
233
205
  }
234
206
 
235
- // More actions can be added here
236
207
  throw new AppError(`Unknown action: ${action}`, 'BAD_REQUEST', HTTP.BAD_REQUEST);
237
208
  },
238
209
  };
239
210
  }
240
211
 
241
- /**
242
- * Coerce field value to correct type
243
- * @param {any} value
244
- * @param {string} type
245
- * @returns {any}
246
- */
247
212
  function coerceFieldValue(value, type) {
248
213
  if (value === null || value === undefined) return value;
249
214
 
@@ -1,6 +1,9 @@
1
+ import { createLogger } from './logger.js';
1
2
  import crypto from 'crypto';
2
3
  import { HTTP } from '@/config/constants';
3
4
 
5
+ const log = createLogger('[CSRF]');
6
+
4
7
  const tokens = new Map();
5
8
  const TOKEN_TTL = 3600000;
6
9
 
@@ -44,14 +47,14 @@ export async function withCsrfValidation(handler) {
44
47
  const token = request.headers.get('x-csrf-token');
45
48
 
46
49
  if (!token || !validateToken(token)) {
47
- console.warn(`[CSRF] Invalid token for ${method}`);
50
+ log.warn(`invalid token for ${method}`);
48
51
  return new Response(JSON.stringify({ error: 'Invalid CSRF token' }), {
49
52
  status: HTTP.FORBIDDEN,
50
53
  headers: { 'Content-Type': 'application/json' },
51
54
  });
52
55
  }
53
56
  } catch (e) {
54
- console.error('[CSRF] Validation error:', e.message);
57
+ log.error('validation error:', { message: e.message });
55
58
  return new Response(JSON.stringify({ error: 'CSRF validation failed' }), {
56
59
  status: HTTP.BAD_REQUEST,
57
60
  headers: { 'Content-Type': 'application/json' },
@@ -1,20 +1,10 @@
1
- /**
2
- * Date/Time Utilities - Common date operations
3
- */
4
-
5
- import { now } from './database-core.js';
1
+ import { now } from './id-helpers.js';
6
2
 
7
3
  const SECONDS_PER_MINUTE = 60;
8
4
  const SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE;
9
5
  const SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR;
10
6
  const SECONDS_PER_YEAR = 365.25 * SECONDS_PER_DAY;
11
7
 
12
- /**
13
- * Format Unix timestamp to human-readable date
14
- * @param {number} timestamp - Unix seconds
15
- * @param {string} [format='short']
16
- * @returns {string}
17
- */
18
8
  export function formatDate(timestamp, format = 'short') {
19
9
  if (!timestamp) return '—';
20
10
  const date = new Date(timestamp * 1000);
@@ -34,11 +24,6 @@ export function formatDate(timestamp, format = 'short') {
34
24
  return date.toLocaleDateString();
35
25
  }
36
26
 
37
- /**
38
- * Format relative time (e.g., "2 hours ago")
39
- * @param {number} timestamp - Unix seconds
40
- * @returns {string}
41
- */
42
27
  export function formatRelative(timestamp) {
43
28
  const nowSec = now();
44
29
  const diff = nowSec - timestamp;
@@ -59,13 +44,6 @@ export function formatRelative(timestamp) {
59
44
  return formatDate(timestamp, 'short');
60
45
  }
61
46
 
62
- /**
63
- * Check if timestamp is within allowed year range
64
- * @param {number} timestamp
65
- * @param {number} minYearsAgo
66
- * @param {number} maxYearsAhead
67
- * @returns {boolean}
68
- */
69
47
  export function isWithinYears(timestamp, minYearsAgo = 10, maxYearsAhead = 5) {
70
48
  const nowSec = now();
71
49
  return (
@@ -74,59 +52,28 @@ export function isWithinYears(timestamp, minYearsAgo = 10, maxYearsAhead = 5) {
74
52
  );
75
53
  }
76
54
 
77
- /**
78
- * Check if date1 is before date2
79
- * @param {number} date1 - Unix timestamp
80
- * @param {number} date2 - Unix timestamp
81
- * @returns {boolean}
82
- */
83
55
  export function isBeforeDate(date1, date2) {
84
56
  return date1 < date2;
85
57
  }
86
58
 
87
- /**
88
- * Add days to current timestamp
89
- * @param {number} days
90
- * @returns {number}
91
- */
92
59
  export function addDays(days) {
93
60
  return now() + (days * SECONDS_PER_DAY);
94
61
  }
95
62
 
96
- /**
97
- * Add hours to current timestamp
98
- * @param {number} hours
99
- * @returns {number}
100
- */
101
63
  export function addHours(hours) {
102
64
  return now() + (hours * SECONDS_PER_HOUR);
103
65
  }
104
66
 
105
- /**
106
- * Start of day (midnight) for timestamp
107
- * @param {number} [timestamp]
108
- * @returns {number}
109
- */
110
67
  export function startOfDay(timestamp = now()) {
111
68
  const date = new Date(timestamp * 1000);
112
69
  date.setHours(0, 0, 0, 0);
113
70
  return Math.floor(date.getTime() / 1000);
114
71
  }
115
72
 
116
- /**
117
- * End of day (23:59:59) for timestamp
118
- * @param {number} [timestamp]
119
- * @returns {number}
120
- */
121
73
  export function endOfDay(timestamp = now()) {
122
74
  return startOfDay(timestamp) + SECONDS_PER_DAY - 1;
123
75
  }
124
76
 
125
- /**
126
- * Get days remaining until deadline
127
- * @param {number} deadlineTs - Unix timestamp
128
- * @returns {number} - negative if past due
129
- */
130
77
  export function daysRemaining(deadlineTs) {
131
78
  const nowSec = now();
132
79
  const startOfNextDay = startOfDay(nowSec) + SECONDS_PER_DAY;
@@ -134,20 +81,10 @@ export function daysRemaining(deadlineTs) {
134
81
  return Math.ceil(remaining / SECONDS_PER_DAY);
135
82
  }
136
83
 
137
- /**
138
- * Check if timestamp is in the past
139
- * @param {number} timestamp
140
- * @returns {boolean}
141
- */
142
84
  export function isPast(timestamp) {
143
85
  return now() > timestamp;
144
86
  }
145
87
 
146
- /**
147
- * Format duration in seconds to human-readable
148
- * @param {number} seconds
149
- * @returns {string}
150
- */
151
88
  export function formatDuration(seconds) {
152
89
  if (seconds < 60) return `${seconds}s`;
153
90
  const mins = Math.floor(seconds / 60);
@@ -162,23 +99,12 @@ export function formatDuration(seconds) {
162
99
  // Additive helpers ported from moonlanding (adapted to Unix-seconds model)
163
100
  // ---------------------------------------------------------------------------
164
101
 
165
- /**
166
- * Whether a timestamp falls on a working day (Mon–Fri).
167
- * @param {number} timestamp - Unix seconds
168
- * @returns {boolean}
169
- */
170
102
  export function isWorkingDay(timestamp) {
171
103
  if (!timestamp) return false;
172
104
  const day = new Date(timestamp * 1000).getDay();
173
105
  return day !== 0 && day !== 6;
174
106
  }
175
107
 
176
- /**
177
- * Count working days (inclusive) between two Unix-seconds timestamps.
178
- * @param {number} startTs - Unix seconds
179
- * @param {number} endTs - Unix seconds
180
- * @returns {number}
181
- */
182
108
  export function getWorkingDaysDiff(startTs, endTs) {
183
109
  if (!startTs || !endTs) return 0;
184
110
  const end = new Date(endTs * 1000);
@@ -191,12 +117,6 @@ export function getWorkingDaysDiff(startTs, endTs) {
191
117
  return count;
192
118
  }
193
119
 
194
- /**
195
- * Add a number of working days (Mon–Fri) to a Unix-seconds timestamp.
196
- * @param {number} startTs - Unix seconds
197
- * @param {number} numDays
198
- * @returns {number} Unix seconds
199
- */
200
120
  export function addWorkingDays(startTs, numDays) {
201
121
  if (!startTs || numDays <= 0) return startTs;
202
122
  const date = new Date(startTs * 1000);
@@ -208,11 +128,7 @@ export function addWorkingDays(startTs, numDays) {
208
128
  return Math.floor(date.getTime() / 1000);
209
129
  }
210
130
 
211
- /**
212
- * Financial year (Mar 1 – Feb end) for a Unix-seconds timestamp.
213
- * @param {number} [timestamp] - Unix seconds (defaults to now)
214
- * @returns {number|null}
215
- */
131
+ // Financial year runs March 1 to end of February (South African fiscal convention).
216
132
  export function getFinancialYear(timestamp) {
217
133
  const ms = timestamp ? timestamp * 1000 : Date.now();
218
134
  const date = new Date(ms);
@@ -222,11 +138,6 @@ export function getFinancialYear(timestamp) {
222
138
  return month >= 2 ? year : year - 1;
223
139
  }
224
140
 
225
- /**
226
- * Start/end Unix-seconds bounds for a financial year (Mar 1 – Feb end).
227
- * @param {number} year
228
- * @returns {{ start: number, end: number }}
229
- */
230
141
  export function getFinancialYearRange(year) {
231
142
  const start = new Date(year, 2, 1);
232
143
  const lastDay = new Date(year + 1, 2, 0).getDate();
@@ -237,13 +148,6 @@ export function getFinancialYearRange(year) {
237
148
  };
238
149
  }
239
150
 
240
- /**
241
- * Format an amount as currency.
242
- * @param {number|string} amount
243
- * @param {string} [currency='ZAR']
244
- * @param {string} [locale='en-ZA']
245
- * @returns {string|null}
246
- */
247
151
  export function formatCurrency(amount, currency = 'ZAR', locale = 'en-ZA') {
248
152
  if (amount === null || amount === undefined) return null;
249
153
  const num = typeof amount === 'string' ? parseFloat(amount) : amount;
@@ -251,13 +155,6 @@ export function formatCurrency(amount, currency = 'ZAR', locale = 'en-ZA') {
251
155
  return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(num);
252
156
  }
253
157
 
254
- /**
255
- * Format a number with fixed decimals.
256
- * @param {number|string} value
257
- * @param {number} [decimals=0]
258
- * @param {string} [locale='en-ZA']
259
- * @returns {string|null}
260
- */
261
158
  export function formatNumber(value, decimals = 0, locale = 'en-ZA') {
262
159
  if (value === null || value === undefined) return null;
263
160
  const num = typeof value === 'string' ? parseFloat(value) : value;
@@ -268,11 +165,6 @@ export function formatNumber(value, decimals = 0, locale = 'en-ZA') {
268
165
  }).format(num);
269
166
  }
270
167
 
271
- /**
272
- * Human-readable file size.
273
- * @param {number} bytes
274
- * @returns {string|null}
275
- */
276
168
  export function formatFileSize(bytes) {
277
169
  if (bytes === null || bytes === undefined || bytes < 0) return null;
278
170
  if (bytes === 0) return '0 B';
@@ -282,13 +174,6 @@ export function formatFileSize(bytes) {
282
174
  return `${size.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
283
175
  }
284
176
 
285
- /**
286
- * Truncate text to a maximum length, appending a suffix.
287
- * @param {*} text
288
- * @param {number} [maxLength=100]
289
- * @param {string} [suffix='...']
290
- * @returns {string}
291
- */
292
177
  export function truncateText(text, maxLength = 100, suffix = '...') {
293
178
  if (!text) return '';
294
179
  const str = String(text);
@@ -296,11 +181,6 @@ export function truncateText(text, maxLength = 100, suffix = '...') {
296
181
  return str.substring(0, maxLength) + suffix;
297
182
  }
298
183
 
299
- /**
300
- * Convert a Unix-seconds timestamp to a UTC ISO-8601 string.
301
- * @param {number} timestamp - Unix seconds
302
- * @returns {string|null}
303
- */
304
184
  export function toUtcIso(timestamp) {
305
185
  if (!timestamp) return null;
306
186
  const date = new Date(timestamp * 1000);
@@ -308,11 +188,7 @@ export function toUtcIso(timestamp) {
308
188
  return date.toISOString();
309
189
  }
310
190
 
311
- /**
312
- * Normalize a Firestore timestamp (or number/date) to Unix seconds.
313
- * @param {*} ts
314
- * @returns {number|null}
315
- */
191
+ // Firestore Timestamp objects expose ._seconds (newer SDK) or .seconds (older); plain numbers and date strings are also accepted.
316
192
  export function fromFirestoreTimestamp(ts) {
317
193
  if (!ts) return null;
318
194
  if (ts._seconds !== undefined) return ts._seconds;
@@ -1,8 +1,3 @@
1
- /**
2
- * Debug Registry - Comprehensive globalThis.__debug__ registry for all Thatcher subsystems
3
- * Modules register on import/init; supports nested namespaces and live data
4
- */
5
-
6
1
  import { createLogger } from './logger.js';
7
2
 
8
3
  const logger = createLogger('[DebugRegistry]');
@@ -1,5 +1,8 @@
1
+ import { createLogger } from './logger.js';
1
2
  import { getConfigEngineSync } from '@/lib/config-generator-engine';
2
3
 
4
+ const log = createLogger('[DomainLoader]');
5
+
3
6
  const requireStr = (val, label) => { if (!val || typeof val !== 'string') throw new Error(`[DomainLoader] ${label} must be a non-empty string`); };
4
7
 
5
8
  export class DomainLoader {
@@ -22,7 +25,7 @@ export class DomainLoader {
22
25
 
23
26
  getSpecsForDomain(domain) {
24
27
  return this.getEntitiesForDomain(domain).reduce((acc, name) => {
25
- try { acc.push(this.engine.generateEntitySpec(name)); } catch (e) { console.error(`[DomainLoader] spec failed for ${name}:`, e.message); }
28
+ try { acc.push(this.engine.generateEntitySpec(name)); } catch (e) { log.error(`spec failed for ${name}:`, { message: e.message }); }
26
29
  return acc;
27
30
  }, []);
28
31
  }
@@ -1,4 +1,8 @@
1
- import { now } from '@/lib/database-core';
1
+ import { createLogger } from './logger.js';
2
+ import { now, genId } from '@/lib/id-helpers';
3
+
4
+ const log = createLogger('[Email]');
5
+ import { list, update, create } from '@/lib/busybase-store';
2
6
  import { sendEmail } from '@/adapters/google-gmail';
3
7
  import { EMAIL_STATUS } from '@/config/constants';
4
8
  import { config } from '@/config/env';
@@ -31,33 +35,33 @@ export function parseAttachments(attachmentsJson) {
31
35
  } catch { return []; }
32
36
  }
33
37
 
34
- export function logEmailActivity(db, emailId, action, metadata = {}) {
38
+ export async function logEmailActivity(emailId, action, metadata = {}) {
35
39
  try {
36
- db.prepare(`INSERT INTO activity_log (id, entity_type, entity_id, action, metadata, created_at) VALUES (?, ?, ?, ?, ?, ?)`)
37
- .run(crypto.randomUUID?.() || `${Date.now()}_${Math.random()}`, 'email', emailId, action, JSON.stringify(metadata), now());
38
- } catch (e) { console.error('[EMAIL] Failed to log activity:', e.message); }
40
+ await create('activity_log', { id: genId(), entity_type: 'email', entity_id: emailId, action, metadata: JSON.stringify(metadata), created_at: now() });
41
+ } catch (e) { log.error('failed to log activity:', { message: e.message }); }
39
42
  }
40
43
 
41
- export function checkFailureRate(db) {
44
+ export async function checkFailureRate() {
42
45
  try {
43
- const stats = db.prepare(`SELECT COUNT(*) as total, SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as failed FROM email WHERE created_at >= ?`)
44
- .get(EMAIL_STATUS.FAILED, now() - 86400);
45
- if (stats.total > 0 && stats.failed / stats.total > 0.5 && stats.total > 10)
46
- console.warn('[EMAIL] HIGH FAILURE RATE ALERT:', { failureRate: `${((stats.failed / stats.total) * 100).toFixed(1)}%`, failed: stats.failed, total: stats.total });
47
- } catch (e) { console.error('[EMAIL] Failed to check failure rate:', e.message); }
46
+ const cutoff = now() - 86400;
47
+ const recent = (await list('email', {})).filter(e => (e.created_at || 0) >= cutoff);
48
+ const total = recent.length;
49
+ const failed = recent.filter(e => e.status === EMAIL_STATUS.FAILED).length;
50
+ if (total > 10 && failed / total > 0.5)
51
+ log.warn('HIGH FAILURE RATE ALERT:', { failureRate: `${((failed / total) * 100).toFixed(1)}%`, failed, total });
52
+ } catch (e) { log.error('failed to check failure rate:', { message: e.message }); }
48
53
  }
49
54
 
50
55
  async function exponentialBackoff(attempt, maxDelayMs) {
51
56
  await new Promise(r => setTimeout(r, Math.min(1000 * Math.pow(2, attempt), maxDelayMs)));
52
57
  }
53
58
 
54
- export async function sendSingleEmail(db, emailRecord, attempt = 1, maxRetries = 3, maxDelayMs = 30000) {
59
+ export async function sendSingleEmail(emailRecord, attempt = 1, maxRetries = 3, maxDelayMs = 30000) {
55
60
  const validationErrors = validateEmailData(emailRecord);
56
61
  if (validationErrors.length > 0) {
57
62
  const errorMsg = validationErrors.join('; ');
58
- db.prepare(`UPDATE email SET status=?, processing_error=?, retry_count=?, updated_at=? WHERE id=?`)
59
- .run(EMAIL_STATUS.FAILED, errorMsg, attempt, now(), emailRecord.id);
60
- logEmailActivity(db, emailRecord.id, 'email_send_failed', { error: errorMsg, attempt });
63
+ await update('email', emailRecord.id, { status: EMAIL_STATUS.FAILED, processing_error: errorMsg, retry_count: attempt, updated_at: now() });
64
+ await logEmailActivity(emailRecord.id, 'email_send_failed', { error: errorMsg, attempt });
61
65
  return { success: false, error: errorMsg, emailId: emailRecord.id };
62
66
  }
63
67
 
@@ -76,9 +80,8 @@ export async function sendSingleEmail(db, emailRecord, attempt = 1, maxRetries =
76
80
  };
77
81
 
78
82
  const result = await sendEmail(emailData);
79
- db.prepare(`UPDATE email SET status=?, processed=?, message_id=?, processing_error=NULL, retry_count=?, processed_at=?, updated_at=? WHERE id=?`)
80
- .run(EMAIL_STATUS.PROCESSED, true, result.id || result.messageId, attempt, now(), now(), emailRecord.id);
81
- logEmailActivity(db, emailRecord.id, 'email_sent', { messageId: result.id || result.messageId, to: emailData.to, attempt });
83
+ await update('email', emailRecord.id, { status: EMAIL_STATUS.PROCESSED, processed: true, message_id: result.id || result.messageId, processing_error: '', retry_count: attempt, processed_at: now(), updated_at: now() });
84
+ await logEmailActivity(emailRecord.id, 'email_sent', { messageId: result.id || result.messageId, to: emailData.to, attempt });
82
85
  return { success: true, messageId: result.id || result.messageId, emailId: emailRecord.id };
83
86
  } catch (error) {
84
87
  const isRateLimit = error.message?.includes('429') || /quota|rate limit/i.test(error.message);
@@ -87,14 +90,13 @@ export async function sendSingleEmail(db, emailRecord, attempt = 1, maxRetries =
87
90
 
88
91
  if (isPermanent || attempt >= maxRetries) {
89
92
  const bounceStatus = isBounce ? 'bounced' : EMAIL_STATUS.FAILED;
90
- db.prepare(`UPDATE email SET status=?, processing_error=?, retry_count=?, bounce_reason=?, bounced_at=?, bounce_permanent=?, updated_at=? WHERE id=?`)
91
- .run(bounceStatus, error.message, attempt, isBounce ? error.message : null, isBounce ? now() : null, isBounce ? 1 : 0, now(), emailRecord.id);
92
- logEmailActivity(db, emailRecord.id, 'email_send_failed', { error: error.message, attempt, permanent: isPermanent });
93
+ await update('email', emailRecord.id, { status: bounceStatus, processing_error: error.message, retry_count: attempt, bounce_reason: isBounce ? error.message : '', bounced_at: isBounce ? now() : 0, bounce_permanent: isBounce ? 1 : 0, updated_at: now() });
94
+ await logEmailActivity(emailRecord.id, 'email_send_failed', { error: error.message, attempt, permanent: isPermanent });
93
95
  return { success: false, error: error.message, emailId: emailRecord.id, permanent: isPermanent };
94
96
  }
95
97
 
96
98
  if (isRateLimit) await exponentialBackoff(attempt, maxDelayMs);
97
- db.prepare(`UPDATE email SET retry_count=?, processing_error=?, updated_at=? WHERE id=?`).run(attempt, error.message, now(), emailRecord.id);
98
- return sendSingleEmail(db, emailRecord, attempt + 1, maxRetries, maxDelayMs);
99
+ await update('email', emailRecord.id, { retry_count: attempt, processing_error: error.message, updated_at: now() });
100
+ return sendSingleEmail(emailRecord, attempt + 1, maxRetries, maxDelayMs);
99
101
  }
100
102
  }
@@ -1,6 +1,4 @@
1
- /**
2
- * Error Handling - Centralized error types and utilities
3
- */
1
+ import { createLogger } from './logger.js';
4
2
 
5
3
  // AppError stays a real class (base error type; `instanceof AppError` works).
6
4
  export class AppError extends Error {
@@ -44,40 +42,12 @@ export function BadRequestError(message = 'Invalid request') {
44
42
  const e = new AppError(message, 'BAD_REQUEST', 400); e.name = 'BadRequestError'; return e;
45
43
  }
46
44
 
47
- /**
48
- * Create an error logger
49
- * @param {string} context
50
- * @returns {object}
51
- */
52
45
  export function createErrorLogger(context = '') {
53
- return {
54
- error: (msg, meta = {}) => {
55
- console.error(`[${context}] ${msg}`, meta);
56
- },
57
- warn: (msg, meta = {}) => {
58
- console.warn(`[${context}] ${msg}`, meta);
59
- },
60
- info: (_msg, _meta = {}) => {},
61
- debug: (msg, meta = {}) => {
62
- if (process.env.DEBUG) {
63
- console.debug(`[${context}] Debug:`, msg, meta);
64
- }
65
- },
66
- };
46
+ const log = createLogger(`[${context}]`);
47
+ return { error: log.error, warn: log.warn, info: (_msg, _meta = {}) => {}, debug: log.debug };
67
48
  }
68
49
 
69
- /**
70
- * Normalize an arbitrary thrown value into one of thatcher's AppError-class
71
- * instances. AppError instances (and anything already shaped like one — a
72
- * numeric `status` + a `code`) pass through untouched; common low-level
73
- * errors are mapped to the appropriate thatcher error class.
74
- *
75
- * (Ported from moonlanding, adapted to thatcher's class-based hierarchy and
76
- * its `status`/`code`/`details` shape. Kept import-free so the module stays
77
- * loadable under the plain-node test runner.)
78
- * @param {unknown} error
79
- * @returns {AppError}
80
- */
50
+ // Ported from moonlanding; kept import-free so this module loads under the plain-node test runner.
81
51
  export function normalizeError(error) {
82
52
  if (error instanceof AppError) {
83
53
  return error;
@@ -112,13 +82,7 @@ export function normalizeError(error) {
112
82
  });
113
83
  }
114
84
 
115
- /**
116
- * Build a JSON-serializable error response body from any thrown value.
117
- * (Ported from moonlanding, adapted to thatcher's error shape.)
118
- * @param {unknown} error
119
- * @param {boolean} [includeStack=false]
120
- * @returns {object}
121
- */
85
+ // Ported from moonlanding.
122
86
  export function formatErrorResponse(error, includeStack = false) {
123
87
  const normalized = normalizeError(error);
124
88
  const response = {