thatcher 1.0.44 → 1.0.46

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 (82) hide show
  1. package/README.md +44 -30
  2. package/package.json +1 -1
  3. package/src/adapters/google-auth.js +0 -7
  4. package/src/adapters/google-drive.js +1 -3
  5. package/src/adapters/google-gmail.js +1 -0
  6. package/src/app/api/[entity]/[[...path]]/route.js +1 -1
  7. package/src/app/api/audit/logs/route.js +1 -1
  8. package/src/app/api/audit/permissions/route.js +0 -1
  9. package/src/app/api/audit/route.js +2 -4
  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/logout/route.js +1 -1
  13. package/src/app/api/csrf-token/route.js +1 -1
  14. package/src/app/api/domains/route.js +1 -1
  15. package/src/app/api/email/allocate/route.js +0 -1
  16. package/src/app/api/email/receive/route.js +1 -1
  17. package/src/app/api/files/[id]/route.js +1 -1
  18. package/src/app/api/health/route.js +1 -1
  19. package/src/cli.js +9 -4
  20. package/src/config/spec-helpers.js +0 -1
  21. package/src/engine.server.js +1 -1
  22. package/src/lib/alert-manager.js +1 -1
  23. package/src/lib/api-helpers.js +1 -1
  24. package/src/lib/auth-route-helpers.js +1 -1
  25. package/src/lib/business-rules-engine.js +3 -3
  26. package/src/lib/busybase/adapter.js +2 -2
  27. package/src/lib/busybase/store.js +1 -1
  28. package/src/lib/config-generator-engine.js +0 -2
  29. package/src/lib/crud-action-helpers.js +1 -1
  30. package/src/lib/crud-handlers.js +2 -2
  31. package/src/lib/csrf-protection.js +0 -1
  32. package/src/lib/debug-registry.js +1 -1
  33. package/src/lib/errors/recovery.js +0 -8
  34. package/src/lib/errors/wrap.js +1 -1
  35. package/src/lib/events-engine.js +1 -1
  36. package/src/lib/field-iterator.js +2 -2
  37. package/src/lib/field-registry.js +2 -1
  38. package/src/lib/hot-reload/index.js +2 -1
  39. package/src/lib/minifier.js +1 -1
  40. package/src/lib/next-shim.js +2 -2
  41. package/src/lib/perf.js +0 -2
  42. package/src/lib/request-trace.js +1 -4
  43. package/src/lib/route-resolver.js +1 -1
  44. package/src/lib/stage-pipeline.js +0 -1
  45. package/src/lib/static-server.js +1 -1
  46. package/src/lib/tracing.js +0 -3
  47. package/src/lib/validation/security-validators.js +2 -2
  48. package/src/lib/validation-middleware.js +1 -1
  49. package/src/lib/workflow-engine.js +1 -1
  50. package/src/lib/xstate-workflow-engine.js +1 -1
  51. package/src/server/server.js +5 -9
  52. package/src/services/collaborator-role.service.js +1 -3
  53. package/src/services/notification-engine.js +3 -3
  54. package/src/services/permission.service.js +1 -1
  55. package/src/ui/advanced-widgets.js +0 -3
  56. package/src/ui/auth-pages.js +1 -1
  57. package/src/ui/common-handlers.js +2 -2
  58. package/src/ui/component-engine.js +2 -2
  59. package/src/ui/dashboard-renderer.js +1 -1
  60. package/src/ui/dialog-engine.js +11 -9
  61. package/src/ui/engagement-cards.js +0 -1
  62. package/src/ui/engagement-dialogs.js +0 -2
  63. package/src/ui/entity-renderer.js +2 -3
  64. package/src/ui/event-delegation.js +1 -1
  65. package/src/ui/file-dialogs.js +0 -2
  66. package/src/ui/job-management-renderer.js +0 -1
  67. package/src/ui/layout.js +4 -4
  68. package/src/ui/page-handler-admin.js +2 -2
  69. package/src/ui/page-handler-helpers.js +3 -3
  70. package/src/ui/page-handler-rfi.js +1 -1
  71. package/src/ui/page-handler.js +1 -1
  72. package/src/ui/permissions-ui.js +0 -12
  73. package/src/ui/picker-dialogs.js +0 -1
  74. package/src/ui/review/comparison.js +1 -1
  75. package/src/ui/review/detail-renderer.js +1 -1
  76. package/src/ui/review/mwr.js +1 -1
  77. package/src/ui/review/widgets.js +0 -1
  78. package/src/ui/rfi-detail-renderer.js +1 -1
  79. package/src/ui/settings/home.js +1 -1
  80. package/src/ui/settings/review.js +0 -1
  81. package/src/ui/settings/teams.js +1 -1
  82. package/src/ui/spacing-system.js +1 -1
