thatcher 1.0.7 → 1.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +4 -16
  2. package/package.json +12 -18
  3. package/src/adapters/google-auth.js +4 -43
  4. package/src/adapters/google-drive.js +12 -67
  5. package/src/app/api/audit/dashboard/route.js +14 -11
  6. package/src/app/api/audit/logs/route.js +2 -2
  7. package/src/app/api/audit/permissions/[id]/route.js +6 -3
  8. package/src/app/api/audit/permissions/route.js +10 -7
  9. package/src/app/api/audit/permissions/stats/route.js +8 -5
  10. package/src/app/api/audit/route.js +4 -4
  11. package/src/app/api/audit/stats/route.js +1 -1
  12. package/src/app/api/auth/google/callback/route.js +5 -2
  13. package/src/app/api/auth/google/route.js +4 -1
  14. package/src/app/api/auth/login/route.js +5 -2
  15. package/src/app/api/auth/mwr-bridge/route.js +11 -21
  16. package/src/app/api/auth/password-reset/route.js +15 -14
  17. package/src/app/api/cron/trigger/route.js +4 -1
  18. package/src/app/api/debug/[[...path]]/route.js +6 -5
  19. package/src/app/api/debug/sqlite/route.js +7 -12
  20. package/src/app/api/debug/sync/route.js +5 -5
  21. package/src/app/api/domains/route.js +4 -1
  22. package/src/app/api/email/allocate/batch/route.js +19 -29
  23. package/src/app/api/email/allocate/route.js +16 -17
  24. package/src/app/api/email/receive/route.js +31 -42
  25. package/src/app/api/email/send/route.js +28 -16
  26. package/src/app/api/email/unallocated/route.js +10 -13
  27. package/src/app/api/files/[id]/route.js +4 -1
  28. package/src/app/api/formula/[[...path]]/route.js +0 -4
  29. package/src/app/api/health/route.js +10 -9
  30. package/src/app/api/metrics/route.js +1 -1
  31. package/src/cli.js +4 -4
  32. package/src/config/config-loader.js +1 -21
  33. package/src/config/constants.js +0 -35
  34. package/src/config/env.js +0 -30
  35. package/src/config/spec-helpers.js +6 -95
  36. package/src/engine.js +23 -0
  37. package/src/engine.server.js +8 -78
  38. package/src/index.js +42 -67
  39. package/src/lib/alert-manager.js +7 -3
  40. package/src/lib/api-helpers.js +4 -3
  41. package/src/lib/api.js +10 -10
  42. package/src/lib/audit-logger-enhanced.js +77 -83
  43. package/src/lib/auth-middleware.js +1 -32
  44. package/src/lib/auth-route-helpers.js +5 -0
  45. package/src/lib/busybase-adapter.js +0 -5
  46. package/src/lib/busybase-audit-reads.js +142 -0
  47. package/src/lib/busybase-audit.js +41 -0
  48. package/src/lib/busybase-lucia-adapter.js +72 -0
  49. package/src/lib/busybase-store.js +338 -0
  50. package/src/lib/config-field-helpers.js +0 -17
  51. package/src/lib/config-generator-engine.js +19 -36
  52. package/src/lib/config-helpers.js +0 -21
  53. package/src/lib/crud-action-helpers.js +2 -2
  54. package/src/lib/crud-factory.js +1 -16
  55. package/src/lib/crud-handlers.js +17 -52
  56. package/src/lib/csrf-protection.js +5 -2
  57. package/src/lib/date-utils.js +3 -127
  58. package/src/lib/debug-registry.js +0 -5
  59. package/src/lib/domain-loader.js +4 -1
  60. package/src/lib/email-sender.js +25 -23
  61. package/src/lib/error-handler.js +5 -41
  62. package/src/lib/error-recovery.js +10 -7
  63. package/src/lib/error-resilience.js +7 -5
  64. package/src/lib/events-engine.js +9 -6
  65. package/src/lib/export-sink.js +0 -5
  66. package/src/lib/field-iterator.js +7 -76
  67. package/src/lib/field-registry.js +0 -22
  68. package/src/lib/generic-crud-handler.js +0 -16
  69. package/src/lib/hook-engine.js +5 -53
  70. package/src/lib/hot-reload/checkpoint.js +5 -2
  71. package/src/lib/hot-reload/promise-container.js +7 -3
  72. package/src/lib/hot-reload/route-wrapper.js +4 -1
  73. package/src/lib/hot-reload/supervisor.js +6 -3
  74. package/src/lib/hyperformula-service.js +0 -5
  75. package/src/lib/id-helpers.js +10 -0
  76. package/src/lib/index.js +4 -7
  77. package/src/lib/log-aggregator.js +0 -1
  78. package/src/lib/logger.js +0 -15
  79. package/src/lib/metrics-collector.js +2 -46
  80. package/src/lib/next-compat.js +15 -1
  81. package/src/lib/observability-bootstrap.js +0 -5
  82. package/src/lib/perf-monitor.js +4 -1
  83. package/src/lib/perf-profiler.js +0 -5
  84. package/src/lib/query-string-adapter.js +5 -47
  85. package/src/lib/realtime-server.js +4 -26
  86. package/src/lib/request-tracing.js +0 -5
  87. package/src/lib/resource-monitor.js +4 -1
  88. package/src/lib/response-formatter.js +0 -38
  89. package/src/lib/route-helpers.js +3 -1
  90. package/src/lib/safe-json.js +5 -1
  91. package/src/lib/server-bootstrap.js +10 -9
  92. package/src/lib/stage-pipeline.js +2 -2
  93. package/src/lib/state-transport-client.js +4 -1
  94. package/src/lib/static-server.js +2 -2
  95. package/src/lib/status-helpers.js +2 -51
  96. package/src/lib/tracing.js +0 -5
  97. package/src/lib/utils.js +6 -67
  98. package/src/lib/validate.js +5 -88
  99. package/src/lib/validators.js +0 -38
  100. package/src/lib/with-audit-logging.js +1 -1
  101. package/src/lib/workflow-engine.js +10 -77
  102. package/src/lib/xstate-workflow-engine.js +3 -9
  103. package/src/plugins/index.js +5 -3
  104. package/src/server/server.js +26 -27
  105. package/src/services/collaborator-role.service.js +21 -77
  106. package/src/services/email-sender.js +7 -34
  107. package/src/services/notification-engine.js +12 -33
  108. package/src/services/permission.service.js +10 -73
  109. package/src/ui/advanced-search-renderer.js +3 -2
  110. package/src/ui/advanced-widgets.js +1 -1
  111. package/src/ui/auth-pages.js +10 -9
  112. package/src/ui/auth-styles.js +1 -1
  113. package/src/ui/client.js +1 -1
  114. package/src/ui/component-engine.js +1 -1
  115. package/src/ui/dashboard-renderer.js +5 -5
  116. package/src/ui/dialog-engine.js +6 -9
  117. package/src/ui/dialog-factory.js +2 -2
  118. package/src/ui/engagement-cards.js +7 -7
  119. package/src/ui/engagement-dialogs.js +2 -3
  120. package/src/ui/engagement-grid-renderer.js +20 -19
  121. package/src/ui/entity-renderer.js +56 -42
  122. package/src/ui/file-dialogs.js +25 -9
  123. package/src/ui/flexup-report-renderer.js +21 -18
  124. package/src/ui/format-helpers.js +27 -28
  125. package/src/ui/highlight-threading-renderer.js +9 -9
  126. package/src/ui/job-management-renderer.js +18 -16
  127. package/src/ui/layout.js +51 -25
  128. package/src/ui/ml-console-renderer.js +15 -16
  129. package/src/ui/monitoring-dashboard-client.js +9 -8
  130. package/src/ui/notifications-renderer.js +11 -10
  131. package/src/ui/page-handler-admin.js +32 -23
  132. package/src/ui/page-handler-helpers.js +27 -23
  133. package/src/ui/page-handler-reviews.js +71 -35
  134. package/src/ui/page-handler-rfi.js +28 -14
  135. package/src/ui/page-handler.js +65 -48
  136. package/src/ui/picker-dialogs.js +26 -19
  137. package/src/ui/render-helpers.js +84 -46
  138. package/src/ui/review-comparison-renderer.js +8 -7
  139. package/src/ui/review-detail-panels.js +8 -9
  140. package/src/ui/review-detail-renderer.js +3 -3
  141. package/src/ui/review-detail-script.js +13 -12
  142. package/src/ui/review-mwr-renderer.js +6 -5
  143. package/src/ui/review-renderer.js +7 -6
  144. package/src/ui/review-widgets.js +3 -3
  145. package/src/ui/rfi-detail-renderer.js +37 -20
  146. package/src/ui/rfi-report-renderer.js +14 -9
  147. package/src/ui/settings-renderer-advanced.js +21 -18
  148. package/src/ui/settings-renderer-advanced2.js +20 -19
  149. package/src/ui/settings-renderer-teams.js +4 -4
  150. package/src/ui/settings-renderer.js +4 -4
  151. package/src/ui/spacing-system.js +5 -2
  152. package/src/ui/standalone-login.js +2 -2
  153. package/src/ui/styles.css +2 -1
  154. package/src/ui/styles2.css +75 -5
  155. package/src/ui/test-page.js +4 -4
  156. package/src/ui/widgets.js +20 -15
  157. package/src/lib/audit-logger.js +0 -193
  158. package/src/lib/database-core.js +0 -255
  159. package/src/lib/database-migrations.js +0 -350
  160. package/src/lib/db-backup.js +0 -97
  161. package/src/lib/error-boundary.js +0 -134
  162. package/src/lib/errors.js +0 -69
  163. package/src/lib/health-monitor.js +0 -134
  164. package/src/lib/index-optimizer.js +0 -96
  165. package/src/lib/query-engine-write.js +0 -221
  166. package/src/lib/query-engine.js +0 -446
  167. package/src/ui/collaboration-dialogs.js +0 -31
  168. package/src/ui/global-tags.js +0 -144
  169. package/src/ui/idle-logout.js +0 -156
  170. package/src/ui/letter-dialogs.js +0 -37
  171. package/src/ui/password-reset-page.js +0 -146
  172. package/src/ui/perf-renderer.js +0 -71
  173. package/src/ui/rfi-renderer.js +0 -194
  174. package/src/ui/validation-ui.js +0 -147
