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
@@ -3,7 +3,7 @@ import { esc, TOAST_SCRIPT, emptyRow } from '@/ui/render-helpers.js';
3
3
 
4
4
  function fmtDate(ts) {
5
5
  if (!ts) return '-';
6
- return new Date(typeof ts === 'number' && ts < 2e10 ? ts * 1000 : ts).toLocaleString();
6
+ return new Date(typeof ts === 'number' && ts < 2e10 ? ts * 1000 : ts).toLocaleString('en-ZA');
7
7
  }
8
8
 
9
9
  function notifRow(n) {
@@ -11,14 +11,14 @@ function notifRow(n) {
11
11
  const typeLabels = { review_notification: 'Review', highlight_response: 'Highlight', rfi_response: 'RFI', general: 'General' };
12
12
  const typeLabel = typeLabels[n.type] || n.type || '-';
13
13
  const entityLink = n.entity_type === 'review' && n.entity_id
14
- ? `<a href="/review/${esc(n.entity_id)}" class="text-primary text-sm" style="text-decoration:underline">View</a>`
14
+ ? `<a href="/review/${esc(n.entity_id)}" class="text-sm" style="text-decoration:underline;color:var(--color-primary)">View</a>`
15
15
  : '';
16
- return `<tr class="hover${isRead ? ' opacity-60' : ''}" data-notif-id="${esc(n.id)}">
17
- <td class="text-xs text-base-content/40 w-32">${fmtDate(n.created_at)}</td>
16
+ return `<tr class="hover${isRead ? ' is-read' : ''}" data-notif-id="${esc(n.id)}" data-notif-row="true">
17
+ <td class="text-xs text-base-content/60 w-32">${fmtDate(n.created_at)}</td>
18
18
  <td class="text-xs"><span class="badge badge-flat-secondary">${esc(typeLabel)}</span></td>
19
19
  <td class="text-sm max-w-md">${esc(n.message || '-')}</td>
20
20
  <td>${entityLink}</td>
21
- <td>${isRead ? '<span class="text-xs text-base-content/30">Read</span>' : `<button data-action="markRead" data-args='["${esc(n.id)}"]' class="btn btn-ghost btn-xs">Mark read</button>`}</td>
21
+ <td>${isRead ? '<span class="text-xs text-base-content/70">Read</span>' : `<button data-action="markRead" data-args='["${esc(n.id)}"]' class="btn btn-ghost btn-xs">Mark read</button>`}</td>
22
22
  </tr>`;
23
23
  }
24
24
 
@@ -28,13 +28,13 @@ export function renderNotificationsPage(user, notifs = []) {
28
28
  const content = `
29
29
  <div class="page-header">
30
30
  <h1 class="page-title">Notifications</h1>
31
- <button data-action="markAllRead" class="btn btn-ghost btn-sm">Mark All Read</button>
31
+ <button data-action="markAllRead" class="btn btn-primary-clean btn-sm">Mark All Read</button>
32
32
  </div>
33
33
  <div class="card-clean">
34
34
  <div class="card-clean-body">
35
35
  <div class="table-wrap">
36
36
  <table class="data-table">
37
- <thead><tr><th>Time</th><th>Type</th><th>Message</th><th></th><th></th></tr></thead>
37
+ <thead><tr><th>Time</th><th>Type</th><th>Message</th><th>Link</th><th>Actions</th></tr></thead>
38
38
  <tbody>${rows}</tbody>
39
39
  </table>
40
40
  </div>
@@ -43,8 +43,9 @@ export function renderNotificationsPage(user, notifs = []) {
43
43
  `;
44
44
 
45
45
  const script = `${TOAST_SCRIPT}
46
- window.markRead=async function(id){try{await fetch('/api/notifications',{method:'PATCH',headers:{'Content-Type':'application/json'},credentials:'include',body:JSON.stringify({id})});var row=document.querySelector('[data-notif-id="'+id+'"]');if(row){row.classList.add('opacity-60');var btn=row.querySelector('button');if(btn)btn.replaceWith(document.createTextNode('Read'))}var count=document.getElementById('notif-count');if(count){var c=parseInt(count.textContent||'0',10)-1;if(c<=0)count.style.display='none';else count.textContent=c}}catch(e){showToast('Error','error')}};
47
- window.markAllRead=async function(){try{await fetch('/api/notifications',{method:'PATCH',headers:{'Content-Type':'application/json'},credentials:'include',body:JSON.stringify({})});document.querySelectorAll('[data-notif-id]').forEach(function(row){row.classList.add('opacity-60');var btn=row.querySelector('button');if(btn)btn.replaceWith(document.createTextNode('Read'))});var count=document.getElementById('notif-count');if(count)count.style.display='none';showToast('All marked read','success')}catch(e){showToast('Error','error')}};`;
46
+ window.markRead=async function(id){var row=document.querySelector('[data-notif-id="'+id+'"]');var b=row&&row.querySelector('button[data-action="markRead"]');if(b){if(b.disabled)return;b.disabled=true}try{await fetch('/api/notifications',{method:'PATCH',headers:{'Content-Type':'application/json'},credentials:'include',body:JSON.stringify({id})});if(row){row.classList.add('is-read');if(b)b.replaceWith(document.createTextNode('Read'))}var count=document.getElementById('notif-count');if(count){var c=parseInt(count.textContent||'0',10)-1;if(c<=0)count.style.display='none';else count.textContent=c}}catch(e){if(b)b.disabled=false;showToast('Error','error')}};
47
+ window.markAllRead=async function(){var b=document.querySelector('button[data-action="markAllRead"]');if(b){if(b.disabled)return;window.loadingBtn(b,true,'Marking...')}try{await fetch('/api/notifications',{method:'PATCH',headers:{'Content-Type':'application/json'},credentials:'include',body:JSON.stringify({})});document.querySelectorAll('[data-notif-id]').forEach(function(row){row.classList.add('is-read');var btn=row.querySelector('button');if(btn)btn.replaceWith(document.createTextNode('Read'))});var count=document.getElementById('notif-count');if(count)count.style.display='none';showToast('All marked read','success')}catch(e){if(b)window.loadingBtn(b,false);showToast('Error','error')}};
48
+ document.querySelectorAll('[data-notif-row]').forEach(function(row){row.addEventListener('keydown',function(e){if(e.key==='Enter'||e.key===' '){e.preventDefault();var btn=row.querySelector('button[data-action="markRead"]');if(btn)btn.click();else window.markRead(row.getAttribute('data-notif-id'))}})});`;
48
49
 
49
- return page(user, 'Notifications | Moonlanding', null, content, [script]);
50
+ return page(user, 'Notifications | Thatcher', null, content, [script]);
50
51
  }
@@ -1,4 +1,4 @@
1
- import { list, get } from '@/lib/query-engine.js';
1
+ import { list, get } from '@/lib/busybase-store.js';
2
2
  import { renderAuditDashboard, renderSystemHealth, renderAccessDenied, generateHtml, REDIRECT } from '@/ui/renderer.js';
3
3
  import { renderSettingsHome, renderSettingsSystem, renderSettingsUsers, renderSettingsTeams, renderSettingsRfiSections } from '@/ui/settings-renderer.js';
4
4
  import { renderSettingsTemplates, renderSettingsNotifications, renderSettingsIntegrations, renderSettingsChecklists, renderSettingsRecreation, renderSettingsEntityTypes, renderSettingsEngagementTypes } from '@/ui/settings-renderer-advanced.js';
@@ -24,97 +24,106 @@ export async function handleAdminPage(normalized, segments, user) {
24
24
  if (!isPartner(user)) return renderAccessDenied(user, 'admin', 'view');
25
25
 
26
26
  if (normalized === '/admin/settings') {
27
- const config = await getSystemConfig();
28
- const counts = await getSettingsCounts();
29
- return renderSettingsHome(user, config, counts);
27
+ try {
28
+ const config = await getSystemConfig();
29
+ const counts = await getSettingsCounts();
30
+ return renderSettingsHome(user, config, counts);
31
+ } catch (err) {
32
+ console.error('Failed to load admin settings:', err);
33
+ return generateHtml('Settings', '<div class="empty-state"><div class="empty-state-title">Failed to load. Refresh.</div><div class="empty-state-desc">The settings could not be loaded. Please refresh the page to try again.</div></div>', []);
34
+ }
30
35
  }
31
36
  if (normalized === '/admin/settings/system') { const config = await getSystemConfig(); return renderSettingsSystem(user, config); }
32
- if (normalized === '/admin/settings/users') { return renderSettingsUsers(user, list('user', {})); }
37
+ if (normalized === '/admin/settings/users') { return renderSettingsUsers(user, await list('user', {})); }
33
38
  if (normalized === '/admin/settings/teams') {
34
39
  const { renderSettingsTeams: _rt } = await lazyRenderer('settings-renderer.js');
35
- return _rt(user, list('team', {}), list('user', {}));
40
+ return _rt(user, await list('team', {}), await list('user', {}));
36
41
  }
37
42
  if (normalized === '/admin/settings/rfi-sections') {
38
- let sections = []; try { sections = list('rfi_section', {}); } catch {}
43
+ let sections = []; try { sections = await list('rfi_section', {}); } catch {}
39
44
  return renderSettingsRfiSections(user, sections);
40
45
  }
41
46
  if (normalized === '/admin/settings/rfi-templates') {
42
47
  const { renderSettingsRfiTemplates } = await lazyRenderer('settings-renderer-rfi-templates.js');
43
- let templates = []; try { templates = list('rfi_template', {}); } catch {}
48
+ let templates = []; try { templates = await list('rfi_template', {}); } catch {}
44
49
  return renderSettingsRfiTemplates(user, templates);
45
50
  }
46
51
  if (normalized === '/admin/settings/templates') {
47
- let templates = []; try { templates = list('review_template', {}); } catch {}
52
+ let templates = []; try { templates = await list('review_template', {}); } catch {}
48
53
  return renderSettingsTemplates(user, templates);
49
54
  }
50
55
  if (normalized === '/admin/settings/notifications') { return renderSettingsNotifications(user, await getSystemConfig()); }
51
56
  if (normalized === '/admin/settings/integrations') {
52
57
  let intSettings = {};
53
- try { const { getDatabase } = await import('@/lib/database-core.js'); const row = getDatabase().prepare('SELECT value FROM system_settings WHERE key = ?').get('integration_settings'); intSettings = row ? JSON.parse(row.value) : {}; } catch {}
58
+ try { const row = await get('system_settings', 'integration_settings'); intSettings = row?.value ? JSON.parse(row.value) : {}; } catch {}
54
59
  return renderSettingsIntegrations(user, intSettings);
55
60
  }
56
61
  if (normalized === '/admin/settings/checklists') {
57
- let checklists = []; try { checklists = list('checklist', {}); } catch {}
62
+ let checklists = []; try { checklists = await list('checklist', {}); } catch {}
58
63
  return renderSettingsChecklists(user, checklists);
59
64
  }
60
65
  if (normalized === '/admin/settings/recreation') {
61
66
  return renderSettingsRecreation(user);
62
67
  }
63
68
  if (normalized === '/admin/settings/entity-types') {
64
- let items = []; try { items = list('entity_type', {}); } catch {}
69
+ let items = []; try { items = await list('entity_type', {}); } catch {}
65
70
  return renderSettingsEntityTypes(user, items);
66
71
  }
67
72
  if (normalized === '/admin/settings/engagement-types') {
68
- let items = []; try { items = list('engagement_type', {}); } catch {}
73
+ let items = []; try { items = await list('engagement_type', {}); } catch {}
69
74
  return renderSettingsEngagementTypes(user, items);
70
75
  }
71
76
  if (normalized === '/admin/settings/permissions') {
72
- let permissions = []; try { permissions = list('permission', {}); } catch {}
77
+ let permissions = []; try { permissions = await list('permission', {}); } catch {}
73
78
  return renderSettingsMwrPermissions(user, permissions);
74
79
  }
75
80
  if (normalized === '/admin/settings/review') { return renderSettingsReviewSettings(user, await getSystemConfig()); }
76
81
  if (normalized === '/admin/settings/file-review') {
77
82
  let frSettings = {};
78
- try { const { getDatabase } = await import('@/lib/database-core.js'); const row = getDatabase().prepare('SELECT value FROM system_settings WHERE key = ?').get('file_review_settings'); frSettings = row ? JSON.parse(row.value) : {}; } catch {}
83
+ try { const row = await get('system_settings', 'file_review_settings'); frSettings = row?.value ? JSON.parse(row.value) : {}; } catch {}
79
84
  return renderSettingsFileReview(user, await getSystemConfig(), frSettings);
80
85
  }
81
86
  if (normalized.startsWith('/admin/settings/templates/') && segments.length === 4) {
82
87
  const templateId = segments[3];
83
88
  let template = {}, sections = [];
84
- try { template = get('review_template', templateId) || {}; sections = list('review_template_section', {}).filter(s => s.review_template_id === templateId); } catch {}
89
+ try { template = await get('review_template', templateId) || {}; sections = (await list('review_template_section', {})).filter(s => s.review_template_id === templateId); } catch {}
85
90
  return renderSettingsTemplateManage(user, template, sections);
86
91
  }
87
92
  if (normalized === '/admin/settings/checklists/manage') {
88
93
  let checklists = [];
89
- try { checklists = list('checklist', {}).map(c => { let items = []; try { items = list('checklist_item', {}).filter(i => i.checklist_id === c.id); } catch {} return { ...c, total_items: items.length }; }); } catch {}
94
+ try {
95
+ const cls = await list('checklist', {});
96
+ const allItems = await list('checklist_item', {});
97
+ checklists = cls.map(c => ({ ...c, total_items: allItems.filter(i => i.checklist_id === c.id).length }));
98
+ } catch {}
90
99
  return renderChecklistsManagement(user, checklists);
91
100
  }
92
101
  if (normalized === '/admin/build-logs') {
93
- let logs = []; try { logs = list('build_log', {}); } catch {}
102
+ let logs = []; try { logs = await list('build_log', {}); } catch {}
94
103
  const content = renderBuildLogsContent(logs);
95
104
  return generateHtml('Build Logs', content, []);
96
105
  }
97
106
  if (normalized === '/admin/health') { return renderSystemHealth(user, await getSystemHealth()); }
98
107
  if (normalized === '/admin/settings/users/new') {
99
108
  const { renderSettingsUserDetail } = await lazyRenderer('settings-user-team-renderer.js');
100
- return renderSettingsUserDetail(user, {}, list('team', {}));
109
+ return renderSettingsUserDetail(user, {}, await list('team', {}));
101
110
  }
102
111
  if (segments.length === 4 && segments[2] === 'users' && segments[3] !== 'new') {
103
112
  const { renderSettingsUserDetail: _rUD } = await lazyRenderer('settings-user-team-renderer.js');
104
- return _rUD(user, get('user', segments[3]) || {}, list('team', {}));
113
+ return _rUD(user, await get('user', segments[3]) || {}, await list('team', {}));
105
114
  }
106
115
  if (normalized === '/admin/settings/teams/new') {
107
116
  const { renderSettingsTeamDetail } = await lazyRenderer('settings-user-team-renderer.js');
108
- return renderSettingsTeamDetail(user, {}, list('user', {}));
117
+ return renderSettingsTeamDetail(user, {}, await list('user', {}));
109
118
  }
110
119
  if (segments.length === 4 && segments[2] === 'teams' && segments[3] !== 'new') {
111
120
  const { renderSettingsTeamDetail: _rTD } = await lazyRenderer('settings-user-team-renderer.js');
112
- return _rTD(user, get('team', segments[3]) || {}, list('user', {}));
121
+ return _rTD(user, await get('team', segments[3]) || {}, await list('user', {}));
113
122
  }
114
123
  if (normalized === '/admin/jobs') {
115
124
  let jobs = [];
116
125
  try { const { getConfigEngineSync } = await import('@/lib/config-generator-engine.js'); const engine = getConfigEngineSync(); const config = engine.getConfig(); jobs = (config.jobs || []).map(j => ({ ...j, status: j.enabled === false ? 'disabled' : 'scheduled' })); } catch {}
117
- let logs = []; try { logs = list('job_log', {}).slice(0, 20); } catch {}
126
+ let logs = []; try { logs = (await list('job_log', {})).slice(0, 20); } catch {}
118
127
  return renderJobManagement(user, jobs, logs);
119
128
  }
120
129
  return null;
@@ -1,4 +1,7 @@
1
- import { list, get, count } from '@/lib/query-engine.js';
1
+ import { list, get, count } from '@/lib/busybase-store.js';
2
+ import { createLogger } from '@/lib/logger.js';
3
+
4
+ const log = createLogger('[PageHandlerHelpers]');
2
5
  import { getConfigEngineSync } from '@/lib/config-generator-engine.js';
3
6
 
4
7
  export function resolveEnumOptions(spec) {
@@ -14,28 +17,28 @@ export function resolveEnumOptions(spec) {
14
17
  return resolvedSpec;
15
18
  }
16
19
 
17
- export function getRefOptions(spec) {
20
+ export async function getRefOptions(spec) {
18
21
  const refOptions = {};
19
22
  for (const [fieldKey, field] of Object.entries(spec.fields || {}).filter(([k, f]) => f.type === 'ref' && f.ref)) {
20
- try { refOptions[fieldKey] = list(field.ref, {}).map(r => ({ value: r.id, label: r.name || r.title || r.label || r.email || r.id })); }
23
+ try { refOptions[fieldKey] = (await list(field.ref, {})).map(r => ({ value: r.id, label: r.name || r.title || r.label || r.email || r.id })); }
21
24
  catch { refOptions[fieldKey] = []; }
22
25
  }
23
26
  return refOptions;
24
27
  }
25
28
 
29
+ // busybase-store's list()/get() already attach `<field>_display` for ref fields (the
30
+ // old LEFT JOIN equivalent), so this is now a sync pass-through: it just guarantees a
31
+ // `_display` key exists (falling back to the raw id) without issuing any query.
26
32
  export function resolveRefFields(items, spec) {
27
33
  try {
28
34
  const refFields = Object.entries(spec.fields || {}).filter(([k, f]) => f.type === 'ref' && f.ref);
29
35
  if (!refFields.length) return items;
30
- const refCaches = {};
31
- for (const [fieldKey, field] of refFields) {
32
- const refIds = [...new Set(items.map(i => i[fieldKey]).filter(Boolean))];
33
- if (refIds.length) try { refCaches[fieldKey] = Object.fromEntries(list(field.ref, {}).map(r => [r.id, r.name || r.title || r.label || r.id])); } catch {}
34
- }
35
36
  return items.map(item => {
36
37
  const resolved = { ...item };
37
38
  for (const [fieldKey] of refFields) {
38
- if (item[fieldKey] && refCaches[fieldKey]) resolved[`${fieldKey}_display`] = refCaches[fieldKey][item[fieldKey]] || item[fieldKey];
39
+ if (item[fieldKey] && resolved[`${fieldKey}_display`] === undefined) {
40
+ resolved[`${fieldKey}_display`] = item[fieldKey];
41
+ }
39
42
  }
40
43
  return resolved;
41
44
  });
@@ -44,11 +47,11 @@ export function resolveRefFields(items, spec) {
44
47
 
45
48
  export async function getDashboardStats(user) {
46
49
  try {
47
- const engagements = list('engagement', {});
48
- const clients = list('client', {});
50
+ const engagements = await list('engagement', {});
51
+ const clients = await list('client', {});
49
52
  const clientMap = Object.fromEntries(clients.map(c => [c.id, c.name]));
50
- const rfis = list('rfi', {});
51
- const reviews = list('review', {});
53
+ const rfis = await list('rfi', {});
54
+ const reviews = await list('review', {});
52
55
  const myRfis = user ? rfis.filter(r => r.assigned_to === user.id) : [];
53
56
  const now = Math.floor(Date.now() / 1000);
54
57
  const overdueRfis = rfis.filter(r => r.status !== 'closed' && r.due_date && r.due_date < now).map(r => ({ ...r, daysOverdue: Math.floor((now - r.due_date) / 86400) }));
@@ -60,7 +63,7 @@ export async function getDashboardStats(user) {
60
63
  export async function getSettingsCounts() {
61
64
  const counts = {};
62
65
  const entities = { users: 'user', teams: 'team', rfiSections: 'rfi_section', templates: 'review_template', checklists: 'checklist', recreation: 'permission_audit', rfiTemplates: 'rfi_template' };
63
- for (const [key, entity] of Object.entries(entities)) { try { counts[key] = count(entity); } catch { counts[key] = 0; } }
66
+ for (const [key, entity] of Object.entries(entities)) { try { counts[key] = await count(entity); } catch { counts[key] = 0; } }
64
67
  return counts;
65
68
  }
66
69
 
@@ -75,27 +78,28 @@ export async function getSystemConfig() {
75
78
 
76
79
  export async function getAuditData() {
77
80
  try {
78
- const audits = list('permission_audit', {});
81
+ const audits = await list('permission_audit', {});
79
82
  const cutoff = Math.floor(Date.now() / 1000) - 30 * 86400;
80
83
  const recent = audits.filter(a => (a.timestamp || a.created_at) > cutoff);
81
84
  return { summary: { total_actions: recent.length, grants: recent.filter(a => a.action === 'grant').length, revokes: recent.filter(a => a.action === 'revoke').length, role_changes: recent.filter(a => a.action === 'role_change').length }, recentActivity: audits.slice(0, 50) };
82
85
  } catch { return { summary: {}, recentActivity: [] }; }
83
86
  }
84
87
 
85
- export function getClientDashboardStats(clientId) {
88
+ export async function getClientDashboardStats(clientId) {
86
89
  try {
87
- const engagements = list('engagement', {}).filter(e => e.client_id === clientId);
88
- const rfis = list('rfi', {}).filter(r => engagements.some(e => e.id === r.engagement_id));
89
- const reviews = list('review', {}).filter(r => engagements.some(e => e.id === r.engagement_id));
90
- const users = list('user', {}).filter(u => u.client_id === clientId);
90
+ const engagements = await list('engagement', { client_id: clientId });
91
+ const engIds = new Set(engagements.map(e => e.id));
92
+ const rfis = (await list('rfi', {})).filter(r => engIds.has(r.engagement_id));
93
+ const reviews = (await list('review', {})).filter(r => engIds.has(r.engagement_id));
94
+ const users = await list('user', { client_id: clientId });
91
95
  return { engagements: engagements.length, activeRfis: rfis.filter(r => r.status !== 'closed' && r.status !== 'completed').length, users: users.length, reviews: reviews.length, engagementList: engagements.slice(0, 10) };
92
- } catch { return { engagements: 0, activeRfis: 0, users: 0, reviews: 0, engagementList: [] }; }
96
+ } catch (err) { log.error('getClientDashboardStats failed', { clientId, message: err?.message || String(err) }); return { engagements: 0, activeRfis: 0, users: 0, reviews: 0, engagementList: [] }; }
93
97
  }
94
98
 
95
99
  export async function getSystemHealth() {
96
100
  try {
97
101
  const entityCounts = {};
98
- for (const e of ['user','client','engagement','rfi','review','team']) { try { entityCounts[e] = count(e); } catch { entityCounts[e] = 0; } }
102
+ for (const e of ['user','client','engagement','rfi','review','team']) { try { entityCounts[e] = await count(e); } catch { entityCounts[e] = 0; } }
99
103
  const u = process.uptime();
100
104
  return { database: { status: 'Connected', type: 'SQLite' }, server: { port: 3004, uptime: `${Math.floor(u/3600)}h ${Math.floor((u%3600)/60)}m` }, entities: entityCounts };
101
105
  } catch { return {}; }
@@ -104,7 +108,7 @@ export async function getSystemHealth() {
104
108
  export function renderBuildLogsContent(logs) {
105
109
  const rows = (logs || []).map(l => {
106
110
  const sts = l.status === 'success' ? '<span class="pill pill-success">Success</span>' : l.status === 'failed' ? '<span class="pill pill-danger">Failed</span>' : `<span class="pill pill-warning">${l.status || 'pending'}</span>`;
107
- const ts = l.created_at ? new Date(l.created_at * 1000).toLocaleString() : '-';
111
+ const ts = l.created_at ? new Date(l.created_at * 1000).toLocaleString('en-ZA') : '-';
108
112
  return `<tr><td style="padding:8px 12px">${l.version||'-'}</td><td style="padding:8px 12px">${sts}</td><td style="padding:8px 12px;font-size:0.78rem;color:var(--color-text-muted)">${ts}</td><td style="padding:8px 12px">${l.duration?l.duration+'s':'-'}</td><td style="padding:8px 12px;font-size:0.78rem">${l.message||'-'}</td></tr>`;
109
113
  }).join('');
110
114
  return `<div style="margin-bottom:24px"><h1 class="page-title">Build Logs</h1></div><div class="table-wrap" style="overflow-x:auto"><table class="data-table"><thead><tr><th>Version</th><th>Status</th><th>Time</th><th>Duration</th><th>Message</th></tr></thead><tbody>${rows||'<tr><td colspan="5" style="padding:32px;text-align:center;color:var(--color-text-muted)">No build logs</td></tr>'}</tbody></table></div>`;
@@ -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');