@@ -27,7 +27,7 @@ export function resolveSpecificParams(routeFile, pathParts) {
27
27
  for (let i = 0; i < routeSegments.length && i < urlSegments.length; i++) {
28
28
  const seg = routeSegments[i];
29
29
  if (seg.startsWith('[') && seg.endsWith(']')) {
30
- const paramName = seg.replace(/^\[\.\.\./, '').replace(/[\[\]]/g, '');
30
+ const paramName = seg.replace(/^\[\.\.\./, '').replace(/[[\]]/g, '');
31
31
  result[paramName] = urlSegments[i];
32
32
  }
33
33
  }
@@ -21,7 +21,6 @@ export function createStagePipeline(options = {}) {
21
21
  stages.forEach((stage, idx) => {
22
22
  const isComplete = idx < currentStageIndex;
23
23
  const isCurrent = idx === currentStageIndex;
24
- const isUpcoming = idx > currentStageIndex;
25
24
 
26
25
  const stageItem = document.createElement('div');
27
26
  stageItem.className = `stage-item stage-${isComplete ? 'complete' : isCurrent ? 'current' : 'upcoming'}`;
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
5
5
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
6
  const ROOT = path.join(__dirname, '../..');
7
7
 
8
- export function serveStatic(pathname, req, res, compress, getCacheHeaders, loadModule) {
8
+ export function serveStatic(pathname, req, res, compress, getCacheHeaders, _loadModule) {
9
9
  const acceptEncoding = req.headers['accept-encoding'] || '';
10
10
 
11
11
  if (pathname === '/favicon.ico') {
@@ -1,7 +1,4 @@
1
1
  import { AsyncLocalStorage } from 'async_hooks';
2
- import { createLogger } from './logger.js';
3
-
4
- const logger = createLogger('[Tracing]');
5
2
 
6
3
  const _asyncLocalStorage = new AsyncLocalStorage();
7
4
  const _traceBuffer = [];
@@ -12,7 +12,7 @@ const XSS_PATTERNS = [
12
12
  ];
13
13
 
14
14
  const SQL_INJECTION_PATTERNS = [
15
- /('|(\-\-)|(;)|(\|\|)|(\/\*)|(\*\/)|xp_)/gi,
15
+ /('|(--)|(;)|(\|\|)|(\/\*)|(\*\/)|xp_)/gi,
16
16
  /(union|select|insert|update|delete|drop|create|alter|exec|execute)\s/gi,
17
17
  /0x[0-9a-f]+/gi,
18
18
  /char\(/gi,
@@ -20,7 +20,7 @@ const SQL_INJECTION_PATTERNS = [
20
20
  ];
21
21
 
22
22
  const PATH_TRAVERSAL_PATTERNS = [
23
- /\.\.[\\\/]/g,
23
+ /\.\.[\\/]/g,
24
24
  /\.\.%/g,
25
25
  /%2e%2e/gi,
26
26
  /\.\.\./g
@@ -10,7 +10,7 @@ import {
10
10
  import { validateEntity, validateUpdate, hasErrors } from '@/lib/validation/entity-validators';
11
11
 
12
12
  export async function validateRequest(request, options = {}) {
13
- const { requireCSRF = true, rateLimit = { max: 100, window: 60000 }, entityName = null } = options;
13
+ const { requireCSRF = true, rateLimit = { max: 100, window: 60000 } } = options;
14
14
 
15
15
  const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown';
16
16
  const sessionId = request.headers.get('cookie')?.match(/session=([^;]+)/)?.[1] || ip;
@@ -18,7 +18,7 @@
18
18
  // does NOT currently supersede this file; if anything the reverse holds (this file
19
19
  // is live, that one is dormant/debug-only). Re-check import sites before treating
20
20
  // either as the "old" one.
21
- import { get, update, create } from './busybase/store.js';
21
+ import { get } from './busybase/store.js';
22
22
  import { getConfigEngineSync } from './config-generator-engine.js';
23
23
  import { executeHook } from './hook-engine.js';
24
24
  import { AppError } from './errors/index.js';
@@ -14,7 +14,7 @@
14
14
  // src/lib/events-engine.js and re-exported by src/index.js's Thatcher class).
15
15
  // This file does NOT supersede workflow-engine.js; treat it as a dormant/
16
16
  // debug-only alternate implementation until a real call site adopts it.
17
- import { createMachine, createActor, assign, fromPromise, sendParent } from 'xstate';
17
+ import { createMachine, createActor, assign } from 'xstate';
18
18
  import { getConfigEngineSync } from './config-generator-engine.js';
19
19
  import { hookEngine } from './hook-engine.js';
20
20
  import { AppError } from './errors/index.js';
@@ -1,9 +1,8 @@
1
1
  import http from 'http';
2
2
  import path from 'path';
3
3
  import fs from 'fs';
4
- import { fileURLToPath, pathToFileURL } from 'url';
4
+ import { fileURLToPath } from 'url';
5
5
  import { createLogger } from '../lib/logger.js';
6
- import * as thatcherLib from '../index.js';
7
6
 
8
7
  const log = createLogger('[Server]');
9
8
  const pageLog = createLogger('[Page]');
@@ -13,8 +12,7 @@ const apiLog = createLogger('[API]');
13
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
13
 
15
14
  export function createServer(options) {
16
- const { thatcher, config, configEngine, port, host } = options;
17
- const PORT = port || 3000;
15
+ const { thatcher, configEngine } = options;
18
16
  const SERVER_START = Date.now();
19
17
 
20
18
  let systemInitialized = false;
@@ -50,7 +48,6 @@ export function createServer(options) {
50
48
  };
51
49
 
52
50
  const server = http.createServer(async (req, res) => {
53
- const t0 = Date.now();
54
51
  globalThis.__debug__.activeRequests.count++;
55
52
 
56
53
  try {
@@ -98,7 +95,7 @@ export function createServer(options) {
98
95
  }
99
96
 
100
97
  // Static file serving (simplified)
101
- if (serveStaticFile(pathname, req, res, load)) {
98
+ if (serveStaticFile(pathname, req, res)) {
102
99
  return;
103
100
  }
104
101
 
@@ -135,7 +132,7 @@ export function createServer(options) {
135
132
  return server;
136
133
  }
137
134
 
138
- async function serveStaticFile(pathname, req, res, load) {
135
+ async function serveStaticFile(pathname, req, res) {
139
136
  if (pathname === '/' || pathname === '/index.html') {
140
137
  // Could serve SPA
141
138
  return false;
@@ -220,9 +217,8 @@ async function handleGenericCrud(req, res, entity, id, action, thatcher, configE
220
217
  const { getConfigEngineSync } = await import('../lib/config-generator-engine.js');
221
218
  configEngine = getConfigEngineSync();
222
219
  }
223
- let spec;
224
220
  try {
225
- spec = configEngine.generateEntitySpec(entity);
221
+ configEngine.generateEntitySpec(entity);
226
222
  } catch (e) {
227
223
  res.writeHead(404);
228
224
  res.end(JSON.stringify({ error: `Entity '${entity}' not found` }));
@@ -1,8 +1,6 @@
1
1
  // Extracted from moonlanding/src/services/collaborator-role.service.js
2
2
 
3
3
  import { list, get, update, create } from '../lib/busybase/store.js';
4
- import { AppError } from '../lib/errors/index.js';
5
- import { HTTP } from '../config/constants.js';
6
4
 
7
5
  const COLLABORATOR_ROLE_PERMISSIONS = {
8
6
  viewer: ['view', 'view_highlights', 'view_pdfs'],
@@ -78,7 +76,7 @@ export function getCollaboratorRolePermissions(roleType) {
78
76
  return COLLABORATOR_ROLE_PERMISSIONS[roleType] || [];
79
77
  }
80
78
 
81
- export function canAssignRole(userRole, targetRoleType) {
79
+ export function canAssignRole(userRole, _targetRoleType) {
82
80
  return ['partner', 'manager'].includes(userRole);
83
81
  }
84
82
 
@@ -1,4 +1,4 @@
1
- import { getTransporter, sendEmail } from './email-sender.js';
1
+ import { sendEmail } from './email-sender.js';
2
2
  import { getConfigEngineSync } from '../lib/config-generator-engine.js';
3
3
  import { executeHook } from '../lib/hook-engine.js';
4
4
  import { createLogger } from '../lib/logger.js';
@@ -65,7 +65,7 @@ export async function sendNotification(type, userId, context = {}, options = {})
65
65
  return notification;
66
66
  }
67
67
 
68
- export async function markNotificationRead(notificationId, userId) {
68
+ export async function markNotificationRead(notificationId, _userId) {
69
69
  const { update } = await import('../lib/busybase/store.js');
70
70
  return update('notification', notificationId, {
71
71
  read_at: Math.floor(Date.now() / 1000),
@@ -125,7 +125,7 @@ async function resolveRecipients(recipients, context) {
125
125
  return [];
126
126
  }
127
127
 
128
- function templateNameForType(type) {
128
+ function templateNameForType(_type) {
129
129
  // events-engine.js passes descriptive template names (e.g.
130
130
  // 'engagement_info_gathering'); the built-in getTemplates() set in
131
131
  // email-sender.js is generic ('notification', 'invitation'), so an
@@ -1,6 +1,6 @@
1
1
  // Adapted from moonlanding/src/services/permission.service.js
2
2
 
3
- import { getCollaboratorRole, checkCollaboratorAccess } from '../services/collaborator-role.service.js';
3
+ import { getCollaboratorRole } from '../services/collaborator-role.service.js';
4
4
  import { PermissionError } from '../lib/errors/index.js';
5
5
  import { getConfigEngineSync } from '../lib/config-generator-engine.js';
6
6
 
@@ -1,6 +1,3 @@
1
- import { h } from '@/ui/webjsx.js'
2
- import { TOAST_SCRIPT } from '@/ui/render-helpers.js'
3
-
4
1
  export function dataGridAdvanced(config) {
5
2
  const { columns = [], data = [], groupBy = null, sortable = true, filterable = true, expandable = false, detailRenderer = null } = config
6
3
  const gridId = 'dg-' + Math.random().toString(36).slice(2, 8)
@@ -1,6 +1,6 @@
1
1
  import { generateHtml } from '@/ui/layout.js'
2
2
  import { nav as navFn } from '@/ui/layout.js'
3
- import { AUTH_CSS, GOOGLE_SVG, WARN_SVG, LOGO_SVG, authLogoHtml } from '@/ui/auth-styles.js'
3
+ import { AUTH_CSS, GOOGLE_SVG, WARN_SVG, authLogoHtml } from '@/ui/auth-styles.js'
4
4
  import { esc } from '@/ui/render-helpers.js'
5
5
 
6
6
  export function renderLogin(error = null, hasGoogleAuth = false) {
@@ -122,8 +122,8 @@ export const commonHandlers = {
122
122
 
123
123
  notify: {
124
124
  show(message, type = 'info') {
125
- if (typeof showToast === 'function') {
126
- showToast(message, type);
125
+ if (typeof window.showToast === 'function') {
126
+ window.showToast(message, type);
127
127
  }
128
128
  },
129
129
  success(message) { this.show(message, 'success'); },
@@ -32,7 +32,7 @@ export function renderComponent(componentId, props = {}) {
32
32
  const regex = new RegExp(`{{${key}}}`, 'g');
33
33
  if (value && typeof value === 'object') {
34
34
  if (Array.isArray(value)) {
35
- html = html.replace(regex, value.map(v => renderItem(v, key)).join(''));
35
+ html = html.replace(regex, value.map(v => renderItem(v)).join(''));
36
36
  } else {
37
37
  html = html.replace(regex, esc(JSON.stringify(value)));
38
38
  }
@@ -49,7 +49,7 @@ export function renderComponent(componentId, props = {}) {
49
49
  return `<${element}${cssClass}>${html}</${element}>`;
50
50
  }
51
51
 
52
- function renderItem(item, key) {
52
+ function renderItem(item) {
53
53
  if (typeof item === 'string') return esc(item);
54
54
  if (typeof item === 'object' && item.name && item.color) {
55
55
  const bg = safeColor(item.color);
@@ -1,7 +1,7 @@
1
1
  import { page } from '@/ui/layout.js'
2
2
  import { getQuickActions } from '@/ui/permissions-ui.js'
3
3
  import { esc, stagePill, statusPill, TABLE_SCRIPT, emptyRow } from '@/ui/render-helpers.js'
4
- import { SPACING, renderCard } from '@/ui/spacing-system.js'
4
+ import { SPACING } from '@/ui/spacing-system.js'
5
5
 
6
6
  export function renderDashboard(user, stats = {}) {
7
7
  const isClerk = user?.role === 'clerk';
@@ -1,7 +1,7 @@
1
1
  import { getConfigEngineSync } from '@/lib/config-generator-engine';
2
2
  import { aria, role } from '@/lib/accessibility';
3
3
 
4
- export function renderDialog(dialogId, context = {}) {
4
+ export function renderDialog(dialogId, _context = {}) {
5
5
  const config = getConfigEngineSync().getConfig();
6
6
  const dialogConfig = config.dialogs?.[dialogId];
7
7
 
@@ -10,15 +10,15 @@ export function renderDialog(dialogId, context = {}) {
10
10
  return '';
11
11
  }
12
12
 
13
- const { title, fields = [], actions = [], width = '640px', onSubmit } = dialogConfig;
14
- const dialogHtml = generateDialogHtml(dialogId, title, fields, actions, width, context, onSubmit);
13
+ const { title, fields = [], actions = [], width = '640px' } = dialogConfig;
14
+ const dialogHtml = generateDialogHtml(dialogId, title, fields, actions, width);
15
15
 
16
16
  return dialogHtml;
17
17
  }
18
18
 
19
- function generateDialogHtml(dialogId, title, fields, actions, width, context, onSubmit) {
19
+ function generateDialogHtml(dialogId, title, fields, actions, width) {
20
20
  const titleId = `${dialogId}-title`;
21
- const fieldHtml = fields.map(f => generateFieldHtml(f, dialogId, context)).join('');
21
+ const fieldHtml = fields.map(f => generateFieldHtml(f, dialogId)).join('');
22
22
  const actionButtons = (actions || []).map(a => {
23
23
  if (a.type === 'cancel') {
24
24
  return `<button type="button" class="btn btn-ghost btn-sm" ${aria.label('Cancel dialog')} data-action="closeDialog" data-args='["${dialogId}"]'>${a.label || 'Cancel'}</button>`;
@@ -66,8 +66,8 @@ function generateDialogHtml(dialogId, title, fields, actions, width, context, on
66
66
  `;
67
67
  }
68
68
 
69
- function generateFieldHtml(field, dialogId, context) {
70
- const { name, label, type, required, placeholder, options, value } = field;
69
+ function generateFieldHtml(field, dialogId) {
70
+ const { name, label, type, required, placeholder, options } = field;
71
71
  const fieldId = `${dialogId}-${name}`;
72
72
  const requiredAttr = required ? 'required' : '';
73
73
  const ariaRequired = required ? aria.required() : '';
@@ -92,21 +92,23 @@ function generateFieldHtml(field, dialogId, context) {
92
92
  case 'date':
93
93
  inputHtml = `<input type="date" id="${fieldId}" name="${name}" class="input input-bordered w-full" ${requiredAttr} ${ariaRequired} ${ariaDesc}/>`;
94
94
  break;
95
- case 'select':
95
+ case 'select': {
96
96
  const optionsHtml = (options || []).map(opt =>
97
97
  `<option value="${opt.value}">${escapeHtml(opt.label || opt.value)}</option>`
98
98
  ).join('');
99
99
  inputHtml = `<select id="${fieldId}" name="${name}" class="select select-bordered w-full" ${requiredAttr} ${ariaRequired} ${ariaDesc}><option value="">Select...</option>${optionsHtml}</select>`;
100
100
  break;
101
+ }
101
102
  case 'checkbox':
102
103
  inputHtml = `<label class="flex items-center gap-2"><input type="checkbox" id="${fieldId}" name="${name}" class="checkbox" ${requiredAttr} ${ariaRequired} ${ariaDesc}/><span class="text-sm">${label}</span></label>`;
103
104
  break;
104
- case 'multi-select':
105
+ case 'multi-select': {
105
106
  const multiOptHtml = (options || []).map(opt =>
106
107
  `<label class="flex items-center gap-2"><input type="checkbox" name="${name}" value="${opt.value}" class="checkbox"/><span class="text-sm">${escapeHtml(opt.label || opt.value)}</span></label>`
107
108
  ).join('');
108
109
  inputHtml = `<div class="flex flex-col gap-2" ${role.list}>${multiOptHtml}</div>`;
109
110
  break;
111
+ }
110
112
  case 'file':
111
113
  inputHtml = `<input type="file" id="${fieldId}" name="${name}" class="file-input file-input-bordered w-full" ${requiredAttr} ${ariaRequired} ${ariaDesc}/>`;
112
114
  break;
@@ -1,4 +1,3 @@
1
- import { h } from '@/ui/webjsx.js'
2
1
  import { STAGE_COLORS, statusLabel, esc, icon } from '@/ui/render-helpers.js'
3
2
  import { teamAvatarGroup } from '@/ui/widgets.js'
4
3
  import { canEdit } from '@/ui/permissions-ui.js'
@@ -1,6 +1,4 @@
1
- import { statusLabel } from '@/ui/renderer.js';
2
1
  import { createDialog } from '@/ui/dialog-factory.js';
3
- import { esc } from '@/ui/render-helpers.js';
4
2
 
5
3
  export function engagementFileSearchDialog(engagementId) {
6
4
  const body = `<div class="modal-form-group"><label for="efs-query" class="sr-only">Search files</label><input type="text" id="efs-query" class="input input-bordered w-full" placeholder="Search files..." aria-label="Search engagement files" oninput="efsSearch()"/></div><div id="efs-results" class="flex flex-col gap-2" style="max-height:400px;overflow:auto"></div>`;
@@ -1,5 +1,4 @@
1
- import { h } from '@/ui/webjsx.js'
2
- import { page, confirmDialog, dataTable } from '@/ui/layout.js'
1
+ import { page, dataTable } from '@/ui/layout.js'
3
2
  import { fmtVal, TOAST_SCRIPT, esc } from '@/ui/render-helpers.js'
4
3
  import { canCreate, canEdit, canDelete } from '@/ui/permissions-ui.js'
5
4
 
@@ -7,7 +6,7 @@ export function renderEntityList(entityName, items, spec, user, options = {}) {
7
6
  const label = spec?.labelPlural || spec?.label || entityName
8
7
  const fields = spec?.fields || {}
9
8
  const { groupBy = null } = options
10
- let listFields = Object.entries(fields).filter(([k, f]) => f.list).slice(0, 5)
9
+ let listFields = Object.entries(fields).filter(([, f]) => f.list).slice(0, 5)
11
10
  if (!listFields.length) listFields = Object.entries(fields).filter(([k]) => !['created_by', 'updated_by'].includes(k)).slice(0, 6)
12
11
  if (!listFields.length && items.length > 0) listFields = Object.keys(items[0]).filter(k => !['created_by', 'updated_by'].includes(k)).slice(0, 5).map(k => [k, { label: k }])
13
12
  const headers = listFields.map(([k, f]) => `<th>${esc(f?.label || k)}</th>`).join('') + '<th>Actions</th>'
@@ -260,7 +260,7 @@ const eventDelegation = {
260
260
 
261
261
  navigate(path) { window.location = path; },
262
262
 
263
- toggle(elementId, attr = 'checked') {
263
+ toggle(elementId, _attr = 'checked') {
264
264
  const el = document.getElementById(elementId);
265
265
  if (el?.type === 'checkbox') el.checked = !el.checked;
266
266
  else el.classList.toggle('active');
@@ -1,5 +1,3 @@
1
- import { statusLabel } from '@/ui/renderer.js';
2
-
3
1
  // Inline HTML-escape for client-script innerHTML sinks (file names are DB-sourced).
4
2
  const FD_ESC = `function fdEsc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;')}`;
5
3
 
@@ -1,4 +1,3 @@
1
- import { statusLabel } from '@/ui/renderer.js';
2
1
  import { page } from '@/ui/layout.js';
3
2
  import { isPartner } from '@/ui/permissions-ui.js';
4
3
  import { STATUS_COLORS, esc } from '@/ui/render-helpers.js';
package/src/ui/layout.js CHANGED
@@ -1,6 +1,6 @@
1
- import { h } from '@/ui/webjsx.js'
2
- import { getNavItems, getAdminItems, isPartner, isClerk } from '@/ui/permissions-ui.js'
3
- import { TOAST_SCRIPT, AVATAR_COLORS, esc } from '@/ui/render-helpers.js'
1
+ /* eslint-disable no-useless-escape -- this file embeds raw client <script> text inside a template literal; ESLint parses embedded regex escapes like \d as literal template text and misflags them */
2
+ import { isPartner, isClerk } from '@/ui/permissions-ui.js'
3
+ import { AVATAR_COLORS, esc } from '@/ui/render-helpers.js'
4
4
  import { FETCH_JSON_SCRIPT } from '@/ui/fetch-json.js'
5
5
  import { aria, role, skipLink, liveRegion } from '@/lib/accessibility'
6
6
 
@@ -16,7 +16,7 @@ function withAppName(title) {
16
16
  return base === appName ? base : `${base} | ${appName}`
17
17
  }
18
18
 
19
- export function generateHtml(title, bodyContent, scripts = [], pathname = '/') {
19
+ export function generateHtml(title, bodyContent, scripts = []) {
20
20
  title = withAppName(title)
21
21
  const scriptTags = scripts.map(s =>
22
22
  typeof s === 'string' ? `<script>${s.replace(/<\/script/gi, '<\\/script').replace(/<!--/g, '<\\!--')}</script>` : `<script type="module" src="${s.src}"></script>`
@@ -1,7 +1,7 @@
1
1
  import { list, get } from '@/lib/busybase/store.js';
2
- import { renderAuditDashboard, renderSystemHealth, renderAccessDenied, generateHtml, REDIRECT } from '@/ui/renderer.js';
2
+ import { renderAuditDashboard, renderSystemHealth, renderAccessDenied, generateHtml } from '@/ui/renderer.js';
3
3
  import { renderSettingsHome, renderSettingsSystem, renderSettingsUsers } from '@/ui/settings/home.js';
4
- import { renderSettingsTeams, renderSettingsRfiSections } from '@/ui/settings/teams.js';
4
+ import { renderSettingsRfiSections } from '@/ui/settings/teams.js';
5
5
  import { renderSettingsTemplates, renderSettingsChecklists, renderSettingsEntityTypes, renderSettingsEngagementTypes, renderSettingsTemplateManage } from '@/ui/settings/templates.js';
6
6
  import { renderSettingsNotifications, renderSettingsIntegrations, renderSettingsRecreation, renderSettingsReviewSettings, renderSettingsFileReview, renderSettingsMwrPermissions } from '@/ui/settings/review.js';
7
7
  import { renderChecklistsManagement } from '@/ui/checklist-renderer.js';
@@ -1,4 +1,4 @@
1
- import { list, get, count } from '@/lib/busybase/store.js';
1
+ import { list, count } from '@/lib/busybase/store.js';
2
2
  import { createLogger } from '@/lib/logger.js';
3
3
 
4
4
  const log = createLogger('[PageHandlerHelpers]');
@@ -19,7 +19,7 @@ export function resolveEnumOptions(spec) {
19
19
 
20
20
  export async function getRefOptions(spec) {
21
21
  const refOptions = {};
22
- for (const [fieldKey, field] of Object.entries(spec.fields || {}).filter(([k, f]) => f.type === 'ref' && f.ref)) {
22
+ for (const [fieldKey, field] of Object.entries(spec.fields || {}).filter(([, f]) => f.type === 'ref' && f.ref)) {
23
23
  try { refOptions[fieldKey] = (await list(field.ref, {})).map(r => ({ value: r.id, label: r.name || r.title || r.label || r.email || r.id })); }
24
24
  catch { refOptions[fieldKey] = []; }
25
25
  }
@@ -31,7 +31,7 @@ export async function getRefOptions(spec) {
31
31
  // `_display` key exists (falling back to the raw id) without issuing any query.
32
32
  export function resolveRefFields(items, spec) {
33
33
  try {
34
- const refFields = Object.entries(spec.fields || {}).filter(([k, f]) => f.type === 'ref' && f.ref);
34
+ const refFields = Object.entries(spec.fields || {}).filter(([, f]) => f.type === 'ref' && f.ref);
35
35
  if (!refFields.length) return items;
36
36
  return items.map(item => {
37
37
  const resolved = { ...item };
@@ -1,5 +1,5 @@
1
1
  import { list, get } from '@/lib/busybase/store.js';
2
- import { canView, canList, isClerk, isPartner } from '@/ui/permissions-ui.js';
2
+ import { canView, canList, isClerk } from '@/ui/permissions-ui.js';
3
3
  import { renderAccessDenied } from '@/ui/renderer.js';
4
4
  import { renderRfiList } from '@/ui/rfi-list-renderer.js';
5
5
  import { fileURLToPath } from 'url';
@@ -2,7 +2,7 @@ import { getUser, setCurrentRequest } from '@/engine.server.js';
2
2
  import { hasGoogleAuth } from '@/config/env.js';
3
3
  import { getSpec } from '@/config/spec-helpers.js';
4
4
  import { list, get } from '@/lib/busybase/store.js';
5
- import { renderLogin, renderDashboard, renderEntityList, renderEntityDetail, renderEntityForm, renderAccessDenied, renderPasswordReset, renderPasswordResetConfirm, REDIRECT } from '@/ui/renderer.js';
5
+ import { renderLogin, renderDashboard, renderAccessDenied, renderPasswordReset, renderPasswordResetConfirm, REDIRECT } from '@/ui/renderer.js';
6
6
  import { renderClientDashboard, renderClientList } from '@/ui/client-renderer.js';
7
7
  import { canList, canView, canCreate, canEdit, isPartner, isClerk, isClientUser, canClientAccessEntity } from '@/ui/permissions-ui.js';
8
8
  import { renderEngagementGrid } from '@/ui/engagement-grid-renderer.js';
@@ -1,16 +1,5 @@
1
1
  import { getConfigEngineSync } from '@/lib/config-generator-engine.js';
2
2
 
3
- function getRoleHierarchy() {
4
- const config = getConfigEngineSync();
5
- const roles = config.getRoles();
6
- const hierarchy = {};
7
- let level = 0;
8
- for (const [roleName] of Object.entries(roles)) {
9
- hierarchy[roleName] = level++;
10
- }
11
- return hierarchy;
12
- }
13
-
14
3
  function getEntityPermissions() {
15
4
  const config = getConfigEngineSync();
16
5
  const entityNames = config.getAllEntities();
@@ -40,7 +29,6 @@ function getEntityPermissions() {
40
29
 
41
30
  export function canAccess(user, entity, action) {
42
31
  if (!user?.role) return false;
43
- const config = getConfigEngineSync();
44
32
  const allPermissions = getEntityPermissions();
45
33
  const permissions = allPermissions[entity];
46
34
 
@@ -1,4 +1,3 @@
1
- import { h } from '@/ui/webjsx.js'
2
1
  import { STAGE_COLORS, TOAST_SCRIPT } from '@/ui/render-helpers.js'
3
2
 
4
3
  const DIALOG_COLORS = ['#B0B0B0','#44BBA4','#FF4141','#7F7EFF','#3b82f6','#f59e0b','#ec4899','#8b5cf6','#ef4444','#22c55e','#06b6d4','#f97316','#84cc16','#e11d48','#14b8a6','#6366f1']
@@ -2,7 +2,7 @@ import { statusLabel } from '@/ui/renderer.js';
2
2
  import { page } from '@/ui/layout.js';
3
3
  import { emptyRow, esc } from '../render-helpers.js';
4
4
 
5
- function reviewSummaryPanel(review, highlights, side) {
5
+ function reviewSummaryPanel(review, highlights, _side) {
6
6
  const total = highlights.length;
7
7
  const resolved = highlights.filter(h => h.status === 'resolved').length;
8
8
  const sts = review.status ? statusLabel(review.status) : '';
@@ -2,7 +2,7 @@ import { page } from '@/ui/layout.js';
2
2
  import { reviewZoneNav } from '@/ui/review/zone-nav.js';
3
3
  export { reviewZoneNav };
4
4
  import { canEdit } from '@/ui/permissions-ui.js';
5
- import { esc, statusBadge, TOAST_SCRIPT, icon } from '@/ui/render-helpers.js';
5
+ import { esc, statusBadge, icon } from '@/ui/render-helpers.js';
6
6
  import { reviewDetailScript } from '@/ui/review/detail-script.js';
7
7
  import { highlightRow, collaboratorRow, addCollaboratorDialog } from '@/ui/review/detail-panels.js';
8
8
  import { tenderPanelHtml, tenderDialog, linksPanelHtml, linkDialog, sectionsPanelHtml, compareDialogHtml } from '@/ui/review-detail-panels-extra.js';
@@ -1,6 +1,6 @@
1
1
  import { page } from '@/ui/layout.js';
2
2
  import { reviewZoneNav } from '@/ui/review/zone-nav.js';
3
- import { SPACING, renderCard, renderButton, renderProgress, renderEmptyState, renderStatsRow, renderPageHeader } from '@/ui/spacing-system.js';
3
+ import { SPACING, renderCard, renderButton, renderEmptyState, renderStatsRow, renderPageHeader } from '@/ui/spacing-system.js';
4
4
  import { icon, esc } from '@/ui/format-helpers.js';
5
5
  import { TOAST_SCRIPT } from '@/ui/render-helpers.js';
6
6
 
@@ -1,4 +1,3 @@
1
- import { h } from '@/ui/webjsx.js'
2
1
  import { statusLabel, TOAST_SCRIPT, esc } from '@/ui/render-helpers.js'
3
2
  import { teamAvatarGroup } from '@/ui/widgets.js'
4
3
 
@@ -1,6 +1,6 @@
1
1
  import { page } from '@/ui/layout.js';
2
2
  import { canEdit, isPartner, isManager, isClientUser } from '@/ui/permissions-ui.js';
3
- import { esc, statusBadge, TOAST_SCRIPT, emptyRow } from '@/ui/render-helpers.js';
3
+ import { esc, statusBadge, TOAST_SCRIPT } from '@/ui/render-helpers.js';
4
4
  import { SPACING } from '@/ui/spacing-system.js';
5
5
 
6
6
  const TOAST = TOAST_SCRIPT;
@@ -22,7 +22,7 @@ const SETTINGS_CARDS = [
22
22
  { key: 'permissions', icon: 'lock', title: 'Permissions', desc: 'MWR permissions and access control', href: '/admin/settings/permissions' },
23
23
  ];
24
24
 
25
- export function renderSettingsHome(user, config = {}, counts = {}) {
25
+ export function renderSettingsHome(user, _config = {}, counts = {}) {
26
26
  const cards = SETTINGS_CARDS.map(c => {
27
27
  const cnt = c.countKey && counts[c.countKey] !== undefined ? counts[c.countKey] : null;
28
28
  const badge = cnt !== null ? `<span class="badge badge-flat-primary text-xs">${cnt.toLocaleString('en-ZA')} items</span>` : '';
@@ -181,7 +181,6 @@ export function renderSettingsFileReview(user, config = {}, frSettings = {}) {
181
181
  const fr = config.fileReview || {};
182
182
  const reviewFlags = frSettings.review_flags || [];
183
183
  const tenderFlags = frSettings.tender_flags || [];
184
- const flagManagers = frSettings.flags_managers || [];
185
184
  const tempAccess = frSettings.temp_review_access_period || 0;
186
185
  const toggles = [
187
186
  { id: 'auto_pdf_cache', label: 'Auto-cache PDFs', desc: 'Cache PDF files for faster loading', checked: fr.auto_pdf_cache !== false },
@@ -12,7 +12,7 @@ function parseMembers(t) {
12
12
  } catch { return []; }
13
13
  }
14
14
 
15
- export function renderSettingsTeams(user, teams = [], allUsers = []) {
15
+ export function renderSettingsTeams(user, teams = [], _allUsers = []) {
16
16
  const editDialog = `<div id="team-dialog" class="modal" style="display:none" role="dialog" aria-modal="true" aria-labelledby="team-dialog-title">
17
17
  <div class="modal-overlay" data-dialog-close="team-dialog"></div>
18
18
  <div class="modal-content rounded-box max-w-md p-6">
@@ -93,7 +93,7 @@ export function renderButton(label, opts = {}) {
93
93
  return `<button type="button"${onclickAttr} class="${btnClass}" ${disabled ? 'disabled' : ''} style="${extraStyle}">${label}</button>`;
94
94
  }
95
95
 
96
- export function renderProgress(value, max = 100, variant = 'success', marginTop = SPACING.md) {
96
+ export function renderProgress(value, max = 100, _variant = 'success', marginTop = SPACING.md) {
97
97
  const pct = max > 0 ? Math.round((value / max) * 100) : 0;
98
98
  return `<div style="margin-top:${typeof marginTop === 'string' ? marginTop : SPACING.md}">
99
99
  <div style="display:flex;justify-content:space-between;margin-bottom:${SPACING.sm};align-items:center">