thatcher 1.0.45 → 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 (72) hide show
  1. package/package.json +1 -1
  2. package/src/adapters/google-auth.js +0 -7
  3. package/src/adapters/google-drive.js +1 -3
  4. package/src/app/api/[entity]/[[...path]]/route.js +1 -1
  5. package/src/app/api/audit/logs/route.js +1 -1
  6. package/src/app/api/audit/permissions/route.js +0 -1
  7. package/src/app/api/audit/route.js +2 -4
  8. package/src/app/api/auth/google/callback/route.js +1 -1
  9. package/src/app/api/auth/google/route.js +1 -1
  10. package/src/app/api/auth/logout/route.js +1 -1
  11. package/src/app/api/csrf-token/route.js +1 -1
  12. package/src/app/api/domains/route.js +1 -1
  13. package/src/app/api/email/allocate/route.js +0 -1
  14. package/src/app/api/email/receive/route.js +1 -1
  15. package/src/app/api/health/route.js +1 -1
  16. package/src/cli.js +1 -1
  17. package/src/config/spec-helpers.js +0 -1
  18. package/src/engine.server.js +1 -1
  19. package/src/lib/alert-manager.js +1 -1
  20. package/src/lib/api-helpers.js +1 -1
  21. package/src/lib/auth-route-helpers.js +1 -1
  22. package/src/lib/business-rules-engine.js +3 -3
  23. package/src/lib/busybase/adapter.js +2 -2
  24. package/src/lib/busybase/store.js +1 -1
  25. package/src/lib/config-generator-engine.js +0 -2
  26. package/src/lib/crud-action-helpers.js +1 -1
  27. package/src/lib/crud-handlers.js +2 -2
  28. package/src/lib/csrf-protection.js +0 -1
  29. package/src/lib/debug-registry.js +1 -1
  30. package/src/lib/errors/wrap.js +1 -1
  31. package/src/lib/events-engine.js +1 -1
  32. package/src/lib/field-iterator.js +2 -2
  33. package/src/lib/next-shim.js +2 -2
  34. package/src/lib/perf.js +0 -2
  35. package/src/lib/request-trace.js +1 -4
  36. package/src/lib/stage-pipeline.js +0 -1
  37. package/src/lib/static-server.js +1 -1
  38. package/src/lib/tracing.js +0 -3
  39. package/src/lib/validation-middleware.js +1 -1
  40. package/src/lib/workflow-engine.js +1 -1
  41. package/src/lib/xstate-workflow-engine.js +1 -1
  42. package/src/server/server.js +5 -9
  43. package/src/services/collaborator-role.service.js +1 -3
  44. package/src/services/notification-engine.js +3 -3
  45. package/src/services/permission.service.js +1 -1
  46. package/src/ui/advanced-widgets.js +0 -3
  47. package/src/ui/auth-pages.js +1 -1
  48. package/src/ui/component-engine.js +2 -2
  49. package/src/ui/dashboard-renderer.js +1 -1
  50. package/src/ui/dialog-engine.js +7 -7
  51. package/src/ui/engagement-cards.js +0 -1
  52. package/src/ui/engagement-dialogs.js +0 -2
  53. package/src/ui/entity-renderer.js +2 -3
  54. package/src/ui/event-delegation.js +1 -1
  55. package/src/ui/file-dialogs.js +0 -2
  56. package/src/ui/job-management-renderer.js +0 -1
  57. package/src/ui/layout.js +3 -4
  58. package/src/ui/page-handler-admin.js +2 -2
  59. package/src/ui/page-handler-helpers.js +3 -3
  60. package/src/ui/page-handler-rfi.js +1 -1
  61. package/src/ui/page-handler.js +1 -1
  62. package/src/ui/permissions-ui.js +0 -12
  63. package/src/ui/picker-dialogs.js +0 -1
  64. package/src/ui/review/comparison.js +1 -1
  65. package/src/ui/review/detail-renderer.js +1 -1
  66. package/src/ui/review/mwr.js +1 -1
  67. package/src/ui/review/widgets.js +0 -1
  68. package/src/ui/rfi-detail-renderer.js +1 -1
  69. package/src/ui/settings/home.js +1 -1
  70. package/src/ui/settings/review.js +0 -1
  71. package/src/ui/settings/teams.js +1 -1
  72. package/src/ui/spacing-system.js +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thatcher",
