thatcher 1.0.41 → 1.0.43

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 (122) hide show
  1. package/README.md +27 -0
  2. package/package.json +2 -5
  3. package/src/app/api/audit/dashboard/route.js +2 -2
  4. package/src/app/api/audit/logs/route.js +2 -2
  5. package/src/app/api/audit/permissions/[id]/route.js +2 -2
  6. package/src/app/api/audit/permissions/route.js +3 -3
  7. package/src/app/api/audit/permissions/stats/route.js +2 -2
  8. package/src/app/api/audit/route.js +2 -3
  9. package/src/app/api/audit/stats/route.js +2 -2
  10. package/src/app/api/auth/google/callback/route.js +1 -1
  11. package/src/app/api/auth/google/route.js +1 -1
  12. package/src/app/api/auth/login/route.js +1 -1
  13. package/src/app/api/auth/logout/route.js +1 -1
  14. package/src/app/api/auth/me/route.js +1 -1
  15. package/src/app/api/auth/mwr-bridge/route.js +1 -1
  16. package/src/app/api/auth/password-reset/route.js +1 -1
  17. package/src/app/api/csrf-token/route.js +1 -1
  18. package/src/app/api/debug/[[...path]]/route.js +2 -2
  19. package/src/app/api/debug/config/route.js +1 -1
  20. package/src/app/api/debug/hooks/route.js +1 -1
  21. package/src/app/api/debug/plugins/route.js +1 -1
  22. package/src/app/api/debug/sqlite/route.js +1 -1
  23. package/src/app/api/debug/sync/route.js +1 -1
  24. package/src/app/api/debug/workflow/route.js +1 -1
  25. package/src/app/api/domains/[domain]/route.js +1 -2
  26. package/src/app/api/domains/route.js +1 -1
  27. package/src/app/api/email/allocate/batch/route.js +1 -1
  28. package/src/app/api/email/allocate/route.js +1 -1
  29. package/src/app/api/email/receive/route.js +1 -1
  30. package/src/app/api/email/send/route.js +1 -1
  31. package/src/app/api/email/unallocated/route.js +1 -1
  32. package/src/app/api/files/[id]/route.js +1 -1
  33. package/src/app/api/health/route.js +1 -2
  34. package/src/app/api/metrics/route.js +1 -2
  35. package/src/engine.js +3 -3
  36. package/src/engine.server.js +3 -3
  37. package/src/index.js +35 -10
  38. package/src/lib/action-factory.js +1 -1
  39. package/src/lib/api-helpers.js +1 -1
  40. package/src/lib/api.js +3 -4
  41. package/src/lib/audit-logger-enhanced.js +1 -1
  42. package/src/lib/auth-middleware.js +1 -1
  43. package/src/lib/auth-route-helpers.js +1 -1
  44. package/src/lib/{busybase-adapter.js → busybase/adapter.js} +3 -3
  45. package/src/lib/{busybase-audit-reads.js → busybase/audit-reads.js} +1 -1
  46. package/src/lib/{busybase-audit.js → busybase/audit.js} +2 -2
  47. package/src/lib/{busybase-lucia-adapter.js → busybase/lucia-adapter.js} +1 -1
  48. package/src/lib/{busybase-store.js → busybase/store.js} +3 -3
  49. package/src/lib/config-generator-engine.js +13 -1
  50. package/src/lib/crud-action-helpers.js +1 -1
  51. package/src/lib/crud-handlers.js +4 -4
  52. package/src/lib/email-sender.js +1 -1
  53. package/src/lib/errors/index.js +3 -0
  54. package/src/lib/{error-recovery.js → errors/recovery.js} +120 -4
  55. package/src/lib/{error-handler.js → errors/types.js} +1 -1
  56. package/src/lib/errors/wrap.js +225 -0
  57. package/src/lib/events-engine.js +1 -1
  58. package/src/lib/hot-reload/index.js +34 -0
  59. package/src/lib/index.js +5 -5
  60. package/src/lib/keyed-cache.js +112 -0
  61. package/src/lib/monitor.js +285 -0
  62. package/src/lib/next-shim.js +294 -0
  63. package/src/lib/observability-init.js +213 -0
  64. package/src/lib/perf.js +468 -0
  65. package/src/lib/query-cache.js +40 -46
  66. package/src/lib/realtime-server.js +18 -0
  67. package/src/lib/render-cache.js +14 -28
  68. package/src/lib/{request-tracing.js → request-trace.js} +57 -2
  69. package/src/lib/response-formatter.js +18 -0
  70. package/src/lib/route-helpers.js +1 -1
  71. package/src/lib/state-protocol.js +13 -0
  72. package/src/lib/state-transport-client.js +6 -0
  73. package/src/lib/state-transport-reconnect.js +5 -0
  74. package/src/lib/state-transport-server.js +8 -0
  75. package/src/lib/utils.js +1 -1
  76. package/src/lib/{validate.js → validation/entity-validators.js} +9 -8
  77. package/src/lib/validation/index.js +1 -1
  78. package/src/lib/validation/security-validators.js +1 -1
  79. package/src/lib/validation-middleware.js +2 -2
  80. package/src/lib/workflow-engine.js +23 -3
  81. package/src/lib/xstate-workflow-engine.js +18 -2
  82. package/src/services/collaborator-role.service.js +2 -2
  83. package/src/services/notification-engine.js +5 -5
  84. package/src/services/permission.service.js +1 -1
  85. package/src/ui/format-helpers.js +1 -4
  86. package/src/ui/highlight-threading-renderer.js +1 -1
  87. package/src/ui/page-handler-admin.js +6 -5
  88. package/src/ui/page-handler-helpers.js +2 -2
  89. package/src/ui/page-handler-reviews.js +4 -4
  90. package/src/ui/page-handler-rfi.js +1 -1
  91. package/src/ui/page-handler.js +2 -2
  92. package/src/ui/render-helpers.js +1 -1
  93. package/src/ui/renderer.js +1 -1
  94. package/src/ui/{review-comparison-renderer.js → review/comparison.js} +1 -1
  95. package/src/ui/{review-detail-renderer.js → review/detail-renderer.js} +3 -3
  96. package/src/ui/review/index.js +32 -0
  97. package/src/ui/{review-mwr-renderer.js → review/mwr.js} +1 -1
  98. package/src/ui/{review-renderer.js → review/renderer.js} +1 -1
  99. package/src/ui/{settings-renderer.js → settings/home.js} +7 -34
  100. package/src/ui/{settings-renderer-advanced.js → settings/review.js} +117 -56
  101. package/src/ui/settings/shared.js +45 -0
  102. package/src/ui/{settings-renderer-teams.js → settings/teams.js} +1 -2
  103. package/src/ui/settings/templates.js +116 -0
  104. package/src/lib/api-error-wrapper.js +0 -125
  105. package/src/lib/db-monitor.js +0 -127
  106. package/src/lib/error-resilience.js +0 -132
  107. package/src/lib/monitoring-init.js +0 -67
  108. package/src/lib/next-compat.js +0 -94
  109. package/src/lib/next-polyfills.js +0 -135
  110. package/src/lib/observability-bootstrap.js +0 -116
  111. package/src/lib/perf-monitor.js +0 -94
  112. package/src/lib/perf-profiler.js +0 -233
  113. package/src/lib/query-perf.js +0 -117
  114. package/src/lib/request-tracker.js +0 -43
  115. package/src/lib/resource-monitor.js +0 -120
  116. package/src/lib/validators.js +0 -67
  117. package/src/lib/with-error-handler.js +0 -31
  118. package/src/ui/settings-renderer-advanced2.js +0 -159
  119. /package/src/ui/{review-detail-panels.js → review/detail-panels.js} +0 -0
  120. /package/src/ui/{review-detail-script.js → review/detail-script.js} +0 -0
  121. /package/src/ui/{review-widgets.js → review/widgets.js} +0 -0
  122. /package/src/ui/{review-zone-nav.js → review/zone-nav.js} +0 -0