@@ -1,156 +0,0 @@
1
- export function getIdleLogoutScript(config = {}) {
2
- const idleTimeoutMs = config.idle_timeout_ms || 1800000;
3
- const idleWarningMs = config.idle_warning_ms || 1500000;
4
- const warningConfig = config.idle_warning_dialog_config || {};
5
-
6
- return `
7
- (function() {
8
- let idleTimer = null;
9
- let warningTimer = null;
10
- let isWarningShown = false;
11
-
12
- const CONFIG = {
13
- idleTimeoutMs: ${idleTimeoutMs},
14
- idleWarningMs: ${idleWarningMs},
15
- warningTitle: '${warningConfig.title || 'Session Expiring'}',
16
- warningMessage: '${warningConfig.message || 'Your session will expire due to inactivity'}',
17
- stayButtonLabel: '${warningConfig.buttons?.[0]?.label || 'Stay Logged In'}',
18
- logoutButtonLabel: '${warningConfig.buttons?.[1]?.label || 'Logout'}'
19
- };
20
-
21
- function resetIdleTimer() {
22
- clearTimeout(idleTimer);
23
- clearTimeout(warningTimer);
24
- isWarningShown = false;
25
- removeWarningDialog();
26
-
27
- warningTimer = setTimeout(() => {
28
- showWarningDialog();
29
- }, CONFIG.idleWarningMs);
30
-
31
- idleTimer = setTimeout(() => {
32
- logout();
33
- }, CONFIG.idleTimeoutMs);
34
- }
35
-
36
- function showWarningDialog() {
37
- if (isWarningShown) return;
38
- isWarningShown = true;
39
-
40
- const dialog = document.createElement('div');
41
- dialog.id = 'idle-warning-dialog';
42
- dialog.style.cssText = \`
43
- position: fixed;
44
- top: 0;
45
- left: 0;
46
- right: 0;
47
- bottom: 0;
48
- background: rgba(0,0,0,0.5);
49
- display: flex;
50
- align-items: center;
51
- justify-content: center;
52
- z-index: 10000;
53
- \`;
54
-
55
- const content = document.createElement('div');
56
- content.style.cssText = \`
57
- background: var(--color-surface, #fff);
58
- padding: 2rem;
59
- border-radius: 8px;
60
- box-shadow: 0 4px 16px rgba(0,0,0,0.2);
61
- max-width: 400px;
62
- text-align: center;
63
- color: var(--color-text, #111);
64
- \`;
65
-
66
- const title = document.createElement('h3');
67
- title.textContent = CONFIG.warningTitle;
68
- title.style.marginBottom = '1rem';
69
- title.style.color = 'var(--color-text, #333)';
70
-
71
- const message = document.createElement('p');
72
- message.textContent = CONFIG.warningMessage;
73
- message.style.marginBottom = '1.5rem';
74
- message.style.color = 'var(--color-text-muted, #666)';
75
-
76
- const buttonContainer = document.createElement('div');
77
- buttonContainer.style.cssText = 'display: flex; gap: 1rem;';
78
-
79
- const stayBtn = document.createElement('button');
80
- stayBtn.textContent = CONFIG.stayButtonLabel;
81
- stayBtn.style.cssText = \`
82
- flex: 1;
83
- padding: 0.75rem;
84
- background: #007bff;
85
- color: white;
86
- border: none;
87
- border-radius: 4px;
88
- cursor: pointer;
89
- font-weight: 600;
90
- \`;
91
- stayBtn.onclick = () => {
92
- removeWarningDialog();
93
- resetIdleTimer();
94
- };
95
-
96
- const logoutBtn = document.createElement('button');
97
- logoutBtn.textContent = CONFIG.logoutButtonLabel;
98
- logoutBtn.style.cssText = \`
99
- flex: 1;
100
- padding: 0.75rem;
101
- background: #6c757d;
102
- color: white;
103
- border: none;
104
- border-radius: 4px;
105
- cursor: pointer;
106
- font-weight: 600;
107
- \`;
108
- logoutBtn.onclick = logout;
109
-
110
- buttonContainer.appendChild(stayBtn);
111
- buttonContainer.appendChild(logoutBtn);
112
-
113
- content.appendChild(title);
114
- content.appendChild(message);
115
- content.appendChild(buttonContainer);
116
- dialog.appendChild(content);
117
- document.body.appendChild(dialog);
118
- }
119
-
120
- function removeWarningDialog() {
121
- const dialog = document.getElementById('idle-warning-dialog');
122
- if (dialog) dialog.remove();
123
- }
124
-
125
- function logout() {
126
- fetch('/api/auth/logout', { method: 'POST' })
127
- .then(() => {
128
- window.location.href = '/login';
129
- })
130
- .catch(() => {
131
- window.location.href = '/login';
132
- });
133
- }
134
-
135
- const events = ['mousedown', 'keydown', 'scroll', 'touchstart', 'click'];
136
-
137
- events.forEach(event => {
138
- document.addEventListener(event, resetIdleTimer, true);
139
- });
140
-
141
- resetIdleTimer();
142
-
143
- window.idleLogout = { resetIdleTimer, logout };
144
- })();
145
- `;
146
- }
147
-
148
- export function injectIdleLogoutScript(html, config = {}) {
149
- const script = getIdleLogoutScript(config);
150
- const tag = `<script>${script}</script>`;
151
-
152
- if (html.includes('</body>')) {
153
- return html.replace('</body>', `${tag}</body>`);
154
- }
155
- return html + tag;
156
- }
@@ -1,37 +0,0 @@
1
- export function engagementLetterDialog(engagementId) {
2
- return `<div id="eng-letter-dialog" class="dialog-overlay" style="display:none" data-dialog-close-overlay="true" onkeydown="if(event.key==='Escape')this.style.display='none'" role="dialog" aria-modal="true" aria-labelledby="eng-letter-dialog-title" aria-hidden="true">
3
- <div class="dialog-panel" style="max-width:640px">
4
- <div class="dialog-header"><span class="dialog-title" id="eng-letter-dialog-title">Engagement Letter</span><button class="dialog-close" data-dialog-close="eng-letter-dialog" aria-label="Close dialog">&times;</button></div>
5
- <div class="dialog-body">
6
- <div class="modal-form-group"><label for="eld-template">Template</label><select id="eld-template" class="select select-bordered w-full" onchange="eldLoadTemplate()"><option value="">Select template...</option></select></div>
7
- <div class="modal-form-group"><label for="eld-date">Letter Date</label><input type="date" id="eld-date" class="input input-bordered w-full"/></div>
8
- <div class="modal-form-group"><label for="eld-recipient">Recipient Name</label><input id="eld-recipient" class="input input-bordered w-full" placeholder="Client contact name"/></div>
9
- <div class="modal-form-group"><label for="eld-content">Content</label><textarea id="eld-content" class="textarea textarea-bordered w-full" rows="8" placeholder="Letter content..."></textarea></div>
10
- </div>
11
- <div class="dialog-footer"><button class="btn btn-ghost btn-sm" data-dialog-close="eng-letter-dialog">Cancel</button><button class="btn btn-outline btn-sm" data-action="eldPreview">Preview</button><button class="btn btn-primary btn-sm" data-action="eldSave">Save Letter</button></div>
12
- </div></div>
13
- <script>
14
- (function(){fetch('/api/letter_template').then(function(r){return r.json()}).then(function(d){var sel=document.getElementById('eld-template');(d.data||d||[]).forEach(function(t){var o=document.createElement('option');o.value=t.id;o.textContent=t.name||t.id;sel.appendChild(o)})}).catch(function(){})})();
15
- window.openEngLetter=function(){document.getElementById('eng-letter-dialog').style.display='flex'};
16
- window.eldLoadTemplate=function(){var id=document.getElementById('eld-template').value;if(!id)return;fetch('/api/letter_template/'+id).then(function(r){return r.json()}).then(function(d){var t=d.data||d;document.getElementById('eld-content').value=t.content||t.body||''}).catch(function(){})};
17
- window.eldPreview=function(){var content=document.getElementById('eld-content').value;var w=window.open('','_blank','width=800,height=600');w.document.write('<html><body style="padding:2rem;font-family:serif;max-width:700px;margin:auto"><h2>Engagement Letter</h2><p>Date: '+(document.getElementById('eld-date').value||'-')+'</p><p>To: '+(document.getElementById('eld-recipient').value||'-')+'</p><hr/><div style="white-space:pre-wrap">'+content+'</div></body></html>');w.document.close()};
18
- window.eldSave=async function(){var body={engagement_id:'${engagementId}',template_id:document.getElementById('eld-template').value||null,date:document.getElementById('eld-date').value||null,recipient:document.getElementById('eld-recipient').value||null,content:document.getElementById('eld-content').value};try{var r=await fetch('/api/letter',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});if(r.ok){showToast('Letter saved','success');document.getElementById('eng-letter-dialog').style.display='none';location.reload()}else showToast('Failed','error')}catch(e){showToast('Error','error')}};
19
- </script>`;
20
- }
21
-
22
- export function engagementLetterTemplateUpload() {
23
- return `<div id="letter-tpl-upload" class="dialog-overlay" style="display:none" data-dialog-close-overlay="true" onkeydown="if(event.key==='Escape')this.style.display='none'" role="dialog" aria-modal="true" aria-labelledby="letter-tpl-upload-title" aria-hidden="true">
24
- <div class="dialog-panel">
25
- <div class="dialog-header"><span class="dialog-title" id="letter-tpl-upload-title">Upload Letter Template</span><button class="dialog-close" data-dialog-close="letter-tpl-upload" aria-label="Close dialog">&times;</button></div>
26
- <div class="dialog-body">
27
- <div class="modal-form-group"><label for="ltu-name">Template Name</label><input id="ltu-name" class="input input-bordered w-full" placeholder="Template name"/></div>
28
- <div class="modal-form-group"><label for="ltu-file">Template File</label><input type="file" id="ltu-file" class="file-input file-input-bordered w-full" accept=".docx,.doc,.html,.txt"/></div>
29
- <div class="modal-form-group"><label for="ltu-content">Or paste content</label><textarea id="ltu-content" class="textarea textarea-bordered w-full" rows="5" placeholder="Template content..."></textarea></div>
30
- </div>
31
- <div class="dialog-footer"><button class="btn btn-ghost btn-sm" data-dialog-close="letter-tpl-upload">Cancel</button><button class="btn btn-primary btn-sm" data-action="ltuSave">Save Template</button></div>
32
- </div></div>
33
- <script>
34
- window.openLetterTemplateUpload=function(){document.getElementById('letter-tpl-upload').style.display='flex'};
35
- window.ltuSave=async function(){var name=document.getElementById('ltu-name').value.trim();if(!name){showToast('Name required','error');return}var file=document.getElementById('ltu-file').files[0];var content=document.getElementById('ltu-content').value;if(file){var fd=new FormData();fd.append('file',file);fd.append('name',name);try{var r=await fetch('/api/letter_template/upload',{method:'POST',body:fd});if(r.ok){showToast('Template uploaded','success');document.getElementById('letter-tpl-upload').style.display='none';location.reload()}else showToast('Upload failed','error')}catch(e){showToast('Error','error')}}else if(content){try{var r=await fetch('/api/letter_template',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:name,content:content})});if(r.ok){showToast('Template saved','success');document.getElementById('letter-tpl-upload').style.display='none';location.reload()}else showToast('Failed','error')}catch(e){showToast('Error','error')}}else{showToast('Provide file or content','error')}};
36
- </script>`;
37
- }
@@ -1,146 +0,0 @@
1
- export function renderPasswordResetPage(params = {}) {
2
- const token = params.token || '';
3
- const isConfirm = !!token;
4
- const title = isConfirm ? 'Set New Password' : 'Forgot Password';
5
- const subtitle = isConfirm ? 'Enter your new password below' : 'Enter your email to receive a reset link';
6
- const btnLabel = isConfirm ? 'Update Password' : 'Send Reset Link';
7
-
8
- return `<!DOCTYPE html>
9
- <html lang="en">
10
- <head>
11
- <meta charset="UTF-8">
12
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
13
- <title>${title} | Moonlanding</title>
14
- <style>
15
- *{margin:0;padding:0;box-sizing:border-box}
16
- :root{--bg:#f1f5f9;--surface:#fff;--border:#e2e8f0;--text:#0f172a;--text-muted:#64748b;--primary:#04141f;--accent:#2563eb;--danger:#b91c1c;--danger-bg:#fee2e2;--danger-border:#fca5a5;--success-bg:#dcfce7;--success:#15803d;--success-border:#86efac;--info-bg:#dbeafe;--info:#1d4ed8;--info-border:#93c5fd}
17
- @media(prefers-color-scheme:dark){:root{--bg:#0f172a;--surface:#1a1a1a;--border:#334155;--text:#f1f5f9;--text-muted:#94a3b8;--primary:#1e3a5f}}
18
- body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px;color:var(--text)}
19
- .wrap{width:100%;max-width:400px}
20
- .logo{text-align:center;margin-bottom:32px}
21
- .logo-mark{display:inline-flex;align-items:center;justify-content:center;width:52px;height:52px;background:var(--primary);border-radius:14px;margin-bottom:14px;box-shadow:0 4px 12px rgba(4,20,31,0.25)}
22
- .logo-name{font-size:1.35rem;font-weight:800;color:var(--text);letter-spacing:0.5px;display:block;margin-bottom:4px}
23
- .logo-sub{color:var(--text-muted);font-size:0.875rem}
24
- .card{background:var(--surface);border-radius:10px;box-shadow:0 4px 24px rgba(0,0,0,0.09);padding:32px;border:1px solid var(--border)}
25
- .field{margin-bottom:18px}
26
- label{display:block;font-size:0.8125rem;font-weight:600;color:var(--text);margin-bottom:6px}
27
- input{width:100%;padding:10px 13px;border:1.5px solid var(--border);border-radius:8px;font-size:0.9rem;outline:none;transition:border-color 0.15s,box-shadow 0.15s;background:var(--surface);color:var(--text);min-height:44px}
28
- input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(37,99,235,0.12)}
29
- input::placeholder{color:var(--text-muted)}
30
- .submit-btn{width:100%;padding:12px;background:var(--primary);color:#fff;border:none;border-radius:8px;font-size:0.9375rem;font-weight:600;cursor:pointer;transition:all 0.15s;min-height:46px;margin-top:4px}
31
- .submit-btn:hover:not(:disabled){background:#0a2535}
32
- .submit-btn:disabled{opacity:0.65;cursor:not-allowed}
33
- .submit-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
34
- .alert{padding:11px 14px;border-radius:8px;margin-bottom:18px;font-size:0.875rem;border:1px solid;line-height:1.4}
35
- .alert-error{background:var(--danger-bg);color:var(--danger);border-color:var(--danger-border)}
36
- .alert-success{background:var(--success-bg);color:var(--success);border-color:var(--success-border)}
37
- .alert-info{background:var(--info-bg);color:var(--info);border-color:var(--info-border)}
38
- .back{text-align:center;margin-top:18px;font-size:0.875rem;color:var(--text-muted)}
39
- .back a{color:var(--accent);text-decoration:none;font-weight:500}
40
- .back a:hover{text-decoration:underline}
41
- @media(prefers-color-scheme:dark){
42
- .card{background:#1a1a1a;border-color:#334155;box-shadow:0 4px 24px rgba(0,0,0,0.4)}
43
- input{background:#1a1a1a;border-color:#334155;color:#f1f5f9}
44
- .submit-btn{background:#1e3a5f}
45
- .submit-btn:hover:not(:disabled){background:#1e293b}
46
- }
47
- </style>
48
- </head>
49
- <body>
50
- <div class="wrap">
51
- <div class="logo">
52
- <div class="logo-mark">
53
- <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M12 1v6m0 6v6M4.22 4.22l4.24 4.24m4.24 4.24l4.24 4.24M1 12h6m6 0h6M4.22 19.78l4.24-4.24m4.24-4.24l4.24-4.24"/></svg>
54
- </div>
55
- <span class="logo-name">MOONLANDING</span>
56
- <div class="logo-sub">${subtitle}</div>
57
- </div>
58
- <div class="card">
59
- <div id="alertContainer" role="alert" aria-live="assertive"></div>
60
- <form id="resetForm" aria-label="${title}" novalidate>
61
- ${!isConfirm ? `
62
- <div class="field">
63
- <label for="email">Email address</label>
64
- <input type="email" name="email" id="email" placeholder="you@example.com" required autocomplete="email" autofocus>
65
- </div>
66
- <button type="submit" id="submitBtn" class="submit-btn">${btnLabel}</button>
67
- ` : `
68
- <input type="hidden" name="token" value="${token}">
69
- <div class="field">
70
- <label for="password">New password</label>
71
- <input type="password" name="password" id="password" placeholder="At least 8 characters" required minlength="8" autocomplete="new-password" autofocus>
72
- </div>
73
- <div class="field">
74
- <label for="confirm">Confirm new password</label>
75
- <input type="password" name="confirm" id="confirm" placeholder="Same password again" required minlength="8" autocomplete="new-password">
76
- </div>
77
- <button type="submit" id="submitBtn" class="submit-btn">${btnLabel}</button>
78
- `}
79
- </form>
80
- <div class="back"><a href="/login">Back to sign in</a></div>
81
- </div>
82
- </div>
83
- <script src="/ui/event-delegation.js"></script>
84
- <script>
85
- var form = document.getElementById('resetForm');
86
- var submitBtn = document.getElementById('submitBtn');
87
- var alertContainer = document.getElementById('alertContainer');
88
- var isConfirm = ${isConfirm};
89
-
90
- function showAlert(message, type) {
91
- alertContainer.innerHTML = '<div class="alert alert-' + (type || 'error') + '">' + message + '</div>';
92
- }
93
-
94
- if (isConfirm && !form.token.value) {
95
- showAlert('Invalid or expired reset link. Please request a new one.', 'error');
96
- submitBtn.disabled = true;
97
- }
98
-
99
- form.addEventListener('submit', async function(e) {
100
- e.preventDefault();
101
- alertContainer.innerHTML = '';
102
- var formData = new FormData(form);
103
- var data = Object.fromEntries(formData);
104
-
105
- if (isConfirm && data.password !== data.confirm) {
106
- showAlert('Passwords do not match.', 'error');
107
- return;
108
- }
109
-
110
- var origText = submitBtn.textContent;
111
- submitBtn.textContent = isConfirm ? 'Updating\u2026' : 'Sending\u2026';
112
- submitBtn.disabled = true;
113
-
114
- try {
115
- var body = isConfirm ? { token: data.token, password: data.password } : { email: data.email };
116
- var res = await fetch('/api/auth/password-reset', {
117
- method: isConfirm ? 'PUT' : 'POST',
118
- headers: { 'Content-Type': 'application/json' },
119
- body: JSON.stringify(body)
120
- });
121
- var result = await res.json().catch(function() { return {}; });
122
- if (res.ok) {
123
- if (isConfirm) {
124
- showAlert('Password updated. <a href="/login" style="color:var(--success);font-weight:600;text-decoration:underline">Sign in now</a>', 'success');
125
- form.style.display = 'none';
126
- } else {
127
- form.reset();
128
- showAlert('If an account exists with that email, a reset link has been sent.', 'info');
129
- submitBtn.textContent = origText;
130
- submitBtn.disabled = false;
131
- }
132
- } else {
133
- showAlert(result.error || 'An error occurred. Please try again.', 'error');
134
- submitBtn.textContent = origText;
135
- submitBtn.disabled = false;
136
- }
137
- } catch(err) {
138
- showAlert('Network error. Please try again.', 'error');
139
- submitBtn.textContent = origText;
140
- submitBtn.disabled = false;
141
- }
142
- });
143
- </script>
144
- </body>
145
- </html>`;
146
- }
@@ -1,71 +0,0 @@
1
- import { memoize } from '@/lib/render-cache.js'
2
- import { measure } from '@/lib/perf-monitor.js'
3
- import { virtualScrollScript, lazyLoadImages, deferOffscreen } from '@/ui/virtual-scroll.js'
4
- import { eventDelegation, perfBudget, createSkeleton } from '@/ui/perf-helpers.js'
5
-
6
- export function optimizedListRender(items, renderFn, options = {}) {
7
- const { chunkSize = 100, containerId = 'list-container', enableVirtual = true } = options
8
- return measure('list-render', () => {
9
- if (!items || items.length === 0) return createSkeleton('list', 3)
10
- if (items.length <= chunkSize) return items.map(renderFn).join('')
11
- const visible = items.slice(0, chunkSize)
12
- const deferred = items.slice(chunkSize)
13
- return visible.map(renderFn).join('') +
14
- (enableVirtual && deferred.length > 0
15
- ? `<div class="defer-load" data-deferred="${deferred.length}"></div>`
16
- : deferred.map(renderFn).join(''))
17
- })
18
- }
19
-
20
- export function withPerformanceMonitoring(renderFn, name) {
21
- return function(...args) {
22
- return measure(name, () => renderFn(...args))
23
- }
24
- }
25
-
26
- export function withMemoization(renderFn) {
27
- return memoize(renderFn)
28
- }
29
-
30
- export function optimizedTableScripts(tableId, options = {}) {
31
- const { enableVirtual = false, enableDefer = true, rowHeight = 48, perfBudgetMs = 200 } = options
32
- const scripts = []
33
- if (enableVirtual) scripts.push(virtualScrollScript(tableId, rowHeight))
34
- if (enableDefer) scripts.push(deferOffscreen('.defer-load'))
35
- scripts.push(perfBudget(`${tableId}-render`, perfBudgetMs))
36
- scripts.push(lazyLoadImages())
37
- return scripts.filter(Boolean)
38
- }
39
-
40
- export function batchOperations(operations, batchSize = 10) {
41
- const batches = []
42
- for (let i = 0; i < operations.length; i += batchSize) {
43
- batches.push(operations.slice(i, i + batchSize))
44
- }
45
- return batches.map((batch, idx) => {
46
- if (idx === 0) return batch
47
- return { deferred: true, operations: batch }
48
- })
49
- }
50
-
51
- export function progressiveLoad(content, priority = 'high') {
52
- if (priority === 'high') return content
53
- return `<div class="defer-load" data-content="${escapeAttr(content)}"></div>`
54
- }
55
-
56
- function escapeAttr(str) {
57
- return String(str).replace(/"/g, '&quot;').replace(/'/g, '&#39;')
58
- }
59
-
60
- export const perfConfig = {
61
- tableChunkSize: 100,
62
- cardChunkSize: 50,
63
- virtualScrollThreshold: 200,
64
- deferLoadThreshold: 100,
65
- perfBudgets: {
66
- 'list-render': 100,
67
- 'table-render': 150,
68
- 'card-render': 80,
69
- 'page-render': 500
70
- }
71
- }