3
- "version": "1.0.45",
3
+ "version": "1.0.46",
4
4
  "description": "A config-driven application framework for building data-intensive web apps without code.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -4,19 +4,12 @@ import { google } from 'googleapis';
4
4
  const log = createLogger('[GoogleAuth]');
5
5
  import { buildConfig } from '../config/env.js';
6
6
  import fs from 'fs';
7
- import path from 'path';
8
7
 
9
8
  let _oauth2Client = null;
10
9
  let _jwtClient = null;
11
10
 
12
11
  export function initGoogleAuth(config = null) {
13
12
  const cfg = config || buildConfig();
14
- const scopes = [
15
- 'https://www.googleapis.com/auth/userinfo.email',
16
- 'https://www.googleapis.com/auth/userinfo.profile',
17
- 'https://www.googleapis.com/auth/drive',
18
- 'https://www.googleapis.com/auth/gmail.send',
19
- ];
20
13
 
21
14
  // OAuth2 client for web flow
22
15
  _oauth2Client = new google.auth.OAuth2(
@@ -4,8 +4,6 @@ import path from 'path';
4
4
  import { getJWTClient, getOAuth2Client } from './google-auth.js';
5
5
  import { buildConfig } from '../config/env.js';
6
6
 
7
- const DRIVE_SCOPES = ['https://www.googleapis.com/auth/drive'];
8
-
9
7
  export function getDriveClient(user = null) {
10
8
  let client;
11
9
 
@@ -26,7 +24,7 @@ export function getDriveClient(user = null) {
26
24
  export async function uploadFile(filePath, name, options = {}, user = null) {
27
25
  const drive = getDriveClient(user);
28
26
  const absPath = path.resolve(filePath);
29
- const fileSize = fs.statSync(absPath).size;
27
+ fs.statSync(absPath);
30
28
 
31
29
  const res = await drive.files.create({
32
30
  requestBody: {
@@ -1,7 +1,7 @@
1
1
  import { createCrudHandlers } from '@/lib/crud-factory';
2
2
  import { setCurrentRequest } from '@/engine.server';
3
3
 
4
- function wrapHandler(method) {
4
+ function wrapHandler(_method) {
5
5
  return async (request, context) => {
6
6
  setCurrentRequest(request);
7
7
  const params = await context.params;
@@ -1,6 +1,6 @@
1
1
  import { NextResponse } from '@/lib/next-shim';
2
2
  import { requireAuth } from '@/lib/auth-middleware';
3
- import { searchLogs, getLogStats, rotateLogsOlderThan } from '@/lib/audit-logger-enhanced';
3
+ import { searchLogs, rotateLogsOlderThan } from '@/lib/audit-logger-enhanced';
4
4
  import { withErrorHandler } from '@/lib/errors';
5
5
 
6
6
  export const GET = withErrorHandler(async (request) => {
@@ -33,7 +33,6 @@ export async function GET(request) {
33
33
  const search = searchParams.get('search');
34
34
  const startDate = searchParams.get('start_date');
35
35
  const endDate = searchParams.get('end_date');
36
- const action = searchParams.get('action');
37
36
  const limit = parseInt(searchParams.get('limit') || '100', 10);
38
37
  const offset = parseInt(searchParams.get('offset') || '0', 10);
39
38
  const format = searchParams.get('format');
@@ -1,13 +1,11 @@
1
- import { requireAuth, requirePermission } from '@/lib/auth-middleware';
1
+ import { requireAuth } from '@/lib/auth-middleware';
2
2
  import { getAuditHistory, getActionStats, getUserStats } from '@/lib/busybase/audit-reads';
3
- import { getSpec } from '@/config/spec-helpers';
4
3
  import { paginated, ok } from '@/lib/response-formatter';
5
4
  import { withErrorHandler, AppError } from '@/lib/errors';
6
5
  import { HTTP } from '@/config/constants';
7
- import { parse as parseQuery } from '@/lib/query-string-adapter';
8
6
 
9
7
  export const GET = withErrorHandler(async (request) => {
10
- const user = await requireAuth();
8
+ await requireAuth();
11
9
  const url = new URL(request.url);
12
10
 
13
11
  const entityType = url.searchParams.get('entityType');
@@ -1,5 +1,5 @@
1
1
  import { createLogger } from '@/lib/logger.js';
2
- import { google, createSession } from '@/engine.server';
2
+ import { createSession } from '@/engine.server';
3
3
 
4
4
  const log = createLogger('[OAuth]');
5
5
  import { getBy, create } from '@/engine';
@@ -50,7 +50,7 @@ export async function GET(request) {
50
50
  });
51
51
  }
52
52
 
53
- export async function HEAD(request) {
53
+ export async function HEAD(_request) {
54
54
  try {
55
55
  const { valid } = validateOAuthProvider(google);
56
56
  return new Response(null, { status: valid ? 200 : 503 });
@@ -8,7 +8,7 @@ function buildLogoutHeaders(headerMap) {
8
8
  : [];
9
9
  }
10
10
 
11
- export async function GET(request, { res } = {}) {
11
+ export async function GET(request) {
12
12
  try {
13
13
  setEngineRequest(request);
14
14
  setCurrentRequest(request);
@@ -2,7 +2,7 @@ import { generateToken } from '@/lib/csrf-protection';
2
2
  import { ok } from '@/lib/response-formatter';
3
3
  import { withErrorHandler } from '@/lib/errors';
4
4
 
5
- export const GET = withErrorHandler(async (request) => {
5
+ export const GET = withErrorHandler(async (_request) => {
6
6
  const token = generateToken();
7
7
  return ok({ csrfToken: token });
8
8
  }, 'CSRF:GetToken');
@@ -6,7 +6,7 @@ import { ok } from '@/lib/response-formatter';
6
6
  import { withErrorHandler } from '@/lib/errors';
7
7
  import { getConfigEngine } from '@/lib/config-generator-engine';
8
8
 
9
- export const GET = withErrorHandler(async (request) => {
9
+ export const GET = withErrorHandler(async (_request) => {
10
10
  await getConfigEngine();
11
11
  const domainLoader = getDomainLoader();
12
12
  const validDomains = domainLoader.getValidDomains();
@@ -7,7 +7,6 @@ const log = createLogger('[EmailAllocate]');
7
7
  import {
8
8
  allocateEmailToEntity,
9
9
  autoAllocateEmail,
10
- validateAllocation,
11
10
  findEntityByAlternateId,
12
11
  } from '@/lib/email-parser';
13
12
 
@@ -143,7 +143,7 @@ export async function POST(request) {
143
143
  }
144
144
  }
145
145
 
146
- export async function GET(request) {
146
+ export async function GET(_request) {
147
147
  return NextResponse.json({
148
148
  endpoint: 'Email Receive Webhook',
149
149
  method: 'POST',
@@ -85,7 +85,7 @@ export const GET = async (request) => {
85
85
  }
86
86
  }
87
87
 
88
- export const HEAD = async (request) => {
88
+ export const HEAD = async (_request) => {
89
89
  try {
90
90
  await count('user', {})
91
91
  return new Response(null, { status: 200 })
package/src/cli.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * thatcher example - Generate example config
12
12
  */
13
13
 
14
- import { startThatcher, Thatcher } from './index.js';
14
+ import { Thatcher } from './index.js';
15
15
  import * as readline from 'readline';
16
16
  import fs from 'fs';
17
17
  import path from 'path';
@@ -135,7 +135,6 @@ export function buildNavigation(configEngine, user = null) {
135
135
  // Filter by user permissions if provided
136
136
  if (user && configEngine.getRolePermissions) {
137
137
  return items.filter(item => {
138
- const spec = configEngine.generateEntitySpec(item.name);
139
138
  const perms = configEngine.getRolePermissions(user.role, item.name);
140
139
  return perms.includes('list') || perms.includes('view') || perms.includes('all');
141
140
  });
@@ -104,7 +104,7 @@ export async function createSession(userId) {
104
104
  }
105
105
 
106
106
  export async function invalidateSession() {
107
- const lucia = getLucia();
107
+ getLucia();
108
108
  // Simplified - caller handles cookie clearing
109
109
  // In full app this uses @lucia-auth/adapter-sqlite's cookie management
110
110
  }
@@ -22,7 +22,7 @@ function checkErrorRate(errorMetrics) {
22
22
  const window = thresholds.errorRate.window
23
23
  let recentErrors = 0
24
24
 
25
- for (const [endpoint, data] of Object.entries(errorMetrics.byEndpoint)) {
25
+ for (const [, data] of Object.entries(errorMetrics.byEndpoint)) {
26
26
  const recent = (data.recent || []).filter(e => (now - e.ts) < window)
27
27
  recentErrors += recent.length
28
28
  }
@@ -30,7 +30,7 @@ export const ok = (data) => NextResponse.json(withMetadata(data, HTTP.OK, 'succe
30
30
  export const created = (data) => NextResponse.json(withMetadata(data, HTTP.CREATED, 'created'), { status: HTTP.CREATED });
31
31
  export const notFound = (entity = 'Resource') => NextResponse.json(withMetadata({ error: ERROR_MESSAGES.notFound(entity) }, HTTP.NOT_FOUND, 'error'), { status: HTTP.NOT_FOUND });
32
32
  export const badRequest = (reason = 'invalid data') => NextResponse.json(withMetadata({ error: ERROR_MESSAGES.invalidRequest(reason) }, HTTP.BAD_REQUEST, 'error'), { status: HTTP.BAD_REQUEST });
33
- export const unauthorized = (action = 'perform this action') => NextResponse.json(withMetadata({ error: ERROR_MESSAGES.permission.denied }, HTTP.FORBIDDEN, 'error'), { status: HTTP.FORBIDDEN });
33
+ export const unauthorized = (_action = 'perform this action') => NextResponse.json(withMetadata({ error: ERROR_MESSAGES.permission.denied }, HTTP.FORBIDDEN, 'error'), { status: HTTP.FORBIDDEN });
34
34
  export const serverError = (msg = null) => NextResponse.json(withMetadata({ error: msg || ERROR_MESSAGES.operationFailed('server operation') }, HTTP.INTERNAL_ERROR, 'error'), { status: HTTP.INTERNAL_ERROR });
35
35
 
36
36
  export async function withEntityAccess(entity, action, handler) {
@@ -23,7 +23,7 @@ export function validateOAuthProvider(provider) {
23
23
  return { valid: true };
24
24
  }
25
25
 
26
- export async function setOAuthCookie(name, value, options = {}) {
26
+ export async function setOAuthCookie(name, value) {
27
27
  const timestamp = Date.now();
28
28
  const key = `oauth-${timestamp}-${Math.random().toString(36).substring(7)}`;
29
29
  const expiresAt = timestamp + (SESSION.cookieMaxAge * 1000);
@@ -11,7 +11,7 @@ export class BusinessRulesEngine {
11
11
  return rule && !this.isDuplicateEngagement(engagement);
12
12
  });
13
13
 
14
- this.registerRule('daysOutstandingNotificationNeeded', (rfi) => {
14
+ this.registerRule('daysOutstandingNotificationNeeded', (_rfi) => {
15
15
  return this.businessRules.rfiDaysOutstanding.enabled;
16
16
  });
17
17
 
@@ -23,7 +23,7 @@ export class BusinessRulesEngine {
23
23
  return true;
24
24
  });
25
25
 
26
- this.registerRule('clientCanRate', (user, engagement) => {
26
+ this.registerRule('clientCanRate', (user, _engagement) => {
27
27
  return user.type === 'client' && user.role === 'client_admin';
28
28
  });
29
29
 
@@ -66,7 +66,7 @@ export class BusinessRulesEngine {
66
66
  return this.businessRules.recreationRules.engagement[interval];
67
67
  }
68
68
 
69
- isDuplicateEngagement(engagement) {
69
+ isDuplicateEngagement(_engagement) {
70
70
  return false;
71
71
  }
72
72
 
@@ -32,7 +32,7 @@ export class BusyBaseAdapter {
32
32
  this._client = this._embedded;
33
33
  logger.info('Embedded mode initialized', { dir: this.config.dir });
34
34
  } else {
35
- const { default: BB, createClient } = await import('busybase');
35
+ const { default: BB } = await import('busybase');
36
36
  this._client = BB(this.config.url, this.config.project);
37
37
  logger.info('Remote mode initialized', { url: this.config.url });
38
38
  }
@@ -106,7 +106,7 @@ export class BusyBaseAdapter {
106
106
  }
107
107
 
108
108
  async unsubscribeAll() {
109
- for (const [name, { channel }] of _realtimeChannels.entries()) {
109
+ for (const [, { channel }] of _realtimeChannels.entries()) {
110
110
  channel.unsubscribe();
111
111
  }
112
112
  _realtimeChannels.clear();
@@ -271,7 +271,7 @@ export async function create(entity, data, user) {
271
271
  // unconditional-write behaviour, unchanged for every existing caller; _version
272
272
  // is added as a new column, ignored by every reader that doesn't ask for it.
273
273
  export async function update(entity, id, data, opts = {}) {
274
- const spec = specOf(entity);
274
+ specOf(entity);
275
275
  const tbl = tableName(entity);
276
276
 
277
277
  const existing = await get(entity, id);
@@ -12,9 +12,7 @@
12
12
  // as of this writing, vs. workflow-engine.js's 2 and
13
13
  // xstate-workflow-engine.js's 2 debug/test-only importers) -- this is live,
14
14
  // central infrastructure, not a candidate for deprecation.
15
- import { load as yamlLoad } from 'js-yaml';
16
15
  import { LRUCache, deepFreeze, deepClone, recursiveResolve } from './config-helpers.js';
17
- import { generateFieldsFromOverrides, ensureFieldLabels } from './config-field-helpers.js';
18
16
 
19
17
  export class ConfigGeneratorEngine {
20
18
  constructor(masterConfig) {
@@ -9,7 +9,7 @@ const ACTION_FIELD_MAPS = {
9
9
  respond: (data, user) => ({ status: 'responded', response: data.response, responded_at: now(), responded_by: user.id }),
10
10
  };
11
11
 
12
- export function resolveActionUpdate(action, data, user, record) {
12
+ export function resolveActionUpdate(action, data, user, _record) {
13
13
  const mapped = ACTION_FIELD_MAPS[action];
14
14
  if (typeof mapped === 'function') return mapped(data, user);
15
15
  if (mapped) {
@@ -3,10 +3,10 @@ import { get, listWithPagination, searchWithPagination, create, update, remove }
3
3
 
4
4
  const log = createLogger('[CRUD]');
5
5
  import { validateEntity, validateUpdate, sanitizeData } from './validation/index.js';
6
- import { requirePermission, getSessionToken } from './auth-middleware.js';
6
+ import { requirePermission } from './auth-middleware.js';
7
7
  import { executeHook } from './hook-engine.js';
8
8
  import { AppError, NotFoundError, ValidationError } from './errors/index.js';
9
- import { ok, created, paginated, noContent, error } from './response-formatter.js';
9
+ import { ok, created, paginated, noContent } from './response-formatter.js';
10
10
  import { HTTP } from '../config/constants.js';
11
11
  import { permissionService } from '../services/permission.service.js';
12
12
  import { parse as parseQuery } from './query-string-adapter.js';
@@ -43,7 +43,6 @@ export async function withCsrfValidation(handler) {
43
43
  }
44
44
 
45
45
  try {
46
- const contentType = request.headers.get('content-type') || '';
47
46
  const token = request.headers.get('x-csrf-token');
48
47
 
49
48
  if (!token || !validateToken(token)) {
@@ -1,6 +1,6 @@
1
1
  import { createLogger } from './logger.js';
2
2
 
3
- const logger = createLogger('[DebugRegistry]');
3
+ createLogger('[DebugRegistry]');
4
4
 
5
5
  class DebugRegistry {
6
6
  constructor() {
@@ -48,7 +48,7 @@ export function wrap(fn, options = {}) {
48
48
  // retryWithBackoff lives here (not recovery.js) because wrap() itself needs it
49
49
  // with no circular dependency; recovery.js imports it back from here.
50
50
  export async function retryWithBackoff(fn, options = {}) {
51
- const { maxAttempts = 3, delay = 1000, backoff = 2, context = {} } = options;
51
+ const { maxAttempts = 3, delay = 1000, backoff = 2 } = options;
52
52
 
53
53
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
54
54
  try {
@@ -175,7 +175,7 @@ export const registerEntityHandlers = () => {
175
175
  create('removed_highlight', { review_id: highlight.review_id, original_id: highlight.id, highlight_data: JSON.stringify(highlight) }, user);
176
176
  });
177
177
 
178
- hookEngine.on('collaborator:afterCreate', async (collaborator, user) => {
178
+ hookEngine.on('collaborator:afterCreate', async (collaborator, _user) => {
179
179
  const review = get('review', collaborator.review_id);
180
180
  const collabUser = get('user', collaborator.user_id);
181
181
  if (review && collabUser) {
@@ -20,13 +20,13 @@ export function getFieldNames(spec) {
20
20
 
21
21
  export function getEditableFields(spec) {
22
22
  return Object.entries(spec.fields || {})
23
- .filter(([key, field]) => !field.readonly && !field.auto && !field.auto_generate)
23
+ .filter(([, field]) => !field.readonly && !field.auto && !field.auto_generate)
24
24
  .map(([key]) => key);
25
25
  }
26
26
 
27
27
  export function getFieldsByType(spec, type) {
28
28
  return Object.entries(spec.fields || {})
29
- .filter(([key, field]) => field.type === type);
29
+ .filter(([, field]) => field.type === type);
30
30
  }
31
31
 
32
32
  export function fieldQuery(spec, predicates, options = {}) {
@@ -262,9 +262,9 @@ export async function cookies() {
262
262
 
263
263
  export function headers() {
264
264
  return {
265
- get: (name) => null,
265
+ get: (_name) => null,
266
266
  getSetCookie: () => [],
267
- has: (name) => false,
267
+ has: (_name) => false,
268
268
  entries: () => [],
269
269
  };
270
270
  }
package/src/lib/perf.js CHANGED
@@ -22,7 +22,6 @@ const logger = createLogger('[Perf]');
22
22
  const _profiles = new Map();
23
23
  const _alerts = [];
24
24
  const MAX_ALERTS = 100;
25
- const BASELINE_WINDOW_MS = 3600000;
26
25
 
27
26
  const DEFAULT_THRESHOLDS = {
28
27
  'http.request': 500,
@@ -119,7 +118,6 @@ export class PerfProfiler {
119
118
  if (!profile) return null;
120
119
 
121
120
  const durations = profile.durations.map(d => d.durationMs).sort((a, b) => a - b);
122
- const count = durations.length;
123
121
 
124
122
  return {
125
123
  operation: profile.operation,
@@ -6,11 +6,8 @@
6
6
 
7
7
  import { tracer } from './tracing.js';
8
8
  import { perfProfiler } from './perf.js';
9
- import { createLogger } from './logger.js';
10
9
  import { recordRequest, recordError } from './metrics-collector.js';
11
10
 
12
- const logger = createLogger('[RequestTracing]');
13
-
14
11
  // ---------------------------------------------------------------------------
15
12
  // From request-tracing.js
16
13
  // ---------------------------------------------------------------------------
@@ -223,7 +220,7 @@ export default withTracing;
223
220
  // ---------------------------------------------------------------------------
224
221
  // From request-tracker.js
225
222
  // ---------------------------------------------------------------------------
226
- function wrapHandler(handler, metadata = {}) {
223
+ function wrapHandler(handler, _metadata = {}) {
227
224
  return async (request, context) => {
228
225
  const start = process.hrtime.bigint();
229
226
  const method = request.method;
@@ -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 = [];
@@ -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) {
@@ -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() : '';
@@ -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,7 +1,6 @@
1
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 { h } from '@/ui/webjsx.js'
3
- import { getNavItems, getAdminItems, isPartner, isClerk } from '@/ui/permissions-ui.js'
4
- import { TOAST_SCRIPT, AVATAR_COLORS, esc } from '@/ui/render-helpers.js'
2
+ import { isPartner, isClerk } from '@/ui/permissions-ui.js'
3
+ import { AVATAR_COLORS, esc } from '@/ui/render-helpers.js'
5
4
  import { FETCH_JSON_SCRIPT } from '@/ui/fetch-json.js'
6
5
  import { aria, role, skipLink, liveRegion } from '@/lib/accessibility'
7
6
 
@@ -17,7 +16,7 @@ function withAppName(title) {
17
16
  return base === appName ? base : `${base} | ${appName}`
18
17
  }
19
18
 
20
- export function generateHtml(title, bodyContent, scripts = [], pathname = '/') {
19
+ export function generateHtml(title, bodyContent, scripts = []) {
21
20
  title = withAppName(title)
22
21
  const scriptTags = scripts.map(s =>
23
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">