@@ -0,0 +1,116 @@
1
+ /*
2
+ * Content-management settings pages: review templates (list + single-template
3
+ * manage), checklists, and the generic entity/engagement "type list" pattern.
4
+ * Ported unchanged from settings-renderer-advanced.js (templates, checklists,
5
+ * entity-types, engagement-types) and settings-renderer-advanced2.js
6
+ * (renderSettingsTemplateManage, the single-template detail/section-manage page).
7
+ */
8
+ import { TOAST_SCRIPT, settingsPage, settingsBack, inlineTable, bc, esc } from '@/ui/settings/shared.js';
9
+
10
+ const editBtn = (href) => `<a href="${href}" data-stop-propagation="true" class="btn btn-ghost btn-xs">Edit</a>`;
11
+ const trClick = (url) => `class="hover cursor-pointer" data-navigate="${url}"`;
12
+ const hdr = (title, addHref, addLabel) => `${settingsBack()}<div class="flex justify-between items-center mb-6">
13
+ <h1 class="text-2xl font-bold">${title}</h1>
14
+ <a href="${addHref}" class="btn btn-primary btn-sm">${addLabel}</a>
15
+ </div>`;
16
+
17
+ export function renderSettingsTemplates(user, templates = []) {
18
+ const rows = templates.map(t => `<tr ${trClick('/review_template/'+t.id)}>
19
+ <td class="text-sm font-medium">${esc(t.name||'-')}</td>
20
+ <td><span class="badge badge-flat-primary text-xs">${esc(t.type||'standard')}</span></td>
21
+ <td>${t.is_active ? '<span class="badge badge-success badge-flat-success text-xs">Active</span>' : '<span class="badge badge-flat-secondary text-xs">Inactive</span>'}</td>
22
+ <td>${editBtn('/review_template/'+t.id+'/edit')}</td>
23
+ </tr>`).join('');
24
+ return settingsPage(user, 'Templates - Settings', bc('Templates'), hdr('Templates', '/review_template/new', '+ Add Template') + inlineTable(['Name', 'Type', 'Status', 'Actions'], rows, 'No templates found'));
25
+ }
26
+
27
+ export function renderSettingsChecklists(user, checklists = []) {
28
+ const rows = checklists.map(c => `<tr ${trClick('/checklist/'+c.id)}>
29
+ <td class="text-sm font-medium">${esc(c.name||'-')}</td>
30
+ <td class="text-sm">${esc(c.type||'-')}</td>
31
+ <td class="text-sm text-base-content/70">${esc(c.review_id||'-')}</td>
32
+ <td>${editBtn('/checklist/'+c.id+'/edit')}</td>
33
+ </tr>`).join('');
34
+ return settingsPage(user, 'Checklists - Settings', bc('Checklists'), hdr('Checklists', '/checklist/new', '+ Add Checklist') + inlineTable(['Name', 'Type', 'Review', 'Actions'], rows, 'No checklists found'));
35
+ }
36
+
37
+ function renderTypeList(user, items, entityKey, title) {
38
+ const rows = items.map(t => `<tr class="hover">
39
+ <td class="font-medium text-sm">${esc(t.name || '-')}</td>
40
+ <td class="text-xs text-base-content/70">${t.created_at ? new Date(t.created_at).toLocaleDateString('en-ZA',{day:'2-digit',month:'short',year:'numeric'}) : '-'}</td>
41
+ <td><div class="flex gap-2">
42
+ <button type="button" class="btn btn-ghost btn-xs type-edit-btn" data-id="${esc(t.id)}" data-name="${esc(t.name||'')}">Edit</button>
43
+ <button type="button" class="btn btn-error btn-xs type-del-btn" data-id="${esc(t.id)}">Delete</button>
44
+ </div></td>
45
+ </tr>`).join('');
46
+ const script = `${TOAST_SCRIPT}
47
+ var _eid='';
48
+ var _typeFormTrigger=null;
49
+ function openAdd(){_eid='';_typeFormTrigger=document.activeElement;document.getElementById('type-name').value='';document.getElementById('type-form').style.display='block';var n=document.getElementById('type-name');if(n)n.focus()}
50
+ function openEdit(id,name){_eid=id;_typeFormTrigger=document.activeElement;document.getElementById('type-name').value=name;document.getElementById('type-form').style.display='block';var n=document.getElementById('type-name');if(n)n.focus()}
51
+ function cancelForm(){document.getElementById('type-form').style.display='none';if(_typeFormTrigger&&typeof _typeFormTrigger.focus==='function')_typeFormTrigger.focus();_typeFormTrigger=null}
52
+ async function saveType(){const name=document.getElementById('type-name').value.trim();if(!name){showToast('Name required','error');return}const url=_eid?'/api/${entityKey}/'+_eid:'/api/${entityKey}';const method=_eid?'PUT':'POST';try{const r=await fetch(url,{method,headers:{'Content-Type':'application/json'},body:JSON.stringify({name})});if(r.ok){showToast(_eid?'Updated':'Created','success');setTimeout(()=>location.reload(),400)}else showToast('Failed','error')}catch(e){showToast('Error','error')}}
53
+ async function delType(id){if(!(await window.gmConfirm({title:'Please confirm',message:'Delete?',danger:true,confirmLabel:'OK'})))return;try{const r=await fetch('/api/${entityKey}/'+id,{method:'DELETE'});if(r.ok){showToast('Deleted','success');setTimeout(()=>location.reload(),400)}else showToast('Failed','error')}catch(e){showToast('Error','error')}}
54
+ document.addEventListener('DOMContentLoaded',function(){document.querySelectorAll('.type-edit-btn').forEach(b=>b.addEventListener('click',function(){openEdit(this.dataset.id,this.dataset.name)}));document.querySelectorAll('.type-del-btn').forEach(b=>b.addEventListener('click',function(){delType(this.dataset.id)}));document.querySelectorAll('.type-add-btn').forEach(b=>b.addEventListener('click',openAdd))});`;
55
+ const formHtml = `<div id="type-form" class="card-clean mb-4" style="display:none"><div class="card-clean-body"><div class="flex flex-wrap gap-2 items-end"><div class="form-group" style="flex:1;min-width:0"><label class="label"><span class="label-text font-medium">Name</span></label><input id="type-name" type="text" class="input input-solid w-full" placeholder="Name"/></div><button data-action="saveType" class="btn btn-primary btn-sm">Save</button><button data-action="cancelForm" class="btn btn-ghost btn-sm">Cancel</button></div></div></div>`;
56
+ const content = `${settingsBack()}<div class="flex justify-between items-center mb-4"><h1 class="text-2xl font-bold">${title}</h1><button class="btn btn-primary btn-sm type-add-btn">Add</button></div>${formHtml}<div class="card-clean"><div class="card-clean-body" style="padding:0">${inlineTable(['Name','Created','Actions'],rows,'No items found.')}</div></div>`;
57
+ return settingsPage(user, `${title} - Settings`, bc(title), content, [script]);
58
+ }
59
+
60
+ export function renderSettingsEntityTypes(user, items = []) {
61
+ return renderTypeList(user, items, 'entity_type', 'Entity Types');
62
+ }
63
+
64
+ export function renderSettingsEngagementTypes(user, items = []) {
65
+ return renderTypeList(user, items, 'engagement_type', 'Engagement Types');
66
+ }
67
+
68
+ export function renderSettingsTemplateManage(user, template = {}, sections = []) {
69
+ const sectionRows = sections.map((s, i) => `<tr data-id="${esc(s.id)}">
70
+ <td><span class="inline-block w-5 h-5 rounded" style="background:${s.color || '#B0B0B0'}"></span></td>
71
+ <td class="text-sm font-medium">${esc(s.name || '-')}</td>
72
+ <td class="text-sm text-base-content/50">${s.order ?? i}</td>
73
+ <td><div class="flex gap-1">
74
+ <button data-action="editTplSection" data-args='["${esc(s.id)}","${esc((s.name||'').replace(/"/g,'&quot;'))}","${esc(s.color||'#B0B0B0')}"]' class="btn btn-ghost btn-xs">Edit</button>
75
+ <button data-action="deleteTplSection" data-args='["${esc(s.id)}"]' class="btn btn-error btn-xs btn-outline">Delete</button>
76
+ </div></td>
77
+ </tr>`).join('');
78
+ const tplBc = [{ href: '/', label: 'Dashboard' }, { href: '/admin/settings', label: 'Settings' }, { href: '/admin/settings/templates', label: 'Templates' }, { label: template.name || 'Template' }];
79
+ const content = `${settingsBack()}<div class="mb-6">
80
+ <h1 class="text-2xl font-bold">${esc(template.name || 'Template')}</h1>
81
+ <p class="text-sm text-base-content/50 mt-1">Manage template sections and configuration</p>
82
+ </div>
83
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
84
+ <div class="card-clean"><div class="card-clean-body">
85
+ <h2 class="card-title text-base mb-4">Template Info</h2>
86
+ <div class="form-group mb-3"><label class="label"><span class="label-text font-semibold">Name</span></label><input type="text" id="tpl-name" class="input input-solid max-w-full" value="${esc(template.name || '')}"/></div>
87
+ <div class="form-group mb-3"><label class="label"><span class="label-text font-semibold">Type</span></label><select id="tpl-type" class="select select-solid max-w-full"><option value="standard" ${template.type==='standard'?'selected':''}>Standard</option><option value="checklist" ${template.type==='checklist'?'selected':''}>Checklist</option><option value="audit" ${template.type==='audit'?'selected':''}>Audit</option></select></div>
88
+ <div class="form-group mb-4"><label class="label cursor-pointer justify-start gap-3"><input type="checkbox" id="tpl-active" class="checkbox checkbox-primary" ${template.is_active?'checked':''}/><span class="label-text">Active</span></label></div>
89
+ <button data-action="saveTplInfo" class="btn btn-primary btn-sm">Save Template Info</button>
90
+ </div></div>
91
+ <div class="card-clean"><div class="card-clean-body">
92
+ <div class="flex justify-between items-center mb-4">
93
+ <h2 class="card-title text-base">Sections</h2>
94
+ <button data-action="addTplSection" class="btn btn-primary btn-sm">+ Add Section</button>
95
+ </div>
96
+ ${inlineTable(['Color', 'Name', 'Order', 'Actions'], sectionRows, 'No sections defined')}
97
+ </div></div>
98
+ </div>
99
+ <div id="tpl-section-form" class="hidden card-clean" style="margin-top:1rem"><div class="card-clean-body">
100
+ <div class="flex flex-wrap gap-4 items-end">
101
+ <div class="form-group flex-1 min-w-40"><label class="label"><span class="label-text font-semibold">Name</span></label><input type="text" id="tpl-sec-name" class="input input-solid max-w-full" placeholder="Section name"/></div>
102
+ <div class="form-group"><label class="label"><span class="label-text font-semibold">Color</span></label><input type="color" id="tpl-sec-color" value="#B0B0B0" class="input input-solid" style="height:42px;width:60px;padding:4px"/></div>
103
+ <div class="flex gap-2"><button data-action="saveTplSection" class="btn btn-primary btn-sm">Save</button><button data-action="cancelTplSection" class="btn btn-ghost btn-sm">Cancel</button></div>
104
+ </div>
105
+ <input type="hidden" id="tpl-sec-id" value=""/>
106
+ </div></div>`;
107
+ const script = `${TOAST_SCRIPT}
108
+ var tplId='${esc(template.id || '')}';
109
+ window.saveTplInfo=async function(){var body={name:document.getElementById('tpl-name').value,type:document.getElementById('tpl-type').value,is_active:document.getElementById('tpl-active').checked?1:0};try{var res=await fetch('/api/review_template/'+tplId,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});if(res.ok){showToast('Template updated','success')}else{showToast('Update failed','error')}}catch(e){showToast('Error','error')}};
110
+ window.addTplSection=function(){document.getElementById('tpl-section-form').classList.remove('hidden');document.getElementById('tpl-sec-id').value='';document.getElementById('tpl-sec-name').value=''};
111
+ window.editTplSection=function(id,name,color){document.getElementById('tpl-section-form').classList.remove('hidden');document.getElementById('tpl-sec-id').value=id;document.getElementById('tpl-sec-name').value=name;document.getElementById('tpl-sec-color').value=color};
112
+ window.cancelTplSection=function(){document.getElementById('tpl-section-form').classList.add('hidden')};
113
+ window.saveTplSection=async function(){var id=document.getElementById('tpl-sec-id').value;var body={name:document.getElementById('tpl-sec-name').value,color:document.getElementById('tpl-sec-color').value,review_template_id:tplId};if(!body.name){showToast('Name required','error');return}var url=id?'/api/review_template_section/'+id:'/api/review_template_section';var method=id?'PUT':'POST';try{var res=await fetch(url,{method:method,headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});if(res.ok){showToast(id?'Updated':'Created','success');setTimeout(function(){location.reload()},500)}else{showToast('Failed','error')}}catch(e){showToast('Error','error')}};
114
+ window.deleteTplSection=async function(id){if(!(await window.gmConfirm({title:'Delete Section',message:'Delete this section? This cannot be undone.',confirmLabel:'Delete',danger:true})))return;try{var res=await fetch('/api/review_template_section/'+id,{method:'DELETE'});if(res.ok){showToast('Deleted','success');setTimeout(function(){location.reload()},500)}else{showToast('Delete failed','error')}}catch(e){showToast('Error','error')}};`;
115
+ return settingsPage(user, `Manage Template - ${esc(template.name || 'Template')}`, tplBc, content, [script]);
116
+ }
@@ -1,125 +0,0 @@
1
- import { normalizeError, createErrorLogger, retryWithBackoff } from '@/lib/error-handler';
2
- import { NextResponse } from '@/lib/next-polyfills';
3
- import { HTTP } from '@/config/constants';
4
-
5
- const logger = createErrorLogger('API');
6
-
7
- export function wrapAPIRoute(handler, options = {}) {
8
- const { retry = false, timeout = 30000, logErrors = true } = options;
9
-
10
- return async (request, context) => {
11
- const startTime = Date.now();
12
- const url = new URL(request.url);
13
-
14
- try {
15
- const operation = async () => {
16
- const timeoutPromise = new Promise((_, reject) =>
17
- setTimeout(() => reject(new Error(`Request timeout after ${timeout}ms`)), timeout)
18
- );
19
-
20
- const handlerPromise = handler(request, context);
21
-
22
- return await Promise.race([handlerPromise, timeoutPromise]);
23
- };
24
-
25
- const result = retry
26
- ? await retryWithBackoff(operation, { maxAttempts: 3, context: { url: url.pathname } })
27
- : await operation();
28
-
29
- const duration = Date.now() - startTime;
30
-
31
- if (duration > 1000) {
32
- logger.warn('Slow request', { url: url.pathname, duration });
33
- }
34
-
35
- return result;
36
- } catch (error) {
37
- const duration = Date.now() - startTime;
38
- const normalized = normalizeError(error);
39
-
40
- if (logErrors) {
41
- logger.error('Request failed', {
42
- url: url.pathname,
43
- method: request.method,
44
- error: normalized.toJSON(),
45
- duration
46
- });
47
- }
48
-
49
- return NextResponse.json(
50
- normalized.toJSON(),
51
- {
52
- status: normalized.statusCode,
53
- headers: { 'Content-Type': 'application/json' }
54
- }
55
- );
56
- }
57
- };
58
- }
59
-
60
- export function wrapGETRoute(handler, options = {}) {
61
- return wrapAPIRoute(handler, { ...options, retry: true });
62
- }
63
-
64
- export function wrapPOSTRoute(handler, options = {}) {
65
- return wrapAPIRoute(handler, options);
66
- }
67
-
68
- export function wrapPUTRoute(handler, options = {}) {
69
- return wrapAPIRoute(handler, options);
70
- }
71
-
72
- export function wrapDELETERoute(handler, options = {}) {
73
- return wrapAPIRoute(handler, options);
74
- }
75
-
76
- export function createAPIHandler(handlers) {
77
- const wrapped = {};
78
-
79
- if (handlers.GET) wrapped.GET = wrapGETRoute(handlers.GET);
80
- if (handlers.POST) wrapped.POST = wrapPOSTRoute(handlers.POST);
81
- if (handlers.PUT) wrapped.PUT = wrapPUTRoute(handlers.PUT);
82
- if (handlers.DELETE) wrapped.DELETE = wrapDELETERoute(handlers.DELETE);
83
- if (handlers.PATCH) wrapped.PATCH = wrapAPIRoute(handlers.PATCH);
84
- if (handlers.HEAD) wrapped.HEAD = wrapAPIRoute(handlers.HEAD);
85
-
86
- return wrapped;
87
- }
88
-
89
- export async function safeJSONParse(text, fallback = null) {
90
- try {
91
- return JSON.parse(text);
92
- } catch (error) {
93
- logger.warn('JSON parse failed', { error: error.message });
94
- return fallback;
95
- }
96
- }
97
-
98
- export async function safeReadBody(request, fallback = {}) {
99
- try {
100
- const text = await request.text();
101
- return text ? await safeJSONParse(text, fallback) : fallback;
102
- } catch (error) {
103
- logger.warn('Body read failed', { error: error.message });
104
- return fallback;
105
- }
106
- }
107
-
108
- export function validateRequired(data, fields) {
109
- const missing = [];
110
-
111
- for (const field of fields) {
112
- if (data[field] === undefined || data[field] === null || data[field] === '') {
113
- missing.push(field);
114
- }
115
- }
116
-
117
- if (missing.length > 0) {
118
- throw new Error(`Missing required fields: ${missing.join(', ')}`);
119
- }
120
- }
121
-
122
- export function sanitizeError(error) {
123
- const safe = String(error?.message || error || 'Unknown error');
124
- return safe.substring(0, 500);
125
- }
@@ -1,127 +0,0 @@
1
- import { recordDatabase } from '@/lib/metrics-collector.js'
2
-
3
- const dbStats = {
4
- connections: 0,
5
- activeQueries: 0,
6
- locks: 0,
7
- slowQueries: []
8
- }
9
-
10
- function wrapDatabase(db) {
11
- const originalPrepare = db.prepare.bind(db)
12
-
13
- db.prepare = function(sql) {
14
- const stmt = originalPrepare(sql)
15
- const originalRun = stmt.run.bind(stmt)
16
- const originalGet = stmt.get.bind(stmt)
17
- const originalAll = stmt.all.bind(stmt)
18
-
19
- stmt.run = function(...args) {
20
- const start = process.hrtime.bigint()
21
- dbStats.activeQueries++
22
-
23
- try {
24
- const result = originalRun(...args)
25
- const duration = Number(process.hrtime.bigint() - start) / 1000000
26
-
27
- recordDatabase('run', duration, sql)
28
- if (duration > 100) {
29
- recordSlowQuery(sql, duration)
30
- }
31
-
32
- return result
33
- } catch (err) {
34
- recordDatabase('error', 0, sql)
35
- throw err
36
- } finally {
37
- dbStats.activeQueries--
38
- }
39
- }
40
-
41
- stmt.get = function(...args) {
42
- const start = process.hrtime.bigint()
43
- dbStats.activeQueries++
44
-
45
- try {
46
- const result = originalGet(...args)
47
- const duration = Number(process.hrtime.bigint() - start) / 1000000
48
-
49
- recordDatabase('get', duration, sql)
50
- if (duration > 100) {
51
- recordSlowQuery(sql, duration)
52
- }
53
-
54
- return result
55
- } catch (err) {
56
- recordDatabase('error', 0, sql)
57
- throw err
58
- } finally {
59
- dbStats.activeQueries--
60
- }
61
- }
62
-
63
- stmt.all = function(...args) {
64
- const start = process.hrtime.bigint()
65
- dbStats.activeQueries++
66
-
67
- try {
68
- const result = originalAll(...args)
69
- const duration = Number(process.hrtime.bigint() - start) / 1000000
70
-
71
- recordDatabase('all', duration, sql)
72
- if (duration > 100) {
73
- recordSlowQuery(sql, duration)
74
- }
75
-
76
- return result
77
- } catch (err) {
78
- recordDatabase('error', 0, sql)
79
- throw err
80
- } finally {
81
- dbStats.activeQueries--
82
- }
83
- }
84
-
85
- return stmt
86
- }
87
-
88
- return db
89
- }
90
-
91
- function recordSlowQuery(sql, duration) {
92
- dbStats.slowQueries.push({
93
- sql: sql.substring(0, 200),
94
- duration,
95
- timestamp: Date.now()
96
- })
97
-
98
- if (dbStats.slowQueries.length > 100) {
99
- dbStats.slowQueries.shift()
100
- }
101
- }
102
-
103
- function getDatabaseStats() {
104
- return {
105
- connections: dbStats.connections,
106
- activeQueries: dbStats.activeQueries,
107
- locks: dbStats.locks,
108
- slowQueries: dbStats.slowQueries.slice(-10)
109
- }
110
- }
111
-
112
- function clearDatabaseStats() {
113
- dbStats.slowQueries.length = 0
114
- }
115
-
116
- export {
117
- wrapDatabase,
118
- getDatabaseStats,
119
- clearDatabaseStats
120
- }
121
-
122
- if (typeof globalThis !== 'undefined') {
123
- globalThis.__dbMonitor = {
124
- getDatabaseStats,
125
- clearDatabaseStats
126
- }
127
- }
@@ -1,132 +0,0 @@
1
- import { createLogger } from './logger.js';
2
- import { HTTP } from '@/config/constants';
3
-
4
- const log = createLogger('[Resilience]');
5
- import { AppError, normalizeError } from '@/lib/error-handler';
6
-
7
- const errorState = { errors: [], circuitBreakers: new Map(), checkpoints: new Map() };
8
-
9
- export async function retryWithBackoff(fn, options = {}) {
10
- const { maxAttempts = 3, delay = 1000, backoff = 2, context = {} } = options;
11
-
12
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
13
- try {
14
- return await fn();
15
- } catch (error) {
16
- if (attempt === maxAttempts) {
17
- const normalized = normalizeError(error);
18
- log.error('final attempt failed:', { ...context, attempt, error: normalized.toJSON() });
19
- throw normalized;
20
- }
21
-
22
- const waitTime = delay * Math.pow(backoff, attempt - 1);
23
- log.warn(`attempt ${attempt} failed, retrying in ${waitTime}ms`, context);
24
- await new Promise(resolve => setTimeout(resolve, waitTime));
25
- }
26
- }
27
- }
28
-
29
- export function createCircuitBreaker(name, options = {}) {
30
- const { threshold = 5, resetTimeout = 30000 } = options;
31
-
32
- if (!errorState.circuitBreakers.has(name)) {
33
- errorState.circuitBreakers.set(name, {
34
- failures: 0,
35
- state: 'closed',
36
- lastFailure: null,
37
- nextAttempt: null,
38
- threshold,
39
- resetTimeout
40
- });
41
- }
42
-
43
- return errorState.circuitBreakers.get(name);
44
- }
45
-
46
- export async function withCircuitBreaker(name, fn, options = {}) {
47
- const breaker = createCircuitBreaker(name, options);
48
-
49
- if (breaker.state === 'open') {
50
- const now = Date.now();
51
- if (breaker.nextAttempt && now < breaker.nextAttempt) {
52
- throw new AppError(`Service unavailable: ${name}`, 'CIRCUIT_OPEN', HTTP.SERVICE_UNAVAILABLE, { nextAttempt: breaker.nextAttempt });
53
- }
54
- breaker.state = 'half-open';
55
- }
56
-
57
- try {
58
- const result = await fn();
59
- if (breaker.state === 'half-open') {
60
- }
61
- breaker.failures = 0;
62
- breaker.state = 'closed';
63
- return result;
64
- } catch (error) {
65
- breaker.failures++;
66
- breaker.lastFailure = Date.now();
67
-
68
- if (breaker.failures >= breaker.threshold) {
69
- breaker.state = 'open';
70
- breaker.nextAttempt = Date.now() + breaker.resetTimeout;
71
- log.error(`circuit ${name} opened after ${breaker.failures} failures`);
72
- }
73
-
74
- throw error;
75
- }
76
- }
77
-
78
- export function checkpoint(name, state) {
79
- errorState.checkpoints.set(name, {
80
- state: JSON.parse(JSON.stringify(state)),
81
- timestamp: Date.now()
82
- });
83
- }
84
-
85
- export function restoreCheckpoint(name) {
86
- const cp = errorState.checkpoints.get(name);
87
- if (cp) {
88
- return cp.state;
89
- }
90
- return null;
91
- }
92
-
93
- export function logRecovery(context, action) {
94
- errorState.errors.push({
95
- type: 'recovery',
96
- context,
97
- action,
98
- timestamp: new Date().toISOString()
99
- });
100
- if (errorState.errors.length > 1000) errorState.errors.shift();
101
- }
102
-
103
- export function getErrorStats() {
104
- const recent = errorState.errors.slice(-100);
105
- const byType = {};
106
-
107
- for (const err of recent) {
108
- byType[err.type || 'error'] = (byType[err.type || 'error'] || 0) + 1;
109
- }
110
-
111
- return {
112
- total: errorState.errors.length,
113
- recent: recent.length,
114
- byType,
115
- circuitBreakers: Array.from(errorState.circuitBreakers.entries()).map(([name, state]) => ({
116
- name,
117
- state: state.state,
118
- failures: state.failures,
119
- lastFailure: state.lastFailure ? new Date(state.lastFailure).toISOString() : null
120
- })),
121
- checkpoints: Array.from(errorState.checkpoints.keys())
122
- };
123
- }
124
-
125
- if (typeof global !== 'undefined') {
126
- global.errorState = errorState;
127
- global.getErrorStats = getErrorStats;
128
- global.retryWithBackoff = retryWithBackoff;
129
- global.withCircuitBreaker = withCircuitBreaker;
130
- global.checkpoint = checkpoint;
131
- global.restoreCheckpoint = restoreCheckpoint;
132
- }
@@ -1,67 +0,0 @@
1
- import { startMonitoring as startResourceMonitoring } from '@/lib/resource-monitor.js'
2
- import { checkAllThresholds, registerAlertHandler } from '@/lib/alert-manager.js'
3
- import { getAllMetrics } from '@/lib/metrics-collector.js'
4
- import { info, warn, error } from '@/lib/log-aggregator.js'
5
- import path from 'path'
6
- import { fileURLToPath } from 'url'
7
-
8
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
- let alertCheckInterval = null
10
- let initialized = false
11
-
12
- function initializeMonitoring(config = {}) {
13
- if (initialized) {
14
- warn('Monitoring already initialized')
15
- return
16
- }
17
-
18
- const {
19
- resourceInterval = 5000,
20
- alertCheckInterval: alertInterval = 10000,
21
- dbPath = path.join(__dirname, '../../data/app.db')
22
- } = config
23
-
24
- startResourceMonitoring(resourceInterval, dbPath)
25
- info('Resource monitoring started', { interval: resourceInterval })
26
-
27
- alertCheckInterval = setInterval(() => {
28
- try {
29
- const metrics = getAllMetrics()
30
- checkAllThresholds(metrics)
31
- } catch (err) {
32
- error('Alert check failed', { error: err.message })
33
- }
34
- }, alertInterval)
35
-
36
- info('Alert checking started', { interval: alertInterval })
37
-
38
- registerAlertHandler((alert) => {
39
- if (alert.severity === 'critical') {
40
- error(`CRITICAL ALERT: ${alert.message}`, alert.metadata)
41
- } else if (alert.severity === 'warning') {
42
- warn(`WARNING: ${alert.message}`, alert.metadata)
43
- }
44
- })
45
-
46
- info('Alert handlers registered')
47
-
48
- initialized = true
49
- info('Monitoring system initialized')
50
- }
51
-
52
- async function shutdownMonitoring() {
53
- if (!initialized) return
54
-
55
- if (alertCheckInterval) {
56
- clearInterval(alertCheckInterval)
57
- alertCheckInterval = null
58
- }
59
-
60
- const { stopMonitoring } = await import('@/lib/resource-monitor.js')
61
- stopMonitoring()
62
-
63
- initialized = false
64
- info('Monitoring system shutdown')
65
- }
66
-
67
- export { initializeMonitoring, shutdownMonitoring }
@@ -1,94 +0,0 @@
1
- const MAX_BODY_SIZE = 10 * 1024 * 1024;
2
-
3
- // Wrap node's raw (plain-object, lowercased) headers so both the Fetch-style
4
- // `headers.get('x')` API and direct `headers['x']` index access work. Node
5
- // already lowercases header names; `.get()` lowercases its argument to match.
6
- function wrapHeaders(raw) {
7
- const h = raw || {};
8
- return new Proxy(h, {
9
- get(target, prop) {
10
- if (prop === 'get') return (name) => target[String(name).toLowerCase()] ?? null;
11
- if (prop === 'has') return (name) => String(name).toLowerCase() in target;
12
- return target[prop];
13
- },
14
- });
15
- }
16
-
17
- export class NextRequest {
18
- constructor(req, body, url) {
19
- this.method = req.method;
20
- this.headers = wrapHeaders(req.headers);
21
- this.url = url;
22
- this.body = body;
23
- }
24
-
25
- async json() {
26
- return this.body;
27
- }
28
-
29
- async text() {
30
- return typeof this.body === 'string' ? this.body : JSON.stringify(this.body);
31
- }
32
- }
33
-
34
- export class NextResponse {
35
- constructor(body, init = {}) {
36
- this.body = body;
37
- this.status = init.status || 200;
38
- this.headers = new Map(Object.entries(init.headers || {}));
39
- }
40
-
41
- async json() {
42
- return this.body;
43
- }
44
-
45
- static json(body, init = {}) {
46
- return new NextResponse(body, init);
47
- }
48
- }
49
-
50
- export async function readBody(req) {
51
- return new Promise((resolve, reject) => {
52
- let data = '';
53
- let size = 0;
54
- req.on('data', (chunk) => {
55
- size += chunk.length;
56
- if (size > MAX_BODY_SIZE) {
57
- req.destroy();
58
- reject(new Error('Request body too large'));
59
- return;
60
- }
61
- data += chunk;
62
- });
63
- req.on('end', () => {
64
- try {
65
- resolve(data ? JSON.parse(data) : {});
66
- } catch {
67
- resolve(data);
68
- }
69
- });
70
- req.on('error', (err) => reject(err));
71
- });
72
- }
73
-
74
- const HEADER_MAP = {
75
- 'content-type': 'Content-Type',
76
- 'content-length': 'Content-Length',
77
- 'set-cookie': 'Set-Cookie',
78
- 'cache-control': 'Cache-Control',
79
- 'expires': 'Expires',
80
- 'etag': 'ETag',
81
- 'last-modified': 'Last-Modified',
82
- 'location': 'Location',
83
- 'date': 'Date',
84
- 'connection': 'Connection',
85
- };
86
-
87
- export function normalizeHeaderName(key) {
88
- return HEADER_MAP[key.toLowerCase()] || key;
89
- }
90
-
91
- export function registerGlobals() {
92
- globalThis.NextRequest = NextRequest;
93
- globalThis.NextResponse = NextResponse;
94
- }