thatcher 1.0.6 → 1.0.33

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 +29 -88
  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 +14 -5
  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,6 +1,8 @@
1
- import { list, get } from '@/lib/query-engine.js';
1
+ import { list, get } from '@/lib/busybase-store.js';
2
+ import { createLogger } from '@/lib/logger.js';
2
3
  import { getSpec } from '@/config/spec-helpers.js';
3
- import { getDatabase } from '@/lib/database-core.js';
4
+
5
+ const log = createLogger('[PageHandlerReviews]');
4
6
  import { renderEntityList, renderAccessDenied } from '@/ui/renderer.js';
5
7
  import { renderSectionReport, renderReviewListTabbed } from '@/ui/review-renderer.js';
6
8
  import { renderChecklistDetails, renderChecklistsHome } from '@/ui/checklist-renderer.js';
@@ -21,14 +23,14 @@ async function lazyRenderer(name) {
21
23
  return import(`file://${__dirname_rv}${name}?t=${t}`);
22
24
  }
23
25
 
24
- export function handleFilteredReviewList(user, filter) {
26
+ export async function handleFilteredReviewList(user, filter) {
25
27
  if (!canList(user, 'review')) return renderAccessDenied(user, 'review', 'list');
26
28
  const spec = getSpec('review'); if (!spec) return null;
27
- let items = list('review', {});
29
+ const where = filter === 'archive' ? { status: 'archived' } : {};
30
+ let items = await list('review', where);
28
31
  if (filter === 'active') items = items.filter(r => r.status === 'active' || r.status === 'open');
29
32
  else if (filter === 'priority') { const pids = user.priority_reviews || []; items = items.filter(r => pids.includes(r.id)); }
30
33
  else if (filter === 'history') items = items.filter(r => r.status === 'closed' || r.status === 'completed');
31
- else if (filter === 'archive') items = items.filter(r => r.status === 'archived');
32
34
  return renderEntityList('review', resolveRefFields(items, spec), spec, user);
33
35
  }
34
36
 
@@ -40,25 +42,27 @@ export async function handleReviewRoutes(normalized, segments, user, req) {
40
42
 
41
43
  if (normalized === '/reviews' || normalized === '/review') {
42
44
  if (!canList(user, 'review')) return renderAccessDenied(user, 'review', 'list');
43
- let reviews = []; try { reviews = getDatabase().prepare('SELECT * FROM review ORDER BY updated_at DESC').all(); } catch { try { reviews = list('review', {}); } catch {} }
44
- const engMap = {}; try { list('engagement', {}).forEach(e => { engMap[e.id] = e.name; }); } catch {}
45
+ let reviews = []; try { reviews = await list('review', {}, { sort: { field: 'updated_at', dir: 'DESC' } }); } catch {}
46
+ const engMap = {}; try { (await list('engagement', {})).forEach(e => { engMap[e.id] = e.name; }); } catch {}
45
47
  return renderReviewListTabbed(user, reviews.map(r => ({ ...r, engagement_name: engMap[r.engagement_id] || r.engagement_name || null })));
46
48
  }
47
49
 
48
50
  if (segments[0] === 'review' && segments.length === 3 && segments[2] === 'sections') {
49
51
  const reviewId = segments[1];
50
52
  if (!canView(user, 'review')) return renderAccessDenied(user, 'review', 'view');
51
- const review = get('review', reviewId) || getDatabase().prepare('SELECT * FROM review WHERE id=?').get(reviewId);
53
+ const review = await get('review', reviewId);
52
54
  if (!review) return null;
53
55
  let sections = [];
54
- try { sections = list('review_section', {}).filter(s => s.review_id === reviewId); } catch (e) { console.error('Failed to fetch sections', e); }
56
+ try { sections = (await list('review_section', { review_id: reviewId })); } catch (e) { log.error('Failed to fetch sections', { reviewId, message: e?.message || String(e) }); }
55
57
  return renderSectionReport(user, review, sections);
56
58
  }
57
59
 
58
60
  if (normalized === '/reviews/analytics' || normalized === '/review/analytics') {
59
61
  if (!canList(user, 'review')) return renderAccessDenied(user, 'review', 'list');
60
62
  let reviews = [], highlights = [], activity = [];
61
- try { reviews = list('review', {}); } catch {} try { highlights = list('highlight', {}); } catch {} try { activity = list('activity_log', {}).slice(0, 50); } catch {}
63
+ try { reviews = await list('review', {}); } catch {}
64
+ try { highlights = await list('highlight', {}); } catch {}
65
+ try { activity = (await list('activity_log', {})).slice(0, 50); } catch {}
62
66
  return renderReviewAnalytics(user, { reviews, highlights, recentActivity: activity });
63
67
  }
64
68
 
@@ -67,40 +71,41 @@ export async function handleReviewRoutes(normalized, segments, user, req) {
67
71
  const url = new URL(req.url, `http://${req.headers.host||'localhost'}`);
68
72
  const leftId = url.searchParams.get('left'), rightId = url.searchParams.get('right');
69
73
  if (leftId && rightId) {
70
- const leftReview = get('review', leftId), rightReview = get('review', rightId);
74
+ const leftReview = await get('review', leftId), rightReview = await get('review', rightId);
71
75
  if (!leftReview || !rightReview) return null;
72
76
  let leftH = [], rightH = [];
73
- try { leftH = list('highlight', {}).filter(h => h.review_id === leftId); } catch {} try { rightH = list('highlight', {}).filter(h => h.review_id === rightId); } catch {}
77
+ try { const all = await list('highlight', {}); leftH = all.filter(h => h.review_id === leftId); rightH = all.filter(h => h.review_id === rightId); } catch {}
74
78
  return renderReviewComparison(user, leftReview, rightReview, leftH, rightH);
75
79
  }
76
- let reviews = []; try { reviews = list('review', {}); } catch {}
80
+ let reviews = []; try { reviews = await list('review', {}); } catch {}
77
81
  return renderComparisonPicker(user, reviews);
78
82
  }
79
83
 
80
84
  if (normalized === '/reviews/tenders' || normalized === '/review/tenders') {
81
85
  if (!canList(user, 'review')) return renderAccessDenied(user, 'review', 'list');
82
86
  let tenders = [], reviews = [];
83
- try { tenders = list('tender', {}); } catch {} try { reviews = list('review', {}); } catch {}
87
+ try { tenders = await list('tender', {}); } catch {}
88
+ try { reviews = await list('review', {}); } catch {}
84
89
  return renderTenderDashboard(user, tenders.map(t => { const r = reviews.find(rev => rev.id === t.review_id); return { ...t, review_name: r?.name || r?.title || '' }; }), reviews);
85
90
  }
86
91
 
87
92
  if (normalized === '/reviews/batch' || normalized === '/review/batch') {
88
93
  if (!canEdit(user, 'review')) return renderAccessDenied(user, 'review', 'edit');
89
- let reviews = []; try { reviews = list('review', {}); } catch {}
94
+ let reviews = []; try { reviews = await list('review', {}); } catch {}
90
95
  const spec = getSpec('review'); if (spec) reviews = resolveRefFields(reviews, spec);
91
96
  return renderBatchOperations(user, reviews);
92
97
  }
93
98
 
94
99
  if (segments[0] === 'review' && segments.length === 3) {
95
100
  const reviewId = segments[1], action = segments[2];
96
- const getReview = (id) => get('review', id) || getDatabase().prepare('SELECT * FROM review WHERE id=?').get(id);
101
+ const getReview = (id) => get('review', id);
97
102
  if (action === 'pdf') {
98
103
  if (!canView(user, 'review')) return renderAccessDenied(user, 'review', 'view');
99
- const review = getReview(reviewId); if (!review) return null;
104
+ const review = await getReview(reviewId); if (!review) return null;
100
105
  let h = [], s = [];
101
- try { h = list('highlight', {}).filter(x => x.review_id === reviewId); } catch {}
102
- try { s = list('review_section', {}).filter(x => x.review_id === reviewId); } catch {}
103
- // Flatten bounding_rect JSON → x/y/width/height for the overlay renderer.
106
+ try { h = (await list('highlight', {})).filter(x => x.review_id === reviewId); } catch {}
107
+ try { s = (await list('review_section', {})).filter(x => x.review_id === reviewId); } catch {}
108
+ // Flatten bounding_rect JSON -> x/y/width/height for the overlay renderer.
104
109
  h = h.map((x) => {
105
110
  let br = null; try { br = typeof x.bounding_rect === 'string' ? JSON.parse(x.bounding_rect) : x.bounding_rect; } catch {}
106
111
  let rects = []; try { rects = typeof x.rects === 'string' ? JSON.parse(x.rects) : (x.rects || []); } catch {}
@@ -116,16 +121,31 @@ export async function handleReviewRoutes(normalized, segments, user, req) {
116
121
  });
117
122
  return renderPdfViewer(user, review, h, s);
118
123
  }
119
- if (action === 'editor') { if (!canEdit(user, 'review')) return renderAccessDenied(user, 'review', 'edit'); const review = getReview(reviewId); if (!review) return null; return renderPdfEditorPlaceholder(user, review); }
120
- if (action === 'highlights') { if (!canView(user, 'review')) return renderAccessDenied(user, 'review', 'view'); const review = getReview(reviewId); if (!review) return null; let h = []; try { h = list('highlight', {}).filter(x => x.review_id === reviewId); } catch {} const rm = {}; for (const x of h) { try { rm[x.id] = list('highlight_response', {}).filter(r => r.highlight_id === x.id); } catch { rm[x.id] = []; } } return renderHighlightThreading(user, review, h, rm); }
121
- if (action === 'resolution') { if (!canView(user, 'review')) return renderAccessDenied(user, 'review', 'view'); const review = getReview(reviewId); if (!review) return null; let s = []; try { s = list('review_section', {}).filter(x => x.review_id === reviewId); } catch {} const hbs = {}; for (const sec of s) { try { hbs[sec.id] = list('highlight', {}).filter(h => h.review_id === reviewId && h.section_id === sec.id); } catch { hbs[sec.id] = []; } } return renderSectionResolution(user, review, s, hbs); }
124
+ if (action === 'editor') { if (!canEdit(user, 'review')) return renderAccessDenied(user, 'review', 'edit'); const review = await getReview(reviewId); if (!review) return null; return renderPdfEditorPlaceholder(user, review); }
125
+ if (action === 'highlights') {
126
+ if (!canView(user, 'review')) return renderAccessDenied(user, 'review', 'view');
127
+ const review = await getReview(reviewId); if (!review) return null;
128
+ let h = []; try { h = (await list('highlight', {})).filter(x => x.review_id === reviewId); } catch {}
129
+ const rm = {}; const allResp = await list('highlight_response', {}).catch(() => []);
130
+ for (const x of h) rm[x.id] = allResp.filter(r => r.highlight_id === x.id);
131
+ return renderHighlightThreading(user, review, h, rm);
132
+ }
133
+ if (action === 'resolution') {
134
+ if (!canView(user, 'review')) return renderAccessDenied(user, 'review', 'view');
135
+ const review = await getReview(reviewId); if (!review) return null;
136
+ let s = []; try { s = (await list('review_section', {})).filter(x => x.review_id === reviewId); } catch {}
137
+ const allH = await list('highlight', {}).catch(() => []);
138
+ const hbs = {};
139
+ for (const sec of s) hbs[sec.id] = allH.filter(h => h.review_id === reviewId && h.section_id === sec.id);
140
+ return renderSectionResolution(user, review, s, hbs);
141
+ }
122
142
  if (action === 'edit') {
123
143
  if (!canEdit(user, 'review')) return renderAccessDenied(user, 'review', 'edit');
124
- const review = get('review', reviewId) || getDatabase().prepare('SELECT * FROM review WHERE id=?').get(reviewId);
144
+ const review = await get('review', reviewId);
125
145
  if (!review) return null;
126
146
  let engagements = [], teams = [];
127
- try { engagements = list('engagement', {}); } catch {}
128
- try { teams = list('team', {}); } catch {}
147
+ try { engagements = await list('engagement', {}); } catch {}
148
+ try { teams = await list('team', {}); } catch {}
129
149
  const { renderReviewEdit } = await lazyRenderer('review-edit-renderer.js');
130
150
  return renderReviewEdit(user, review, engagements, teams);
131
151
  }
@@ -135,13 +155,19 @@ export async function handleReviewRoutes(normalized, segments, user, req) {
135
155
  if (segments.length === 2 && segments[0] === 'review' && segments[1] !== 'new') {
136
156
  const reviewId = segments[1];
137
157
  if (!canView(user, 'review')) return renderAccessDenied(user, 'review', 'view');
138
- const review = get('review', reviewId) || getDatabase().prepare('SELECT * FROM review WHERE id=?').get(reviewId); if (!review) return null;
158
+ const review = await get('review', reviewId); if (!review) return null;
139
159
  let highlights = [], collaborators = [], checklists = [], sections = [], tenders = [];
140
- try { highlights = list('highlight', {}).filter(h => h.review_id === reviewId); } catch {}
141
- try { collaborators = list('collaborator', {}).filter(c => c.review_id === reviewId); } catch {}
142
- try { checklists = list('checklist', {}).filter(c => c.review_id === reviewId).map(c => { let items = []; try { items = list('checklist_item', {}).filter(i => i.checklist_id === c.id); } catch {} return { ...c, total_items: items.length, completed_items: items.filter(i => i.completed).length }; }); } catch {}
143
- try { sections = list('review_section', {}).filter(s => s.review_id === reviewId); } catch {}
144
- try { tenders = list('tender', {}).filter(t => t.review_id === reviewId); } catch {}
160
+ try { highlights = (await list('highlight', {})).filter(h => h.review_id === reviewId); } catch {}
161
+ try { collaborators = (await list('collaborator', {})).filter(c => c.review_id === reviewId); } catch {}
162
+ try {
163
+ const allItems = await list('checklist_item', {});
164
+ checklists = (await list('checklist', {})).filter(c => c.review_id === reviewId).map(c => {
165
+ const items = allItems.filter(i => i.checklist_id === c.id);
166
+ return { ...c, total_items: items.length, completed_items: items.filter(i => i.completed).length };
167
+ });
168
+ } catch {}
169
+ try { sections = (await list('review_section', {})).filter(s => s.review_id === reviewId); } catch {}
170
+ try { tenders = (await list('tender', {})).filter(t => t.review_id === reviewId); } catch {}
145
171
  const { renderReviewDetail } = await lazyRenderer('review-detail-renderer.js');
146
172
  return renderReviewDetail(user, review, highlights, collaborators, checklists, sections, tenders);
147
173
  }
@@ -149,15 +175,25 @@ export async function handleReviewRoutes(normalized, segments, user, req) {
149
175
  if (segments[0] === 'checklist' && segments.length === 2 && segments[1] !== 'new') {
150
176
  const checklistId = segments[1];
151
177
  if (!canView(user, 'checklist')) return renderAccessDenied(user, 'checklist', 'view');
152
- const checklist = get('checklist', checklistId); if (!checklist) return null;
153
- let items = []; try { items = getDatabase().prepare('SELECT * FROM checklist_item WHERE checklist_id=? ORDER BY "order" ASC, created_at ASC').all(checklistId); } catch {}
178
+ const checklist = await get('checklist', checklistId); if (!checklist) return null;
179
+ let items = [];
180
+ try { items = (await list('checklist_item', { checklist_id: checklistId })).sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || (a.created_at ?? 0) - (b.created_at ?? 0)); } catch {}
154
181
  return renderChecklistDetails(user, checklist, items);
155
182
  }
156
183
 
157
184
  if (normalized === '/checklists' || normalized === '/checklist') {
158
185
  if (!canList(user, 'checklist')) return renderAccessDenied(user, 'checklist', 'list');
159
186
  let checklists = [];
160
- try { const db = getDatabase(); const stats = db.prepare('SELECT checklist_id, COUNT(*) as total, SUM(CASE WHEN is_done=1 THEN 1 ELSE 0 END) as done FROM checklist_item GROUP BY checklist_id').all(); const statsMap = Object.fromEntries(stats.map(s => [s.checklist_id, s])); checklists = list('checklist', {}).map(c => ({ ...c, total_items: statsMap[c.id]?.total || 0, completed_items: statsMap[c.id]?.done || 0 })); } catch {}
187
+ try {
188
+ // Old GROUP BY checklist_id (total + done) -> client-side aggregate.
189
+ const allItems = await list('checklist_item', {});
190
+ const statsMap = {};
191
+ for (const it of allItems) {
192
+ const s = statsMap[it.checklist_id] || (statsMap[it.checklist_id] = { total: 0, done: 0 });
193
+ s.total++; if (it.is_done === 1 || it.is_done === true || it.completed) s.done++;
194
+ }
195
+ checklists = (await list('checklist', {})).map(c => ({ ...c, total_items: statsMap[c.id]?.total || 0, completed_items: statsMap[c.id]?.done || 0 }));
196
+ } catch {}
161
197
  return renderChecklistsHome(user, checklists);
162
198
  }
163
199
 
@@ -1,5 +1,4 @@
1
- import { list, get } from '@/lib/query-engine.js';
2
- import { getDatabase } from '@/lib/database-core.js';
1
+ import { list, get } from '@/lib/busybase-store.js';
3
2
  import { canView, canList, isClerk, isPartner } from '@/ui/permissions-ui.js';
4
3
  import { renderAccessDenied } from '@/ui/renderer.js';
5
4
  import { renderRfiList } from '@/ui/rfi-list-renderer.js';
@@ -9,31 +8,46 @@ const lazyR = (name) => import(`file://${__dirname_rfi}${name}?t=${globalThis.__
9
8
 
10
9
  export async function handleRfiDetail(user, rfiId) {
11
10
  if (!canView(user, 'rfi')) return renderAccessDenied(user, 'rfi', 'view');
12
- const db = getDatabase();
13
- const rfi = db.prepare('SELECT * FROM rfis WHERE id=?').get(rfiId) || get('rfi', rfiId); if (!rfi) return null;
14
- let questions = []; try { questions = db.prepare('SELECT q.*, (SELECT COUNT(*) FROM rfi_responses r WHERE r.question_id=q.id) as response_count FROM rfi_questions q WHERE q.rfi_id=?').all(rfiId); } catch { try { questions = list('rfi_question', {}).filter(q => q.rfi_id === rfiId); } catch {} }
15
- let sections = []; try { sections = list('rfi_section', {}).filter(s => s.rfi_id === rfiId || s.engagement_id === rfi.engagement_id); } catch {}
16
- let engagement = null; try { if (rfi.engagement_id) engagement = get('engagement', rfi.engagement_id); } catch {}
11
+ const rfi = await get('rfi', rfiId); if (!rfi) return null;
12
+ let questions = [];
13
+ try {
14
+ questions = (await list('rfi_question', { rfi_id: rfiId }));
15
+ // Old correlated subquery (response_count per question) -> client-side aggregate.
16
+ const responses = await list('rfi_response', {});
17
+ const counts = {};
18
+ for (const r of responses) counts[r.question_id] = (counts[r.question_id] || 0) + 1;
19
+ questions = questions.map(q => ({ ...q, response_count: counts[q.id] || 0 }));
20
+ } catch {}
21
+ let sections = []; try {
22
+ const [bySectionId, byEngId] = await Promise.all([
23
+ list('rfi_section', { rfi_id: rfiId }),
24
+ list('rfi_section', { engagement_id: rfi.engagement_id }),
25
+ ]);
26
+ const seen = new Set(); sections = [...bySectionId, ...byEngId].filter(s => seen.has(s.id) ? false : seen.add(s.id));
27
+ } catch {}
28
+ let engagement = null; try { if (rfi.engagement_id) engagement = await get('engagement', rfi.engagement_id); } catch {}
17
29
  const { renderRfiDetail } = await lazyR('rfi-detail-renderer.js');
18
30
  return renderRfiDetail(user, rfi, questions, sections, engagement);
19
31
  }
20
32
 
21
33
  export async function handleRfiList(user) {
22
34
  if (!canList(user, 'rfi')) return renderAccessDenied(user, 'rfi', 'list');
23
- let rfis = []; try { rfis = getDatabase().prepare('SELECT * FROM rfis ORDER BY created_at ASC').all(); } catch {}
24
- let engagements = []; try { engagements = list('engagement', {}); } catch {}
35
+ let rfis = []; try { rfis = await list('rfi', {}, { sort: { field: 'created_at', dir: 'ASC' } }); } catch {}
36
+ let engagements = []; try { engagements = await list('engagement', {}); } catch {}
25
37
  if (isClerk(user)) rfis = rfis.filter(r => engagements.filter(e => e.assigned_to === user.id || e.team_id === user.team_id).some(e => e.id === r.engagement_id));
26
38
  return renderRfiList(user, rfis.map((r, i) => ({ ...r, display_name: 'RFI #' + (i + 1) })), engagements);
27
39
  }
28
40
 
29
41
  export async function handleRfiReport(user, rfiId, res) {
30
- const db = getDatabase();
31
42
  let rfi = null, questions = [], responses = [], engagement = null;
32
- try { rfi = get('rfi', rfiId) || db.prepare('SELECT * FROM rfis WHERE id=?').get(rfiId); } catch {}
43
+ try { rfi = await get('rfi', rfiId); } catch {}
33
44
  if (!rfi) return null;
34
- try { questions = db.prepare('SELECT * FROM rfi_questions WHERE rfi_id=? ORDER BY created_at ASC').all(rfiId); } catch {}
35
- try { const qIds = questions.map(q => q.id); if (qIds.length) { const ph = qIds.map(() => '?').join(','); responses = db.prepare(`SELECT * FROM rfi_responses WHERE question_id IN (${ph})`).all(...qIds); } } catch {}
36
- try { engagement = rfi.engagement_id ? (get('engagement', rfi.engagement_id) || db.prepare('SELECT * FROM engagement WHERE id=?').get(rfi.engagement_id)) : null; } catch {}
45
+ try { questions = await list('rfi_question', { rfi_id: rfiId }, { sort: { field: 'created_at', dir: 'ASC' } }); } catch {}
46
+ try {
47
+ const qIds = new Set(questions.map(q => q.id));
48
+ if (qIds.size) responses = (await list('rfi_response', {})).filter(r => qIds.has(r.question_id));
49
+ } catch {}
50
+ try { engagement = rfi.engagement_id ? await get('engagement', rfi.engagement_id) : null; } catch {}
37
51
  const { renderRfiReport } = await lazyR('rfi-report-renderer.js');
38
52
  const html = renderRfiReport(user, rfi, engagement, questions, responses);
39
53
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
@@ -1,8 +1,7 @@
1
1
  import { getUser, setCurrentRequest } from '@/engine.server.js';
2
2
  import { hasGoogleAuth } from '@/config/env.js';
3
3
  import { getSpec } from '@/config/spec-helpers.js';
4
- import { list, get } from '@/lib/query-engine.js';
5
- import { getDatabase } from '@/lib/database-core.js';
4
+ import { list, get } from '@/lib/busybase-store.js';
6
5
  import { renderLogin, renderDashboard, renderEntityList, renderEntityDetail, renderEntityForm, renderAccessDenied, renderPasswordReset, renderPasswordResetConfirm, REDIRECT } from '@/ui/renderer.js';
7
6
  import { renderClientDashboard, renderClientList } from '@/ui/client-renderer.js';
8
7
  import { canList, canView, canCreate, canEdit, isPartner, isClerk, isClientUser, canClientAccessEntity } from '@/ui/permissions-ui.js';
@@ -25,25 +24,32 @@ function reqUrl(req) {
25
24
 
26
25
  async function handleEngagementDetail(user, engId) {
27
26
  if (!canView(user, 'engagement')) return renderAccessDenied(user, 'engagement', 'view');
28
- const db = getDatabase();
29
- const engagement = get('engagement', engId) || db.prepare('SELECT * FROM engagement WHERE id=?').get(engId);
27
+ const engagement = await get('engagement', engId);
30
28
  if (!engagement) return null;
31
- let client = null; try { client = engagement.client_id ? get('client', engagement.client_id) : null; } catch {}
32
- let rfis = []; try { rfis = db.prepare('SELECT * FROM rfis WHERE engagement_id=?').all(engId); } catch { try { rfis = list('rfi', {}).filter(r => r.engagement_id === engId); } catch {} }
33
- let sections = []; try { sections = db.prepare('SELECT * FROM rfi_section WHERE engagement_id=? ORDER BY sort_order ASC').all(engId); } catch {}
34
- let team = null; try { team = engagement.team_id ? get('team', engagement.team_id) : null; } catch {}
29
+ let client = null; try { client = engagement.client_id ? await get('client', engagement.client_id) : null; } catch {}
30
+ let rfis = []; try { rfis = await list('rfi', { engagement_id: engId }); } catch {}
31
+ let sections = [];
32
+ try {
33
+ sections = await list('rfi_section', { engagement_id: engId }, { sort: { field: 'sort_order', dir: 'ASC' } });
34
+ } catch {}
35
+ let team = null; try { team = engagement.team_id ? await get('team', engagement.team_id) : null; } catch {}
35
36
  let assignedUsers = [];
36
- try { const ids = JSON.parse(engagement.users || '[]'); const um = Object.fromEntries(db.prepare('SELECT id, name, email FROM users').all().map(u => [u.id, u.name || u.email])); assignedUsers = ids.map(id => ({ id, name: um[id] || null })).filter(u => u.name); } catch {}
37
+ try {
38
+ const ids = JSON.parse(engagement.users || '[]');
39
+ const users = await list('user', {});
40
+ const um = Object.fromEntries(users.map(u => [u.id, u.name || u.email]));
41
+ assignedUsers = ids.map(id => ({ id, name: um[id] || null })).filter(u => u.name);
42
+ } catch {}
37
43
  const { renderEngagementDetail } = await lazyRenderer('engagement-detail-renderer.js');
38
44
  return renderEngagementDetail(user, { ...engagement, team_name: team?.name || engagement.team_name, client_name: client?.name || engagement.client_name, assigned_users_resolved: assignedUsers }, client, rfis, sections);
39
45
  }
40
46
  async function handleEngagementList(user, req) {
41
47
  if (!canList(user, 'engagement')) return renderAccessDenied(user, 'engagement', 'list');
42
- let engagements = list('engagement', {});
43
- const clientMap = Object.fromEntries(list('client', {}).map(c => [c.id, c.name]));
48
+ let engagements = await list('engagement', {});
49
+ const clientMap = Object.fromEntries((await list('client', {})).map(c => [c.id, c.name]));
44
50
  engagements = engagements.map(e => ({ ...e, client_name: clientMap[e.client_id] || e.client_name || '-' }));
45
51
  const spec = getSpec('engagement'); if (spec) engagements = resolveRefFields(engagements, spec);
46
- let teams = []; try { teams = list('team', {}); } catch {}
52
+ let teams = []; try { teams = await list('team', {}); } catch {}
47
53
  const teamMap = Object.fromEntries(teams.map(t => [t.id, t.name]));
48
54
  engagements = engagements.map(e => ({ ...e, team_name: teamMap[e.team_id] || e.team_name || '-' }));
49
55
  const years = [...new Set(engagements.map(e => { if (!e.year) return null; const m = String(e.year).match(/\b(20\d{2}|19\d{2})\b/); return m ? m[1] : null; }).filter(Boolean))].sort().reverse();
@@ -53,10 +59,10 @@ async function handleEngagementList(user, req) {
53
59
  async function handleSearch(user, req) {
54
60
  const url = reqUrl(req);
55
61
  const q = url.searchParams.get('q') || '', entityFilter = url.searchParams.get('entity') || '', statusFilter = url.searchParams.get('status') || '';
56
- let teams = []; try { teams = list('team', {}); } catch {}
62
+ let teams = []; try { teams = await list('team', {}); } catch {}
57
63
  const results = {};
58
64
  for (const eName of (entityFilter ? [entityFilter] : ['engagement', 'client', 'rfi', 'review'])) {
59
- try { let items = list(eName, {}); if (q) items = items.filter(i => JSON.stringify(i).toLowerCase().includes(q.toLowerCase())); if (statusFilter) items = items.filter(i => i.status === statusFilter); const spec = getSpec(eName); if (spec) items = resolveRefFields(items, spec); results[eName] = items.slice(0, 50); } catch {}
65
+ try { let items = await list(eName, {}); if (q) items = items.filter(i => JSON.stringify(i).toLowerCase().includes(q.toLowerCase())); if (statusFilter) items = items.filter(i => i.status === statusFilter); const spec = getSpec(eName); if (spec) items = resolveRefFields(items, spec); results[eName] = items.slice(0, 50); } catch {}
60
66
  }
61
67
  return renderAdvancedSearch(user, results, { teams, stages: ['info_gathering', 'commencement', 'team_execution', 'partner_review', 'finalization', 'closeout'] });
62
68
  }
@@ -67,10 +73,10 @@ async function handleGenericEntityView(user, entityName, id) {
67
73
  if (!canCreate(user, entityName)) return renderAccessDenied(user, entityName, 'create');
68
74
  const resolvedSpec = resolveEnumOptions(spec);
69
75
  const { renderEntityForm: lazyEntityForm } = await lazyRenderer('entity-renderer.js');
70
- return lazyEntityForm(entityName, null, resolvedSpec, user, true, getRefOptions(resolvedSpec));
76
+ return lazyEntityForm(entityName, null, resolvedSpec, user, true, await getRefOptions(resolvedSpec));
71
77
  }
72
78
  if (!canView(user, entityName)) return renderAccessDenied(user, entityName, 'view');
73
- const item = get(entityName, id); if (!item) return null;
79
+ const item = await get(entityName, id); if (!item) return null;
74
80
  if (item.team_id && user.team_id && item.team_id !== user.team_id && !isPartner(user)) return renderAccessDenied(user, entityName, 'view');
75
81
  if (isClientUser(user) && user.client_id && item.client_id && item.client_id !== user.client_id) return renderAccessDenied(user, entityName, 'view');
76
82
  const [resolvedItem] = resolveRefFields([item], spec);
@@ -79,14 +85,14 @@ async function handleGenericEntityView(user, entityName, id) {
79
85
  }
80
86
  async function handleClientSubRoute(user, clientId, subRoute) {
81
87
  if (!canView(user, 'client')) return renderAccessDenied(user, 'client', 'view');
82
- const client = get('client', clientId); if (!client) return null;
88
+ const client = await get('client', clientId); if (!client) return null;
83
89
  if (isClientUser(user) && user.client_id && user.client_id !== clientId) return renderAccessDenied(user, 'client', 'view');
84
- if (subRoute === 'dashboard' || subRoute === 'users') return renderClientDashboard(user, client, getClientDashboardStats(clientId));
90
+ if (subRoute === 'dashboard' || subRoute === 'users') return renderClientDashboard(user, client, await getClientDashboardStats(clientId));
85
91
  if (subRoute === 'progress') {
86
- let engagements = []; try { engagements = list('engagement', {}).filter(e => e.client_id === clientId); } catch {}
92
+ let engagements = []; try { engagements = (await list('engagement', {})).filter(e => e.client_id === clientId); } catch {}
87
93
  const spec = getSpec('engagement'); if (spec) engagements = resolveRefFields(engagements, spec);
88
94
  let rfiStats = { total: 0, responded: 0, overdue: 0 };
89
- try { const allRfis = list('rfi', {}).filter(r => engagements.some(e => e.id === r.engagement_id)); const now = Math.floor(Date.now() / 1000); rfiStats = { total: allRfis.length, responded: allRfis.filter(r => r.status === 'responded' || r.status === 'completed').length, overdue: allRfis.filter(r => r.due_date && r.due_date < now && r.status !== 'closed').length }; } catch {}
95
+ try { const allRfis = (await list('rfi', {})).filter(r => engagements.some(e => e.id === r.engagement_id)); const now = Math.floor(Date.now() / 1000); rfiStats = { total: allRfis.length, responded: allRfis.filter(r => r.status === 'responded' || r.status === 'completed').length, overdue: allRfis.filter(r => r.due_date && r.due_date < now && r.status !== 'closed').length }; } catch {}
90
96
  return renderClientProgress(user, client, engagements, rfiStats);
91
97
  }
92
98
  return null;
@@ -94,11 +100,11 @@ async function handleClientSubRoute(user, clientId, subRoute) {
94
100
  async function handleGenericEntityEdit(user, entityName, id) {
95
101
  const spec = getSpec(entityName); if (!spec) return null;
96
102
  if (!canEdit(user, entityName)) return renderAccessDenied(user, entityName, 'edit');
97
- const item = get(entityName, id); if (!item) return null;
103
+ const item = await get(entityName, id); if (!item) return null;
98
104
  if (item.team_id && user.team_id && item.team_id !== user.team_id && !isPartner(user)) return renderAccessDenied(user, entityName, 'edit');
99
105
  const resolvedSpec = resolveEnumOptions(spec);
100
106
  const { renderEntityForm: lazyEntityForm } = await lazyRenderer('entity-renderer.js');
101
- return lazyEntityForm(entityName, item, resolvedSpec, user, false, getRefOptions(resolvedSpec));
107
+ return lazyEntityForm(entityName, item, resolvedSpec, user, false, await getRefOptions(resolvedSpec));
102
108
  }
103
109
  export async function handlePage(pathname, req, res) {
104
110
  setCurrentRequest(req);
@@ -119,18 +125,31 @@ export async function handlePage(pathname, req, res) {
119
125
  const user = await getUser();
120
126
  if (!user) { res.writeHead(302, { Location: '/login' }); res.end(); return REDIRECT; }
121
127
  if (normalized === '/unauthorized') return renderAccessDenied(user, 'system', 'access');
122
- if (normalized === '/notifications') { let notifs=[]; try{notifs=getDatabase().prepare('SELECT * FROM notification WHERE user_id=? ORDER BY created_at DESC LIMIT 100').all(user.id)}catch{} const{renderNotificationsPage}=await lazyRenderer('notifications-renderer.js'); return renderNotificationsPage(user,notifs); }
128
+ if (normalized === '/notifications') { let notifs=[]; try{notifs=await list('notification',{user_id:user.id},{sort:{field:'created_at',dir:'DESC'},limit:100})}catch{} const{renderNotificationsPage}=await lazyRenderer('notifications-renderer.js'); return renderNotificationsPage(user,notifs); }
123
129
  if (normalized === '/' || normalized === '/dashboard') return renderDashboard(user, await getDashboardStats(user));
124
130
  if (normalized.startsWith('/admin/') || normalized === '/admin/jobs') return handleAdminPage(normalized, segments, user);
125
131
  if (segments[0] === 'client' && segments.length === 3 && ['dashboard', 'users', 'progress'].includes(segments[2])) return handleClientSubRoute(user, segments[1], segments[2]);
126
132
  if (isClerk(user) && segments.length >= 1 && ['user', 'team'].includes(segments[0])) return renderAccessDenied(user, segments[0], 'list');
127
133
  if (normalized === '/mwr' || normalized === '/mwr/home') {
128
134
  if (!canList(user, 'review')) return renderAccessDenied(user, 'review', 'list');
129
- const db = getDatabase();
130
135
  let myReviews = [], sharedReviews = [], recentActivity = [];
131
- try { myReviews = db.prepare('SELECT * FROM review WHERE created_by=? OR assigned_to=? ORDER BY updated_at DESC LIMIT 100').all(user.id, user.id); } catch { try { myReviews = list('review', {}).filter(r => r.created_by === user.id || r.assigned_to === user.id); } catch {} }
132
- try { sharedReviews = db.prepare('SELECT r.* FROM review r JOIN collaborator c ON c.review_id=r.id WHERE c.user_id=? ORDER BY r.updated_at DESC LIMIT 100').all(user.id); } catch {}
133
- try { recentActivity = db.prepare("SELECT * FROM audit_logs WHERE entity_type='review' ORDER BY timestamp DESC LIMIT 50").all(); } catch {}
136
+ // OR across two columns: busybase eq() is single-column, so fetch + filter in JS.
137
+ try {
138
+ const reviews = await list('review', {}, { sort: { field: 'updated_at', dir: 'DESC' } });
139
+ myReviews = reviews.filter(r => r.created_by === user.id || r.assigned_to === user.id).slice(0, 100);
140
+ } catch {}
141
+ // Old JOIN collaborator -> two-step: collaborator rows for this user, then their reviews.
142
+ try {
143
+ const collabs = await list('collaborator', { user_id: user.id });
144
+ const ids = new Set(collabs.map(c => c.review_id));
145
+ if (ids.size) {
146
+ const reviews = await list('review', {}, { sort: { field: 'updated_at', dir: 'DESC' } });
147
+ sharedReviews = reviews.filter(r => ids.has(r.id)).slice(0, 100);
148
+ }
149
+ } catch {}
150
+ try {
151
+ recentActivity = (await list('audit_logs', { entity_type: 'review' }, { sort: { field: 'created_at', dir: 'DESC' } })).slice(0, 50);
152
+ } catch {}
134
153
  const all = [...myReviews, ...sharedReviews];
135
154
  const stats = { myReviews, sharedReviews, recentActivity, totalReviews: all.length, activeReviews: all.filter(r => (r.status||'open') !== 'archived' && (r.status||'open') !== 'completed' && (r.status||'open') !== 'closed').length, flaggedReviews: all.filter(r => r.flagged).length, overdueReviews: 0 };
136
155
  const { renderMwrHome } = await lazyRenderer('review-mwr-renderer.js');
@@ -151,18 +170,16 @@ export async function handlePage(pathname, req, res) {
151
170
  return renderFlexUpView(user);
152
171
  }
153
172
  if (normalized === '/ml-console') {
154
- const db = getDatabase();
155
173
  let candidates = []; let reviewMap = {};
156
174
  try {
157
- candidates = db.prepare(`SELECT * FROM highlight
158
- WHERE (comment IS NOT NULL AND comment != '')
159
- AND (comment LIKE '%?' OR length(comment) > 40)
160
- ORDER BY created_at DESC LIMIT 50`).all();
175
+ // Old: highlights with a non-trivial comment (LIKE '%?' OR length>40), newest 50.
176
+ const all = await list('highlight', {}, { sort: { field: 'created_at', dir: 'DESC' } });
177
+ candidates = all.filter(c => c.comment && (c.comment.includes('?') || c.comment.length > 40)).slice(0, 50);
161
178
  const revIds = [...new Set(candidates.map((c) => c.review_id))];
162
179
  if (revIds.length) {
163
- const ph = revIds.map(() => '?').join(',');
164
- const rows = db.prepare(`SELECT id, name FROM review WHERE id IN (${ph})`).all(...revIds);
165
- reviewMap = Object.fromEntries(rows.map((r) => [r.id, r.name || '-']));
180
+ const reviews = await list('review', {});
181
+ const byId = new Map(reviews.map(r => [r.id, r.name || '-']));
182
+ reviewMap = Object.fromEntries(revIds.map(id => [id, byId.get(id) || '-']));
166
183
  }
167
184
  } catch {}
168
185
  const { renderMlConsole } = await lazyRenderer('ml-console-renderer.js');
@@ -171,19 +188,19 @@ export async function handlePage(pathname, req, res) {
171
188
  if (segments.length === 2 && (segments[0] === 'engagements' || segments[0] === 'engagement') && segments[1] !== 'new') return handleEngagementDetail(user, segments[1]);
172
189
  if (segments[0] === 'engagement' && segments.length === 3 && segments[2] === 'letter') {
173
190
  if (!canView(user, 'engagement')) return renderAccessDenied(user, 'engagement', 'view');
174
- const engagement = get('engagement', segments[1]); if (!engagement) return null;
191
+ const engagement = await get('engagement', segments[1]); if (!engagement) return null;
175
192
  return renderLetterWorkflow(user, engagement);
176
193
  }
177
194
  if (segments[0] === 'engagement' && segments.length === 3 && segments[2] === 'report') {
178
195
  if (!canView(user, 'engagement')) return renderAccessDenied(user, 'engagement', 'view');
179
- const engId = segments[1]; const engagement = get('engagement', engId); if (!engagement) return null;
180
- const db = getDatabase(); let client=null,team=null,rfis=[],reviews=[],highlights=[],activity=[];
181
- try{client=engagement.client_id?get('client',engagement.client_id):null}catch{}
182
- try{team=engagement.team_id?get('team',engagement.team_id):null}catch{}
183
- try{rfis=db.prepare('SELECT * FROM rfi WHERE engagement_id=? ORDER BY created_at DESC').all(engId)}catch{}
184
- try{reviews=db.prepare('SELECT * FROM review WHERE engagement_id=? ORDER BY created_at DESC').all(engId)}catch{}
185
- try{const rids=reviews.map(r=>r.id);if(rids.length){const ph=rids.map(()=>'?').join(',');highlights=db.prepare(`SELECT * FROM highlight WHERE review_id IN (${ph})`).all(...rids)}}catch{}
186
- try{activity=db.prepare("SELECT * FROM audit_logs WHERE entity_type='engagement' AND entity_id=? ORDER BY timestamp DESC LIMIT 20").all(engId)}catch{}
196
+ const engId = segments[1]; const engagement = await get('engagement', engId); if (!engagement) return null;
197
+ let client=null,team=null,rfis=[],reviews=[],highlights=[],activity=[];
198
+ try{client=engagement.client_id?await get('client',engagement.client_id):null}catch{}
199
+ try{team=engagement.team_id?await get('team',engagement.team_id):null}catch{}
200
+ try{rfis=await list('rfi',{engagement_id:engId},{sort:{field:'created_at',dir:'DESC'}})}catch{}
201
+ try{reviews=await list('review',{engagement_id:engId},{sort:{field:'created_at',dir:'DESC'}})}catch{}
202
+ try{const rids=new Set(reviews.map(r=>r.id));if(rids.size){const allHl=await list('highlight',{});highlights=allHl.filter(h=>rids.has(h.review_id))}}catch{}
203
+ try{activity=(await list('audit_logs',{entity_type:'engagement',entity_id:engId},{sort:{field:'created_at',dir:'DESC'}})).slice(0,20)}catch{}
187
204
  const{renderFlexupReport}=await lazyRenderer('flexup-report-renderer.js');
188
205
  return renderFlexupReport(user,engagement,client,rfis,reviews,highlights,activity,team);
189
206
  }
@@ -192,7 +209,7 @@ export async function handlePage(pathname, req, res) {
192
209
  if (segments.length === 1 && segments[0] === 'rfi') return handleRfiList(user);
193
210
  if (segments.length === 1 && segments[0] === 'client') {
194
211
  if (!canList(user, 'client')) return renderAccessDenied(user, 'client', 'list');
195
- let clients = list('client', {});
212
+ let clients = await list('client', {});
196
213
  if (isClientUser(user) && user.client_id) clients = clients.filter(c => c.id === user.client_id);
197
214
  return renderClientList(user, clients);
198
215
  }
@@ -201,7 +218,7 @@ export async function handlePage(pathname, req, res) {
201
218
  const spec = getSpec(entityName); if (!spec) return null;
202
219
  if (isClientUser(user) && !canClientAccessEntity(user, entityName)) return renderAccessDenied(user, entityName, 'list');
203
220
  if (!canList(user, entityName)) return renderAccessDenied(user, entityName, 'list');
204
- let items = list(entityName, {});
221
+ let items = await list(entityName, {});
205
222
  if (isClientUser(user) && user.client_id) items = items.filter(item => { if (item.client_id) return item.client_id === user.client_id; if (item.assigned_to) return item.assigned_to === user.id; return true; });
206
223
  const { renderEntityList: lazyEntityList } = await lazyRenderer('entity-renderer.js');
207
224
  return lazyEntityList(entityName, resolveRefFields(items, spec), spec, user);
@@ -209,8 +226,8 @@ export async function handlePage(pathname, req, res) {
209
226
  if (segments.length === 2 && segments[0] === 'client' && segments[1] !== 'new') {
210
227
  if (!canView(user, 'client')) return renderAccessDenied(user, 'client', 'view');
211
228
  if (isClientUser(user) && user.client_id && user.client_id !== segments[1]) return renderAccessDenied(user, 'client', 'view');
212
- const client = get('client', segments[1]); if (!client) return null;
213
- return renderClientDashboard(user, client, getClientDashboardStats(segments[1]));
229
+ const client = await get('client', segments[1]); if (!client) return null;
230
+ return renderClientDashboard(user, client, await getClientDashboardStats(segments[1]));
214
231
  }
215
232
  if (segments.length === 2) return handleGenericEntityView(user, segments[0], segments[1]);
216
233
  if (segments.length === 3 && segments[2] === 'edit') return handleGenericEntityEdit(user, segments[0], segments[1]);